From 04905380b94b82b73b343e23e64d53393386d470 Mon Sep 17 00:00:00 2001 From: Julin S <48789920+ju-sh@users.noreply.github.com> Date: Sun, 15 Mar 2020 21:17:05 +0530 Subject: [PATCH 001/245] begin using pathlib --- scripts/asciidoc2html.py | 57 ++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/scripts/asciidoc2html.py b/scripts/asciidoc2html.py index ceac1ff41..58e5da249 100755 --- a/scripts/asciidoc2html.py +++ b/scripts/asciidoc2html.py @@ -19,6 +19,7 @@ # along with qutebrowser. If not, see . """Generate the html documentation based on the asciidoc files.""" +from typing import Optional, List, Tuple import re import os @@ -30,11 +31,14 @@ import shutil import tempfile import argparse import io +import pathlib sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) from scripts import utils +DOC_DIR = pathlib.Path("qutebrowser/html/doc") + class AsciiDoc: @@ -42,24 +46,23 @@ class AsciiDoc: FILES = ['faq', 'changelog', 'contributing', 'quickstart', 'userscripts'] - def __init__(self, asciidoc, website): + def __init__(self, asciidoc: pathlib.Path, website: pathlib.Path) -> None: self._cmd = None - self._asciidoc = asciidoc - self._website = website - self._homedir = None - self._themedir = None - self._tempdir = None - self._failed = False + self._asciidoc: Optional[pathlib.Path] = asciidoc + self._website: list = website + self._homedir: Optional[pathlib.Path] = None + self._themedir: Optional[pathlib.Path] = None + self._tempdir: Optional[pathlib.Path] = None + self._failed: bool = False def prepare(self): """Get the asciidoc command and create the homedir to use.""" self._cmd = self._get_asciidoc_cmd() - self._homedir = tempfile.mkdtemp() - self._themedir = os.path.join( - self._homedir, '.asciidoc', 'themes', 'qute') - self._tempdir = os.path.join(self._homedir, 'tmp') - os.makedirs(self._tempdir) - os.makedirs(self._themedir) + self._homedir = pathlib.Path(tempfile.mkdtemp()) + self._themedir = self._homedir / '.asciidoc' / 'themes' / 'qute' + self._tempdir = self._homedir / 'tmp' + self._tempdir.mkdir(parents=True) + self._themedir.mkdir(parents=True) def cleanup(self): """Clean up the temporary home directory for asciidoc.""" @@ -76,12 +79,11 @@ class AsciiDoc: def _build_docs(self): """Render .asciidoc files to .html sites.""" - files = [('doc/{}.asciidoc'.format(f), - 'qutebrowser/html/doc/{}.html'.format(f)) + files: List[Tuple[pathlib.Path, pathlib.Path]] = [(pathlib.Path('doc/{}.asciidoc'.format(f)), + DOC_DIR / (f + ".html")) for f in self.FILES] - for src in glob.glob('doc/help/*.asciidoc'): - name, _ext = os.path.splitext(os.path.basename(src)) - dst = 'qutebrowser/html/doc/{}.html'.format(name) + for src in pathlib.Path('doc/help/').glob('*.asciidoc'): + dst = DOC_DIR / (src.stem + ".html") files.append((src, dst)) # patch image links to use local copy @@ -94,8 +96,7 @@ class AsciiDoc: asciidoc_args = ['-a', 'source-highlighter=pygments'] for src, dst in files: - src_basename = os.path.basename(src) - modified_src = os.path.join(self._tempdir, src_basename) + modified_src = self._tempdir / src.name with open(modified_src, 'w', encoding='utf-8') as modified_f, \ open(src, 'r', encoding='utf-8') as f: for line in f: @@ -189,7 +190,8 @@ class AsciiDoc: def _build_website(self): """Prepare and build the website.""" - theme_file = os.path.abspath(os.path.join('www', 'qute.css')) + #theme_file = os.path.abspath(os.path.join('www', 'qute.css')) + theme_file = (pathlib.Path('www') / 'qute.css').resolve() shutil.copy(theme_file, self._themedir) outdir = self._website[0] @@ -243,7 +245,7 @@ class AsciiDoc: raise FileNotFoundError - def call(self, src, dst, *args): + def call(self, src: pathlib.Path, dst: pathlib.Path, *args): """Call asciidoc for the given files. Args: @@ -251,15 +253,18 @@ class AsciiDoc: dst: The destination .html file, or None to auto-guess. *args: Additional arguments passed to asciidoc. """ - print("Calling asciidoc for {}...".format(os.path.basename(src))) + #print("Calling asciidoc for {}...".format(os.path.basename(src))) + src = pathlib.Path(src) + dst = pathlib.Path(dst) + print("Calling asciidoc for {}...".format(src.name)) cmdline = self._cmd[:] if dst is not None: - cmdline += ['--out-file', dst] + cmdline += ['--out-file', str(dst)] cmdline += args - cmdline.append(src) + cmdline.append(str(src)) try: env = os.environ.copy() - env['HOME'] = self._homedir + env['HOME'] = str(self._homedir) subprocess.run(cmdline, check=True, env=env) except (subprocess.CalledProcessError, OSError) as e: self._failed = True From c77757125499369a68263e06292e682f4efe86f3 Mon Sep 17 00:00:00 2001 From: Julin S <48789920+ju-sh@users.noreply.github.com> Date: Sun, 15 Mar 2020 21:38:30 +0530 Subject: [PATCH 002/245] finish _build_website() --- scripts/asciidoc2html.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/scripts/asciidoc2html.py b/scripts/asciidoc2html.py index 58e5da249..cd91f65c9 100755 --- a/scripts/asciidoc2html.py +++ b/scripts/asciidoc2html.py @@ -190,24 +190,20 @@ class AsciiDoc: def _build_website(self): """Prepare and build the website.""" - #theme_file = os.path.abspath(os.path.join('www', 'qute.css')) theme_file = (pathlib.Path('www') / 'qute.css').resolve() shutil.copy(theme_file, self._themedir) - outdir = self._website[0] + outdir = pathlib.Path(self._website[0]) - for root, _dirs, files in os.walk(os.getcwd()): - for filename in files: - basename, ext = os.path.splitext(filename) - if (ext != '.asciidoc' or - basename in ['header', 'OpenSans-License']): - continue - self._build_website_file(root, filename) + for item_path in pathlib.Path().rglob('*.asciidoc'): + if item_path.stem in ['header', 'OpenSans-License']: + continue + self._build_website_file(item_path.parent, item_path.name) copy = {'icons': 'icons', 'doc/img': 'doc/img', 'www/media': 'media/'} for src, dest in copy.items(): - full_dest = os.path.join(outdir, dest) + full_dest = outdir / dest try: shutil.rmtree(full_dest) except FileNotFoundError: @@ -216,9 +212,9 @@ class AsciiDoc: for dst, link_name in [ ('README.html', 'index.html'), - (os.path.join('doc', 'quickstart.html'), 'quickstart.html')]: + ((pathlib.Path('doc') / 'quickstart.html'), 'quickstart.html')]: try: - os.symlink(dst, os.path.join(outdir, link_name)) + (outdir / link_name).symlink_to(dst) except FileExistsError: pass From e298992ec1b18c3fe516471a46f52b3a8c0bb174 Mon Sep 17 00:00:00 2001 From: Julin S <48789920+ju-sh@users.noreply.github.com> Date: Sun, 15 Mar 2020 22:27:39 +0530 Subject: [PATCH 003/245] finish _build_website_file() --- scripts/asciidoc2html.py | 47 ++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/scripts/asciidoc2html.py b/scripts/asciidoc2html.py index cd91f65c9..fac0c4607 100755 --- a/scripts/asciidoc2html.py +++ b/scripts/asciidoc2html.py @@ -105,7 +105,7 @@ class AsciiDoc: modified_f.write(line) self.call(modified_src, dst, *asciidoc_args) - def _copy_images(self): + def _copy_images(self) -> None: """Copy image files to qutebrowser/html/doc.""" print("Copying files...") dst_path = os.path.join('qutebrowser', 'html', 'doc', 'img') @@ -118,21 +118,29 @@ class AsciiDoc: dst = os.path.join(dst_path, filename) shutil.copy(src, dst) - def _build_website_file(self, root, filename): + def _build_website_file(self, root: pathlib.Path, filename: str) -> None: """Build a single website file.""" - src = os.path.join(root, filename) - src_basename = os.path.basename(src) - parts = [self._website[0]] - dirname = os.path.dirname(src) - if dirname: - parts.append(os.path.relpath(os.path.dirname(src))) - parts.append( - os.extsep.join((os.path.splitext(src_basename)[0], - 'html'))) - dst = os.path.join(*parts) - os.makedirs(os.path.dirname(dst), exist_ok=True) + #src = os.path.join(root, filename) + src = root / filename + src_basename = src.name + dst = pathlib.Path(self._website[0]) + #parts = [self._website[0]] + dirname = src.parent + dst = src.parent.relative_to('.') / (src.stem + ".html") + dst.parent.mkdir(exist_ok=True) + + #if len(dirname.parents) > 0: + # parts.append(os.path.relpath(dirname)) + #parts.append(os.extsep.join(src.stem, 'html')) #WHY CAN'T WE MAKE IT A SIMPLE +??? + + #parts.append( + # os.extsep.join((os.path.splitext(src_basename)[0], + # 'html'))) + #dst = os.path.join(*parts) + #os.makedirs(os.path.dirname(dst), exist_ok=True) - modified_src = os.path.join(self._tempdir, src_basename) + #modified_src = os.path.join(self._tempdir, src.name) + modified_src = self._tempdir / src.name shutil.copy('www/header.asciidoc', modified_src) outfp = io.StringIO() @@ -188,7 +196,7 @@ class AsciiDoc: '-a', 'source-highlighter=pygments'] self.call(modified_src, dst, *asciidoc_args) - def _build_website(self): + def _build_website(self) -> None: """Prepare and build the website.""" theme_file = (pathlib.Path('www') / 'qute.css').resolve() shutil.copy(theme_file, self._themedir) @@ -281,12 +289,9 @@ def parse_args(): return parser.parse_args() -def run(**kwargs): +def run(**kwargs) -> None: """Regenerate documentation.""" - try: - os.mkdir('qutebrowser/html/doc') - except FileExistsError: - pass + DOC_DIR.mkdir(exist_ok=True) asciidoc = AsciiDoc(**kwargs) try: @@ -303,7 +308,7 @@ def run(**kwargs): asciidoc.cleanup() -def main(colors=False): +def main(colors: bool = False) -> None: """Generate html files for the online documentation.""" utils.change_cwd() utils.use_color = colors From 6fab7845f0ce3f77037d73a928fc2a973ddf12f5 Mon Sep 17 00:00:00 2001 From: Julin S <48789920+ju-sh@users.noreply.github.com> Date: Sun, 15 Mar 2020 23:09:11 +0530 Subject: [PATCH 004/245] add pathlib to asciidoc2html script --- scripts/asciidoc2html.py | 61 +++++++++++++++------------------------- 1 file changed, 23 insertions(+), 38 deletions(-) diff --git a/scripts/asciidoc2html.py b/scripts/asciidoc2html.py index fac0c4607..ce00043fe 100755 --- a/scripts/asciidoc2html.py +++ b/scripts/asciidoc2html.py @@ -19,7 +19,7 @@ # along with qutebrowser. If not, see . """Generate the html documentation based on the asciidoc files.""" -from typing import Optional, List, Tuple +from typing import List, Tuple import re import os @@ -46,16 +46,20 @@ class AsciiDoc: FILES = ['faq', 'changelog', 'contributing', 'quickstart', 'userscripts'] - def __init__(self, asciidoc: pathlib.Path, website: pathlib.Path) -> None: + def __init__(self, asciidoc, website) -> None: + """ + asciidoc: Optional[List[str]] + website: Optional[List[str]]) + """ self._cmd = None - self._asciidoc: Optional[pathlib.Path] = asciidoc - self._website: list = website - self._homedir: Optional[pathlib.Path] = None - self._themedir: Optional[pathlib.Path] = None - self._tempdir: Optional[pathlib.Path] = None - self._failed: bool = False + self._asciidoc = asciidoc + self._website = website + self._homedir = None + self._themedir = None + self._tempdir = None + self._failed = False - def prepare(self): + def prepare(self) -> None: """Get the asciidoc command and create the homedir to use.""" self._cmd = self._get_asciidoc_cmd() self._homedir = pathlib.Path(tempfile.mkdtemp()) @@ -64,12 +68,12 @@ class AsciiDoc: self._tempdir.mkdir(parents=True) self._themedir.mkdir(parents=True) - def cleanup(self): + def cleanup(self) -> None: """Clean up the temporary home directory for asciidoc.""" if self._homedir is not None and not self._failed: shutil.rmtree(self._homedir) - def build(self): + def build(self) -> None: """Build either the website or the docs.""" if self._website: self._build_website() @@ -77,7 +81,7 @@ class AsciiDoc: self._build_docs() self._copy_images() - def _build_docs(self): + def _build_docs(self) -> None: """Render .asciidoc files to .html sites.""" files: List[Tuple[pathlib.Path, pathlib.Path]] = [(pathlib.Path('doc/{}.asciidoc'.format(f)), DOC_DIR / (f + ".html")) @@ -108,38 +112,22 @@ class AsciiDoc: def _copy_images(self) -> None: """Copy image files to qutebrowser/html/doc.""" print("Copying files...") - dst_path = os.path.join('qutebrowser', 'html', 'doc', 'img') - try: - os.mkdir(dst_path) - except FileExistsError: - pass + dst_path = DOC_DIR / 'img' + dst_path.mkdir(exist_ok=True) for filename in ['cheatsheet-big.png', 'cheatsheet-small.png']: - src = os.path.join('doc', 'img', filename) - dst = os.path.join(dst_path, filename) + src = pathlib.Path('doc') / 'img' / filename + dst = dst_path / filename shutil.copy(src, dst) def _build_website_file(self, root: pathlib.Path, filename: str) -> None: """Build a single website file.""" - #src = os.path.join(root, filename) src = root / filename src_basename = src.name dst = pathlib.Path(self._website[0]) - #parts = [self._website[0]] dirname = src.parent dst = src.parent.relative_to('.') / (src.stem + ".html") dst.parent.mkdir(exist_ok=True) - #if len(dirname.parents) > 0: - # parts.append(os.path.relpath(dirname)) - #parts.append(os.extsep.join(src.stem, 'html')) #WHY CAN'T WE MAKE IT A SIMPLE +??? - - #parts.append( - # os.extsep.join((os.path.splitext(src_basename)[0], - # 'html'))) - #dst = os.path.join(*parts) - #os.makedirs(os.path.dirname(dst), exist_ok=True) - - #modified_src = os.path.join(self._tempdir, src.name) modified_src = self._tempdir / src.name shutil.copy('www/header.asciidoc', modified_src) @@ -222,11 +210,11 @@ class AsciiDoc: ('README.html', 'index.html'), ((pathlib.Path('doc') / 'quickstart.html'), 'quickstart.html')]: try: - (outdir / link_name).symlink_to(dst) + (outdir / link_name).symlink_to(dst) # mypy gives error here. Not sure why except FileExistsError: pass - def _get_asciidoc_cmd(self): + def _get_asciidoc_cmd(self): # -> List[str] """Try to find out what commandline to use to invoke asciidoc.""" if self._asciidoc is not None: return self._asciidoc @@ -257,9 +245,6 @@ class AsciiDoc: dst: The destination .html file, or None to auto-guess. *args: Additional arguments passed to asciidoc. """ - #print("Calling asciidoc for {}...".format(os.path.basename(src))) - src = pathlib.Path(src) - dst = pathlib.Path(dst) print("Calling asciidoc for {}...".format(src.name)) cmdline = self._cmd[:] if dst is not None: @@ -277,7 +262,7 @@ class AsciiDoc: sys.exit(1) -def parse_args(): +def parse_args() -> argparse.Namespace: """Parse command-line arguments.""" parser = argparse.ArgumentParser() parser.add_argument('--website', help="Build website into a given " From 0d94278967b63630f7c50a719f765c636a1e7e44 Mon Sep 17 00:00:00 2001 From: Julin S <48789920+ju-sh@users.noreply.github.com> Date: Sun, 19 Apr 2020 14:17:16 +0530 Subject: [PATCH 005/245] add type annotations to asciidoc2html script --- scripts/asciidoc2html.py | 46 ++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/scripts/asciidoc2html.py b/scripts/asciidoc2html.py index ce00043fe..cf1d2b200 100755 --- a/scripts/asciidoc2html.py +++ b/scripts/asciidoc2html.py @@ -19,14 +19,12 @@ # along with qutebrowser. If not, see . """Generate the html documentation based on the asciidoc files.""" -from typing import List, Tuple - +from typing import List, Tuple, Optional import re import os import os.path import sys import subprocess -import glob import shutil import tempfile import argparse @@ -46,17 +44,15 @@ class AsciiDoc: FILES = ['faq', 'changelog', 'contributing', 'quickstart', 'userscripts'] - def __init__(self, asciidoc, website) -> None: - """ - asciidoc: Optional[List[str]] - website: Optional[List[str]]) - """ - self._cmd = None + def __init__(self, + asciidoc: Optional[List[str]], + website: Optional[str]) -> None: + self._cmd = None # type: Optional[List[str]] self._asciidoc = asciidoc self._website = website - self._homedir = None - self._themedir = None - self._tempdir = None + self._homedir = None # type: Optional[pathlib.Path] + self._themedir = None # type: Optional[pathlib.Path] + self._tempdir = None # type: Optional[pathlib.Path] self._failed = False def prepare(self) -> None: @@ -83,9 +79,8 @@ class AsciiDoc: def _build_docs(self) -> None: """Render .asciidoc files to .html sites.""" - files: List[Tuple[pathlib.Path, pathlib.Path]] = [(pathlib.Path('doc/{}.asciidoc'.format(f)), - DOC_DIR / (f + ".html")) - for f in self.FILES] + files = [(pathlib.Path('doc/{}.asciidoc'.format(f)), + DOC_DIR / (f + ".html")) for f in self.FILES] for src in pathlib.Path('doc/help/').glob('*.asciidoc'): dst = DOC_DIR / (src.stem + ".html") files.append((src, dst)) @@ -100,6 +95,7 @@ class AsciiDoc: asciidoc_args = ['-a', 'source-highlighter=pygments'] for src, dst in files: + assert self._tempdir is not None # for mypy modified_src = self._tempdir / src.name with open(modified_src, 'w', encoding='utf-8') as modified_f, \ open(src, 'r', encoding='utf-8') as f: @@ -122,12 +118,12 @@ class AsciiDoc: def _build_website_file(self, root: pathlib.Path, filename: str) -> None: """Build a single website file.""" src = root / filename - src_basename = src.name - dst = pathlib.Path(self._website[0]) - dirname = src.parent + assert self._website is not None # for mypy + dst = pathlib.Path(self._website) dst = src.parent.relative_to('.') / (src.stem + ".html") dst.parent.mkdir(exist_ok=True) - + + assert self._tempdir is not None # for mypy modified_src = self._tempdir / src.name shutil.copy('www/header.asciidoc', modified_src) @@ -187,9 +183,11 @@ class AsciiDoc: def _build_website(self) -> None: """Prepare and build the website.""" theme_file = (pathlib.Path('www') / 'qute.css').resolve() + assert self._themedir is not None # for mypy shutil.copy(theme_file, self._themedir) - outdir = pathlib.Path(self._website[0]) + assert self._website is not None # for mypy + outdir = pathlib.Path(self._website) for item_path in pathlib.Path().rglob('*.asciidoc'): if item_path.stem in ['header', 'OpenSans-License']: @@ -209,12 +207,13 @@ class AsciiDoc: for dst, link_name in [ ('README.html', 'index.html'), ((pathlib.Path('doc') / 'quickstart.html'), 'quickstart.html')]: + assert isinstance(dst, (str, pathlib.Path)) # for mypy try: - (outdir / link_name).symlink_to(dst) # mypy gives error here. Not sure why + (outdir / link_name).symlink_to(dst) except FileExistsError: pass - def _get_asciidoc_cmd(self): # -> List[str] + def _get_asciidoc_cmd(self) -> List[str]: """Try to find out what commandline to use to invoke asciidoc.""" if self._asciidoc is not None: return self._asciidoc @@ -246,6 +245,7 @@ class AsciiDoc: *args: Additional arguments passed to asciidoc. """ print("Calling asciidoc for {}...".format(src.name)) + assert self._cmd is not None # for mypy cmdline = self._cmd[:] if dst is not None: cmdline += ['--out-file', str(dst)] @@ -266,7 +266,7 @@ def parse_args() -> argparse.Namespace: """Parse command-line arguments.""" parser = argparse.ArgumentParser() parser.add_argument('--website', help="Build website into a given " - "directory.", nargs=1) + "directory.") parser.add_argument('--asciidoc', help="Full path to python and " "asciidoc.py. If not given, it's searched in PATH.", nargs=2, required=False, From 9914e448dc2362d88a40765360963133194563cd Mon Sep 17 00:00:00 2001 From: Julin S <48789920+ju-sh@users.noreply.github.com> Date: Sun, 19 Apr 2020 14:19:58 +0530 Subject: [PATCH 006/245] add blank line between module docstring and imports --- scripts/asciidoc2html.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/asciidoc2html.py b/scripts/asciidoc2html.py index cf1d2b200..e792792d9 100755 --- a/scripts/asciidoc2html.py +++ b/scripts/asciidoc2html.py @@ -19,6 +19,7 @@ # along with qutebrowser. If not, see . """Generate the html documentation based on the asciidoc files.""" + from typing import List, Tuple, Optional import re import os From 680d6ebb583bef77b9b28d0fede471a6efa0fdba Mon Sep 17 00:00:00 2001 From: Julin S <48789920+ju-sh@users.noreply.github.com> Date: Sun, 19 Apr 2020 17:04:36 +0530 Subject: [PATCH 007/245] fix pylint error --- scripts/asciidoc2html.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/asciidoc2html.py b/scripts/asciidoc2html.py index e792792d9..fea106979 100755 --- a/scripts/asciidoc2html.py +++ b/scripts/asciidoc2html.py @@ -81,7 +81,7 @@ class AsciiDoc: def _build_docs(self) -> None: """Render .asciidoc files to .html sites.""" files = [(pathlib.Path('doc/{}.asciidoc'.format(f)), - DOC_DIR / (f + ".html")) for f in self.FILES] + DOC_DIR / (f + ".html")) for f in self.FILES] for src in pathlib.Path('doc/help/').glob('*.asciidoc'): dst = DOC_DIR / (src.stem + ".html") files.append((src, dst)) From fd9e0f19c6153ab04bc5940212294e96f805fe5c Mon Sep 17 00:00:00 2001 From: Julin S <48789920+ju-sh@users.noreply.github.com> Date: Mon, 20 Apr 2020 13:30:23 +0530 Subject: [PATCH 008/245] fix another pylint error --- scripts/asciidoc2html.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/asciidoc2html.py b/scripts/asciidoc2html.py index fea106979..90b94f014 100755 --- a/scripts/asciidoc2html.py +++ b/scripts/asciidoc2html.py @@ -20,7 +20,7 @@ """Generate the html documentation based on the asciidoc files.""" -from typing import List, Tuple, Optional +from typing import List, Optional import re import os import os.path @@ -207,7 +207,8 @@ class AsciiDoc: for dst, link_name in [ ('README.html', 'index.html'), - ((pathlib.Path('doc') / 'quickstart.html'), 'quickstart.html')]: + ((pathlib.Path('doc') / 'quickstart.html'), + 'quickstart.html')]: assert isinstance(dst, (str, pathlib.Path)) # for mypy try: (outdir / link_name).symlink_to(dst) From fb19a2815d2eaec1dd5a58939863f50e2e0aecd8 Mon Sep 17 00:00:00 2001 From: svetelna Date: Mon, 20 Apr 2020 15:42:04 +0200 Subject: [PATCH 009/245] implementing visual-line-mode on webengine side --- qutebrowser/browser/browsertab.py | 24 ++++- qutebrowser/browser/webengine/webenginetab.py | 17 +++- qutebrowser/components/caretcommands.py | 10 +- qutebrowser/config/configdata.yml | 1 + qutebrowser/javascript/caret.js | 91 +++++++++++++++---- qutebrowser/mainwindow/statusbar/bar.py | 12 ++- qutebrowser/mainwindow/tabbedbrowser.py | 2 +- 7 files changed, 127 insertions(+), 30 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 00d46c813..16ddeef8b 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -427,13 +427,22 @@ class AbstractZoom(QObject): self._set_factor_internal(self._zoom_factor) +class SelectionState(enum.IntEnum): + + """Possible states of selection in Caret mode.""" + + none = 1 + normal = 2 + line = 3 + + class AbstractCaret(QObject): """Attribute ``caret`` of AbstractTab for caret browsing.""" #: Signal emitted when the selection was toggled. #: (argument - whether the selection is now active) - selection_toggled = pyqtSignal(bool) + selection_toggled = pyqtSignal(SelectionState) #: Emitted when a ``follow_selection`` action is done. follow_selected_done = pyqtSignal() @@ -444,7 +453,7 @@ class AbstractCaret(QObject): super().__init__(parent) self._tab = tab self._widget = typing.cast(QWidget, None) - self.selection_enabled = False + self.selection_state = SelectionState.none self._mode_manager = mode_manager mode_manager.entered.connect(self._on_mode_entered) mode_manager.left.connect(self._on_mode_left) @@ -500,7 +509,7 @@ class AbstractCaret(QObject): def move_to_end_of_document(self) -> None: raise NotImplementedError - def toggle_selection(self) -> None: + def toggle_selection(self, line: bool = False) -> None: raise NotImplementedError def drop_selection(self) -> None: @@ -826,6 +835,15 @@ class AbstractTabPrivate: def shutdown(self) -> None: raise NotImplementedError + def run_js_sync(self, code: str) -> None: + """Run javascript sync. + + Result will be returned when running JS is complete. + This is only implemented for QtWebKit. + For QtWebEngine, always raises UnsupportedOperationError. + """ + raise NotImplementedError + class AbstractTab(QWidget): diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 647fa60ab..5b0721c18 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -369,7 +369,10 @@ class WebEngineCaret(browsertab.AbstractCaret): if enabled is None: log.webview.debug("Ignoring selection status None") return - self.selection_toggled.emit(enabled) + if enabled: + self.selection_toggled.emit(browsertab.SelectionState.normal) + else: + self.selection_toggled.emit(browsertab.SelectionState.none) @pyqtSlot(usertypes.KeyMode) def _on_mode_left(self, mode): @@ -424,8 +427,9 @@ class WebEngineCaret(browsertab.AbstractCaret): def move_to_end_of_document(self): self._js_call('moveToEndOfDocument') - def toggle_selection(self): - self._js_call('toggleSelection', callback=self.selection_toggled.emit) + def toggle_selection(self, line=False): + self._js_call('toggleSelection', line, + callback=self._toggle_sel_translate) def drop_selection(self): self._js_call('dropSelection') @@ -500,6 +504,10 @@ class WebEngineCaret(browsertab.AbstractCaret): code = javascript.assemble('caret', command, *args) self._tab.run_js_async(code, callback) + def _toggle_sel_translate(self, state_int): + state = browsertab.SelectionState(state_int) + self.selection_toggled.emit(state) + class WebEngineScroller(browsertab.AbstractScroller): @@ -1231,6 +1239,9 @@ class WebEngineTabPrivate(browsertab.AbstractTabPrivate): self._tab.action.exit_fullscreen() self._widget.shutdown() + def run_js_sync(self, code): + raise browsertab.UnsupportedOperationError + class WebEngineTab(browsertab.AbstractTab): diff --git a/qutebrowser/components/caretcommands.py b/qutebrowser/components/caretcommands.py index 173653bd9..966b193de 100644 --- a/qutebrowser/components/caretcommands.py +++ b/qutebrowser/components/caretcommands.py @@ -185,9 +185,13 @@ def move_to_end_of_document(tab: apitypes.Tab) -> None: @cmdutils.register(modes=[cmdutils.KeyMode.caret]) @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) -def toggle_selection(tab: apitypes.Tab) -> None: - """Toggle caret selection mode.""" - tab.caret.toggle_selection() +def toggle_selection(tab: apitypes.Tab, line: bool = False) -> None: + """Toggle caret selection mode. + + Args: + line: Enables line-selection. + """ + tab.caret.toggle_selection(line) @cmdutils.register(modes=[cmdutils.KeyMode.caret]) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 38db52304..df4efee06 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -3051,6 +3051,7 @@ bindings.default: : leave-mode caret: v: toggle-selection + V: toggle-selection --line : toggle-selection : drop-selection c: enter-mode normal diff --git a/qutebrowser/javascript/caret.js b/qutebrowser/javascript/caret.js index 55ff6a8b5..6d7a3bb00 100644 --- a/qutebrowser/javascript/caret.js +++ b/qutebrowser/javascript/caret.js @@ -705,6 +705,16 @@ window._qutebrowser.caret = (function() { */ CaretBrowsing.isCaretVisible = false; + /** + * selection modes + * @type {enum} + */ + CaretBrowsing.SelectionState = { + "NONE": 1, + "NORMAL": 2, + "LINE": 3, + }; + /** * The actual caret element, an absolute-positioned flashing line. * @type {Element} @@ -887,7 +897,11 @@ window._qutebrowser.caret = (function() { CaretBrowsing.injectCaretStyles(); CaretBrowsing.toggle(); CaretBrowsing.initiated = true; - CaretBrowsing.selectionEnabled = selectionRange > 0; + if (selectionRange > 0) { + CaretBrowsing.selectionState = CaretBrowsing.SelectionState.NORMAL; + } else { + CaretBrowsing.selectionState = CaretBrowsing.SelectionState.NONE; + } }; /** @@ -1145,16 +1159,52 @@ window._qutebrowser.caret = (function() { } }; + CaretBrowsing.reverseSelection = () => { + const sel = window.getSelection(); + sel.setBaseAndExtent( + sel.extentNode, sel.extentOffset, sel.baseNode, + sel.baseOffset + ); + }; + + CaretBrowsing.selectLine = function() { + const sel = window.getSelection(); + sel.modify("extend", "right", "lineboundary"); + CaretBrowsing.reverseSelection(); + sel.modify("extend", "left", "lineboundary"); + CaretBrowsing.reverseSelection(); + }; + + CaretBrowsing.updateLineSelection = function(direction, granularity) { + if (!(granularity === "character") && !(granularity === "word")) { + window. + getSelection(). + modify("extend", direction, granularity); + CaretBrowsing.selectLine(); + } + }; + + CaretBrowsing.selectionEnabled = function() { + if (CaretBrowsing.selectionState === CaretBrowsing.SelectionState.NONE) { + return false; + } + return true; + }; + CaretBrowsing.move = function(direction, granularity, count = 1) { let action = "move"; - if (CaretBrowsing.selectionEnabled) { + if (CaretBrowsing.selectionState !== CaretBrowsing.SelectionState.NONE) { action = "extend"; } for (let i = 0; i < count; i++) { - window. - getSelection(). - modify(action, direction, granularity); + if (CaretBrowsing.selectionState === CaretBrowsing.SelectionState.LINE) { + CaretBrowsing.updateLineSelection(direction, granularity); + } else { + window. + getSelection(). + modify(action, direction, granularity); + } } if (CaretBrowsing.isWindows && @@ -1174,7 +1224,7 @@ window._qutebrowser.caret = (function() { CaretBrowsing.moveToBlock = function(paragraph, boundary, count = 1) { let action = "move"; - if (CaretBrowsing.selectionEnabled) { + if (CaretBrowsing.selectionState !== CaretBrowsing.SelectionState.NONE) { action = "extend"; } for (let i = 0; i < count; i++) { @@ -1185,6 +1235,10 @@ window._qutebrowser.caret = (function() { window. getSelection(). modify(action, boundary, "paragraphboundary"); + + if (CaretBrowsing.selectionState === CaretBrowsing.SelectionState.LINE) { + CaretBrowsing.selectLine(); + } } }; @@ -1294,14 +1348,14 @@ window._qutebrowser.caret = (function() { funcs.setInitialCursor = () => { if (!CaretBrowsing.initiated) { CaretBrowsing.setInitialCursor(); - return CaretBrowsing.selectionEnabled; + return CaretBrowsing.selectionEnabled(); } if (window.getSelection().toString().length === 0) { positionCaret(); } CaretBrowsing.toggle(); - return CaretBrowsing.selectionEnabled; + return CaretBrowsing.selectionEnabled(); }; funcs.setFlags = (flags) => { @@ -1399,17 +1453,22 @@ window._qutebrowser.caret = (function() { funcs.getSelection = () => window.getSelection().toString(); - funcs.toggleSelection = () => { - CaretBrowsing.selectionEnabled = !CaretBrowsing.selectionEnabled; - return CaretBrowsing.selectionEnabled; + funcs.toggleSelection = (line) => { + if (line) { + CaretBrowsing.selectionState = + CaretBrowsing.SelectionState.LINE; + CaretBrowsing.selectLine(); + CaretBrowsing.finishMove(); + } else if (CaretBrowsing.selectionState === CaretBrowsing.SelectionState.NONE) { + CaretBrowsing.selectionState = CaretBrowsing.SelectionState.NORMAL; + } else { + CaretBrowsing.selectionState = CaretBrowsing.SelectionState.NONE; + } + return CaretBrowsing.selectionState; }; funcs.reverseSelection = () => { - const sel = window.getSelection(); - sel.setBaseAndExtent( - sel.extentNode, sel.extentOffset, sel.baseNode, - sel.baseOffset - ); + CaretBrowsing.reverseSelection(); }; return funcs; diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index b1aa4da38..95022803d 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -372,13 +372,17 @@ class StatusBar(QWidget): self.maybe_hide() assert tab.is_private == self._color_flags.private - @pyqtSlot(bool) - def on_caret_selection_toggled(self, selection): + @pyqtSlot(browsertab.SelectionState) + def on_caret_selection_toggled(self, selection_state): """Update the statusbar when entering/leaving caret selection mode.""" - log.statusbar.debug("Setting caret selection {}".format(selection)) - if selection: + log.statusbar.debug("Setting caret selection {}" + .format(selection_state)) + if selection_state is browsertab.SelectionState.normal: self._set_mode_text("caret selection") self._color_flags.caret = ColorFlags.CaretMode.selection + elif selection_state is browsertab.SelectionState.line: + self._set_mode_text("caret line selection") + self._color_flags.caret = ColorFlags.CaretMode.selection else: self._set_mode_text("caret") self._color_flags.caret = ColorFlags.CaretMode.on diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index f5dc3277b..e7756d321 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -189,7 +189,7 @@ class TabbedBrowser(QWidget): cur_scroll_perc_changed = pyqtSignal(int, int) cur_load_status_changed = pyqtSignal(usertypes.LoadStatus) cur_fullscreen_requested = pyqtSignal(bool) - cur_caret_selection_toggled = pyqtSignal(bool) + cur_caret_selection_toggled = pyqtSignal(browsertab.SelectionState) close_window = pyqtSignal() resized = pyqtSignal('QRect') current_tab_changed = pyqtSignal(browsertab.AbstractTab) From 90028d21d6ecf6d7c2271167f17bd264cec68eb7 Mon Sep 17 00:00:00 2001 From: svetelna Date: Sun, 26 Apr 2020 13:55:18 +0200 Subject: [PATCH 010/245] adding webkit part of line selection --- qutebrowser/browser/webkit/webkittab.py | 199 ++++++++++++++++-------- 1 file changed, 137 insertions(+), 62 deletions(-) diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index d1122b78e..0a60e073b 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -177,8 +177,11 @@ class WebKitCaret(browsertab.AbstractCaret): if mode != usertypes.KeyMode.caret: return - self.selection_enabled = self._widget.hasSelection() - self.selection_toggled.emit(self.selection_enabled) + if self._widget.hasSelection(): + self.selection_state = browsertab.SelectionState.normal + else: + self.selection_state = browsertab.SelectionState.none + self.selection_toggled.emit(self.selection_state) settings = self._widget.settings() settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True) @@ -193,7 +196,7 @@ class WebKitCaret(browsertab.AbstractCaret): # # Note: We can't use hasSelection() here, as that's always # true in caret mode. - if not self.selection_enabled: + if self.selection_state is browsertab.SelectionState.none: self._widget.page().currentFrame().evaluateJavaScript( utils.read_file('javascript/position_caret.js')) @@ -201,151 +204,190 @@ class WebKitCaret(browsertab.AbstractCaret): def _on_mode_left(self, _mode): settings = self._widget.settings() if settings.testAttribute(QWebSettings.CaretBrowsingEnabled): - if self.selection_enabled and self._widget.hasSelection(): + if (self.selection_state is not + browsertab.SelectionState.none and + self._widget.hasSelection()): # Remove selection if it exists self._widget.triggerPageAction(QWebPage.MoveToNextChar) settings.setAttribute(QWebSettings.CaretBrowsingEnabled, False) - self.selection_enabled = False + self.selection_state = browsertab.SelectionState.none def move_to_next_line(self, count=1): - if not self.selection_enabled: - act = QWebPage.MoveToNextLine - else: + if self.selection_state is not browsertab.SelectionState.none: act = QWebPage.SelectNextLine + else: + act = QWebPage.MoveToNextLine for _ in range(count): self._widget.triggerPageAction(act) + if self.selection_state is browsertab.SelectionState.line: + self._select_line_to_end() def move_to_prev_line(self, count=1): - if not self.selection_enabled: - act = QWebPage.MoveToPreviousLine - else: + if self.selection_state is not browsertab.SelectionState.none: act = QWebPage.SelectPreviousLine + else: + act = QWebPage.MoveToPreviousLine for _ in range(count): self._widget.triggerPageAction(act) + if self.selection_state is browsertab.SelectionState.line: + self._select_line_to_start() def move_to_next_char(self, count=1): - if not self.selection_enabled: - act = QWebPage.MoveToNextChar - else: + if self.selection_state is browsertab.SelectionState.normal: act = QWebPage.SelectNextChar + elif self.selection_state is browsertab.SelectionState.line: + return + else: + act = QWebPage.MoveToNextChar for _ in range(count): self._widget.triggerPageAction(act) def move_to_prev_char(self, count=1): - if not self.selection_enabled: - act = QWebPage.MoveToPreviousChar - else: + if self.selection_state is browsertab.SelectionState.normal: act = QWebPage.SelectPreviousChar + elif self.selection_state is browsertab.SelectionState.line: + return + else: + act = QWebPage.MoveToPreviousChar for _ in range(count): self._widget.triggerPageAction(act) def move_to_end_of_word(self, count=1): - if not self.selection_enabled: - act = [QWebPage.MoveToNextWord] - if utils.is_windows: # pragma: no cover - act.append(QWebPage.MoveToPreviousChar) - else: + if self.selection_state is browsertab.SelectionState.normal: act = [QWebPage.SelectNextWord] if utils.is_windows: # pragma: no cover act.append(QWebPage.SelectPreviousChar) + elif self.selection_state is browsertab.SelectionState.line: + return + else: + act = [QWebPage.MoveToNextWord] + if utils.is_windows: # pragma: no cover + act.append(QWebPage.MoveToPreviousChar) for _ in range(count): for a in act: self._widget.triggerPageAction(a) def move_to_next_word(self, count=1): - if not self.selection_enabled: - act = [QWebPage.MoveToNextWord] - if not utils.is_windows: # pragma: no branch - act.append(QWebPage.MoveToNextChar) - else: + if self.selection_state is browsertab.SelectionState.normal: act = [QWebPage.SelectNextWord] if not utils.is_windows: # pragma: no branch act.append(QWebPage.SelectNextChar) + elif self.selection_state is browsertab.SelectionState.line: + return + else: + act = [QWebPage.MoveToNextWord] + if not utils.is_windows: # pragma: no branch + act.append(QWebPage.MoveToNextChar) for _ in range(count): for a in act: self._widget.triggerPageAction(a) def move_to_prev_word(self, count=1): - if not self.selection_enabled: - act = QWebPage.MoveToPreviousWord - else: + if self.selection_state is browsertab.SelectionState.normal: act = QWebPage.SelectPreviousWord + elif self.selection_state is browsertab.SelectionState.line: + return + else: + act = QWebPage.MoveToPreviousWord for _ in range(count): self._widget.triggerPageAction(act) def move_to_start_of_line(self): - if not self.selection_enabled: - act = QWebPage.MoveToStartOfLine - else: + if self.selection_state is browsertab.SelectionState.normal: act = QWebPage.SelectStartOfLine + elif self.selection_state is browsertab.SelectionState.line: + return + else: + act = QWebPage.MoveToStartOfLine self._widget.triggerPageAction(act) def move_to_end_of_line(self): - if not self.selection_enabled: - act = QWebPage.MoveToEndOfLine - else: + if self.selection_state is browsertab.SelectionState.normal: act = QWebPage.SelectEndOfLine + elif self.selection_state is browsertab.SelectionState.line: + return + else: + act = QWebPage.MoveToEndOfLine self._widget.triggerPageAction(act) def move_to_start_of_next_block(self, count=1): - if not self.selection_enabled: - act = [QWebPage.MoveToNextLine, - QWebPage.MoveToStartOfBlock] - else: + if self.selection_state is not browsertab.SelectionState.none: act = [QWebPage.SelectNextLine, QWebPage.SelectStartOfBlock] + else: + act = [QWebPage.MoveToNextLine, + QWebPage.MoveToStartOfBlock] for _ in range(count): for a in act: self._widget.triggerPageAction(a) + if self.selection_state is browsertab.SelectionState.line: + self._select_line_to_end() def move_to_start_of_prev_block(self, count=1): - if not self.selection_enabled: - act = [QWebPage.MoveToPreviousLine, - QWebPage.MoveToStartOfBlock] - else: + if self.selection_state is not browsertab.SelectionState.none: act = [QWebPage.SelectPreviousLine, QWebPage.SelectStartOfBlock] + else: + act = [QWebPage.MoveToPreviousLine, + QWebPage.MoveToStartOfBlock] for _ in range(count): for a in act: self._widget.triggerPageAction(a) + if self.selection_state is browsertab.SelectionState.line: + self._select_line_to_start() def move_to_end_of_next_block(self, count=1): - if not self.selection_enabled: - act = [QWebPage.MoveToNextLine, - QWebPage.MoveToEndOfBlock] - else: + if self.selection_state is not browsertab.SelectionState.none: act = [QWebPage.SelectNextLine, QWebPage.SelectEndOfBlock] + else: + act = [QWebPage.MoveToNextLine, + QWebPage.MoveToEndOfBlock] for _ in range(count): for a in act: self._widget.triggerPageAction(a) + if self.selection_state is browsertab.SelectionState.line: + self._select_line_to_end() def move_to_end_of_prev_block(self, count=1): - if not self.selection_enabled: - act = [QWebPage.MoveToPreviousLine, QWebPage.MoveToEndOfBlock] - else: + if self.selection_state is not browsertab.SelectionState.none: act = [QWebPage.SelectPreviousLine, QWebPage.SelectEndOfBlock] + else: + act = [QWebPage.MoveToPreviousLine, QWebPage.MoveToEndOfBlock] for _ in range(count): for a in act: self._widget.triggerPageAction(a) + if self.selection_state is browsertab.SelectionState.line: + self._select_line_to_start() def move_to_start_of_document(self): - if not self.selection_enabled: - act = QWebPage.MoveToStartOfDocument - else: + if self.selection_state is not browsertab.SelectionState.none: act = QWebPage.SelectStartOfDocument + else: + act = QWebPage.MoveToStartOfDocument self._widget.triggerPageAction(act) + if self.selection_state is browsertab.SelectionState.line: + self._select_line() def move_to_end_of_document(self): - if not self.selection_enabled: - act = QWebPage.MoveToEndOfDocument - else: + if self.selection_state is not browsertab.SelectionState.none: act = QWebPage.SelectEndOfDocument + else: + act = QWebPage.MoveToEndOfDocument self._widget.triggerPageAction(act) - def toggle_selection(self): - self.selection_enabled = not self.selection_enabled - self.selection_toggled.emit(self.selection_enabled) + def toggle_selection(self, line=False): + if line: + self.selection_state = browsertab.SelectionState.line + self._select_line() + self.reverse_selection() + self._select_line() + self.reverse_selection() + elif self.selection_state is not browsertab.SelectionState.normal: + self.selection_state = browsertab.SelectionState.normal + else: + self.selection_state = browsertab.SelectionState.none + self.selection_toggled.emit(self.selection_state) def drop_selection(self): self._widget.triggerPageAction(QWebPage.MoveToNextChar) @@ -362,6 +404,35 @@ class WebKitCaret(browsertab.AbstractCaret): ); }""") + def _select_line(self): + self._widget.triggerPageAction(QWebPage.SelectStartOfLine) + self.reverse_selection() + self._widget.triggerPageAction(QWebPage.SelectEndOfLine) + self.reverse_selection() + + def _select_line_to_end(self): + # direction of selection (if anchor is to the left or right + # of focus) has to be checked before moving selection + # to the end of line + direction = self._js_selection_direction() + if direction: + self._widget.triggerPageAction(QWebPage.SelectEndOfLine) + + def _select_line_to_start(self): + direction = self._js_selection_direction() + if not direction: + self._widget.triggerPageAction(QWebPage.SelectStartOfLine) + + def _js_selection_direction(self): + # return true if selection's direction + # is left to right else false + return self._tab.private_api.run_js_sync(""" + var sel = window.getSelection(); + var position = sel.anchorNode.compareDocumentPosition(sel.focusNode); + (!position && sel.anchorOffset < sel.focusOffset || + position === Node.DOCUMENT_POSITION_FOLLOWING); + """) + def _follow_selected(self, *, tab=False): if QWebSettings.globalSettings().testAttribute( QWebSettings.JavascriptEnabled): @@ -693,6 +764,11 @@ class WebKitTabPrivate(browsertab.AbstractTabPrivate): def shutdown(self): self._widget.shutdown() + def run_js_sync(self, code): + document_element = self._widget.page().mainFrame().documentElement() + result = document_element.evaluateJavaScript(code) + return result + class WebKitTab(browsertab.AbstractTab): @@ -751,8 +827,7 @@ class WebKitTab(browsertab.AbstractTab): def run_js_async(self, code, callback=None, *, world=None): if world is not None and world != usertypes.JsWorld.jseval: log.webview.warning("Ignoring world ID {}".format(world)) - document_element = self._widget.page().mainFrame().documentElement() - result = document_element.evaluateJavaScript(code) + result = self.private_api.run_js_sync(code) if callback is not None: callback(result) From 7db846e6443ef3f8955a554dc92e94cd5f88657c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 27 Apr 2020 17:45:31 +0200 Subject: [PATCH 011/245] Harden caret tests and always show window With newer Qt versions, it looks like the caret window always needs to be shown for the selection to work correctly. However, the test silently passed when no selection was available. --- tests/unit/browser/test_caret.py | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/tests/unit/browser/test_caret.py b/tests/unit/browser/test_caret.py index 9b817c4ac..fb940f7c0 100644 --- a/tests/unit/browser/test_caret.py +++ b/tests/unit/browser/test_caret.py @@ -29,6 +29,8 @@ from qutebrowser.utils import utils, qtutils, usertypes @pytest.fixture def caret(web_tab, qtbot, mode_manager): + web_tab.container.expose() + with qtbot.wait_signal(web_tab.load_finished): web_tab.load_url(QUrl('qute://testdata/data/caret.html')) @@ -61,9 +63,13 @@ class Selection: selection = selection.strip() assert selection == expected return + elif not selection and not expected: + return self._qtbot.wait(50) + assert False, 'Failed to get selection!' + def check_multiline(self, expected, *, strip=False): self.check(textwrap.dedent(expected).strip(), strip=strip) @@ -287,17 +293,6 @@ def test_drop_selection(caret, selection): class TestSearch: - @pytest.fixture(autouse=True) - def expose(self, web_tab): - """Expose the web view if needed. - - With QtWebEngine 5.13 on macOS/Windows, searching fails (callback - called with False) when the view isn't exposed. - """ - if qtutils.version_check('5.13') and not utils.is_linux: - web_tab.container.expose() - web_tab.show() - # https://bugreports.qt.io/browse/QTBUG-60673 @pytest.mark.qtbug60673 @@ -340,15 +335,6 @@ class TestFollowSelected: def toggle_js(self, request, config_stub): config_stub.val.content.javascript.enabled = request.param - @pytest.fixture(autouse=True) - def expose(self, web_tab): - """Expose the web view if needed. - - On QtWebKit, or Qt < 5.11 and > 5.12 on QtWebEngine, we need to - show the tab for selections to work properly. - """ - web_tab.container.expose() - def test_follow_selected_without_a_selection(self, qtbot, caret, selection, web_tab, mode_manager): caret.move_to_next_word() # Move cursor away from the link From e0a9088f6395fe791568dff8c157fb06d208165f Mon Sep 17 00:00:00 2001 From: svetelna Date: Tue, 28 Apr 2020 17:45:34 +0200 Subject: [PATCH 012/245] added tests --- tests/unit/browser/test_caret.py | 93 ++++++++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 3 deletions(-) diff --git a/tests/unit/browser/test_caret.py b/tests/unit/browser/test_caret.py index fb940f7c0..e5ab90708 100644 --- a/tests/unit/browser/test_caret.py +++ b/tests/unit/browser/test_caret.py @@ -24,7 +24,7 @@ import textwrap import pytest from PyQt5.QtCore import QUrl -from qutebrowser.utils import utils, qtutils, usertypes +from qutebrowser.utils import usertypes @pytest.fixture @@ -73,9 +73,9 @@ class Selection: def check_multiline(self, expected, *, strip=False): self.check(textwrap.dedent(expected).strip(), strip=strip) - def toggle(self): + def toggle(self, line=False): with self._qtbot.wait_signal(self._caret.selection_toggled): - self._caret.toggle_selection() + self._caret.toggle_selection(line=line) @pytest.fixture @@ -391,3 +391,90 @@ class TestReverse: caret.reverse_selection() caret.move_to_start_of_line() selection.check("one two three") + + +class TestLineSelection: + + def test_toggle(self, caret, selection): + selection.toggle(True) + selection.check("one two three") + + def test_toggle_untoggle(self, caret, selection): + selection.toggle() + selection.check("") + selection.toggle(True) + selection.check("one two three") + selection.toggle() + selection.check("one two three") + + def test_from_center(self, caret, selection): + caret.move_to_next_char(4) + selection.toggle(True) + selection.check("one two three") + + def test_more_lines(self, caret, selection): + selection.toggle(True) + caret.move_to_next_line(2) + selection.check_multiline(""" + one two three + eins zwei drei + + four five six + """, strip=True) + + def test_not_selecting_char(self, caret, selection): + selection.toggle(True) + caret.move_to_next_char() + selection.check("one two three") + caret.move_to_prev_char() + selection.check("one two three") + + def test_selecting_prev_next_word(self, caret, selection): + selection.toggle(True) + caret.move_to_next_word() + selection.check("one two three") + caret.move_to_prev_word() + selection.check("one two three") + + def test_selecting_end_word(self, caret, selection): + selection.toggle(True) + caret.move_to_end_of_word() + selection.check("one two three") + + def test_selecting_prev_next_line(self, caret, selection): + selection.toggle(True) + caret.move_to_next_line() + selection.check_multiline(""" + one two three + eins zwei drei + """, strip=True) + caret.move_to_prev_line() + selection.check("one two three") + + def test_not_selecting_start_end_line(self, caret, selection): + selection.toggle(True) + caret.move_to_end_of_line() + selection.check("one two three") + caret.move_to_start_of_line() + selection.check("one two three") + + def test_selecting_block(self, caret, selection): + selection.toggle(True) + caret.move_to_end_of_next_block() + selection.check_multiline(""" + one two three + eins zwei drei + """, strip=True) + + def test_selecting_start_end_document(self, caret, selection): + selection.toggle(True) + caret.move_to_end_of_document() + selection.check_multiline(""" + one two three + eins zwei drei + + four five six + vier fünf sechs + """, strip=True) + caret.move_to_start_of_document() + selection.check("one two three") From 31af9cc54b835064ddd1eb12ab38293e1eb902a1 Mon Sep 17 00:00:00 2001 From: svetelna Date: Mon, 11 May 2020 10:48:43 +0200 Subject: [PATCH 013/245] removed selectionEnabled function in caret.js --- qutebrowser/javascript/caret.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/qutebrowser/javascript/caret.js b/qutebrowser/javascript/caret.js index 6d7a3bb00..51a1cf7cf 100644 --- a/qutebrowser/javascript/caret.js +++ b/qutebrowser/javascript/caret.js @@ -1184,13 +1184,6 @@ window._qutebrowser.caret = (function() { } }; - CaretBrowsing.selectionEnabled = function() { - if (CaretBrowsing.selectionState === CaretBrowsing.SelectionState.NONE) { - return false; - } - return true; - }; - CaretBrowsing.move = function(direction, granularity, count = 1) { let action = "move"; if (CaretBrowsing.selectionState !== CaretBrowsing.SelectionState.NONE) { @@ -1348,14 +1341,14 @@ window._qutebrowser.caret = (function() { funcs.setInitialCursor = () => { if (!CaretBrowsing.initiated) { CaretBrowsing.setInitialCursor(); - return CaretBrowsing.selectionEnabled(); + return CaretBrowsing.selectionState !== CaretBrowsing.SelectionState.NONE; } if (window.getSelection().toString().length === 0) { positionCaret(); } CaretBrowsing.toggle(); - return CaretBrowsing.selectionEnabled(); + return CaretBrowsing.selectionState !== CaretBrowsing.SelectionState.NONE; }; funcs.setFlags = (flags) => { From 5a0497a087f9fda0883930d1beda537a540ba73b Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sun, 17 May 2020 07:01:06 -0400 Subject: [PATCH 014/245] Add loglevel config settings. Log levels can now be configured in config.py by setting loggin.level.console (for stdout/stderr) and logging.level.ram (for :messages). This is of interest for users who would like password-manager integration that uses `fake-key` to send sensitive strings to websites. The default 'debug' ram logging would store various logs containing the sensitive data. Previously the loglevel was only configurable through CLI flags, and those only affected the console loglevel. We felt a config value would be nicer than just adding another flag for RAM loglevel, requiring you to create an alias for qutebrowser and ensure _that_ alias gets used anywhere qutebrowser might be invoked. Logging is initialized before the config is loaded, so configuration-set loglevels have to be loaded in a second, later stage. However, this stage will only set the console loglevel if it wasn't set on the CLI, as a CLI flag feels like a more explicit action that should override a config. Fixes #5286. --- qutebrowser/app.py | 2 ++ qutebrowser/config/configdata.yml | 23 +++++++++++++++++++++++ qutebrowser/qutebrowser.py | 2 +- qutebrowser/utils/log.py | 26 +++++++++++++++++++++++++- tests/unit/utils/test_log.py | 24 ++++++++++++++++++++++++ 5 files changed, 75 insertions(+), 2 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 6d01e0ddd..b89fa4b37 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -379,6 +379,8 @@ def _init_modules(*, args): Args: args: The argparse namespace. """ + log.init.debug("Initializing logging from config...") + log.init_from_config(config.val) log.init.debug("Initializing save manager...") save_manager = savemanager.SaveManager(q_app) objreg.register('save-manager', save_manager) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 393ae8ac0..98ec76fd7 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -3194,3 +3194,26 @@ bindings.commands: * register: Entered when qutebrowser is waiting for a register name/key for commands like `:set-mark`. + +## logging + +logging.level.ram: + default: null + type: + name: String + none_ok: true + # levels match those in qutebrowser/utils/log.py + valid_values: ['VDEBUG', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] + desc: + Level for in-memory logs. + +logging.level.console: + default: null + type: + name: String + none_ok: true + # levels match those in qutebrowser/utils/log.py + valid_values: ['VDEBUG', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] + desc: >- + Level for console (stdout/stderr) logs. + Ignored if the --loglevel or --debug CLI flags are used. diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index 0ffdb5567..3369c1ebe 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -96,7 +96,7 @@ def get_argparser(): debug = parser.add_argument_group('debug arguments') debug.add_argument('-l', '--loglevel', dest='loglevel', - help="Set loglevel", default='info', + help="Override the configured console loglevel", choices=['critical', 'error', 'warning', 'info', 'debug', 'vdebug']) debug.add_argument('--logfilter', type=logfilter_error, diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 2718f10ba..6cf204b5d 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -176,7 +176,7 @@ def stub(suffix: str = '') -> None: def init_log(args: argparse.Namespace) -> None: """Init loggers based on the argparse namespace passed.""" - level = args.loglevel.upper() + level = (args.loglevel or "info").upper() try: numeric_level = getattr(logging, level) except AttributeError: @@ -526,6 +526,30 @@ def hide_qt_warning(pattern: str, logger: str = 'qt') -> typing.Iterator[None]: logger_obj.removeFilter(log_filter) +def init_from_config(conf: typing.Any) -> None: + """Initialize logging settings from the config. + + init_log is called before the config module is initialized, so config-based + initialization cannot be performed there. + + Args: + conf: The global ConfigContainer. + This is passed rather than accessed via the module to avoid a + cyclic import. + """ + ramlevel = conf.logging.level.ram + consolelevel = conf.logging.level.console + if ramlevel and ram_handler: + init.info("Configuring RAM loglevel to %s", ramlevel) + ram_handler.setLevel(LOG_LEVELS[ramlevel]) + if consolelevel and console_handler: + if _args and _args.loglevel: + init.info("--loglevel flag overrides logging.level.console") + else: + init.info("Configuring console loglevel to %s", consolelevel) + console_handler.setLevel(LOG_LEVELS[consolelevel]) + + class QtWarningFilter(logging.Filter): """Filter to filter Qt warnings. diff --git a/tests/unit/utils/test_log.py b/tests/unit/utils/test_log.py index a74d81600..45543a136 100644 --- a/tests/unit/utils/test_log.py +++ b/tests/unit/utils/test_log.py @@ -255,6 +255,30 @@ class TestInitLog: warnings.warn("test warning", PendingDeprecationWarning) +@pytest.mark.parametrize( + 'console_cli,console_conf,console_expected,ram_conf,ram_expected', + [ + (None, None, logging.INFO, None, logging.NOTSET), + (None, None, logging.INFO, 'CRITICAL', logging.CRITICAL), + (None, 'WARNING', logging.WARNING, 'INFO', logging.INFO), + ('INFO', 'WARNING', logging.INFO, 'VDEBUG', logging.VDEBUG), + ('WARNING', 'INFO', logging.WARNING, 'CRITICAL', logging.CRITICAL), + ]) +def test_init_from_config(mocker, console_cli, console_conf, console_expected, + ram_conf, ram_expected): + args = argparse.Namespace(debug=False, loglevel=console_cli, color=True, + loglines=10, logfilter="", force_color=False, + json_logging=False, debug_flags=set()) + log.init_log(args) + + conf = mocker.Mock() + conf.logging.level.ram = ram_conf + conf.logging.level.console = console_conf + log.init_from_config(conf) + assert log.ram_handler.level == ram_expected + assert log.console_handler.level == console_expected + + class TestHideQtWarning: """Tests for hide_qt_warning/QtWarningFilter.""" From 9f1f9e5a40769c8bc9928c287574d5f92858c5d7 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Tue, 19 May 2020 22:45:07 -0400 Subject: [PATCH 015/245] PR feedback for loglevel config. - Use conditional import to enable typechecking without cyclic import - Use asserts to prove optionals are non-null - Use args and config_stub fixtures --- qutebrowser/utils/log.py | 14 ++++++++---- tests/unit/utils/test_log.py | 42 +++++++++++++++++------------------- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 6cf204b5d..20a3dd4fb 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -41,6 +41,9 @@ try: except ImportError: colorama = None +if typing.TYPE_CHECKING: + from qutebrowser.config import config as configmodule + _log_inited = False _args = None @@ -526,7 +529,7 @@ def hide_qt_warning(pattern: str, logger: str = 'qt') -> typing.Iterator[None]: logger_obj.removeFilter(log_filter) -def init_from_config(conf: typing.Any) -> None: +def init_from_config(conf: 'configmodule.ConfigContainer') -> None: """Initialize logging settings from the config. init_log is called before the config module is initialized, so config-based @@ -537,13 +540,16 @@ def init_from_config(conf: typing.Any) -> None: This is passed rather than accessed via the module to avoid a cyclic import. """ + assert ram_handler is not None + assert console_handler is not None + assert _args is not None ramlevel = conf.logging.level.ram consolelevel = conf.logging.level.console - if ramlevel and ram_handler: + if ramlevel: init.info("Configuring RAM loglevel to %s", ramlevel) ram_handler.setLevel(LOG_LEVELS[ramlevel]) - if consolelevel and console_handler: - if _args and _args.loglevel: + if consolelevel: + if _args.loglevel: init.info("--loglevel flag overrides logging.level.console") else: init.info("Configuring console loglevel to %s", consolelevel) diff --git a/tests/unit/utils/test_log.py b/tests/unit/utils/test_log.py index 45543a136..c8b778149 100644 --- a/tests/unit/utils/test_log.py +++ b/tests/unit/utils/test_log.py @@ -254,29 +254,27 @@ class TestInitLog: with pytest.raises(PendingDeprecationWarning): warnings.warn("test warning", PendingDeprecationWarning) + @pytest.mark.parametrize( + 'console_cli, console_conf, console_expected, ram_conf, ram_expected', + [ + (None, None, logging.INFO, None, logging.NOTSET), + (None, None, logging.INFO, 'CRITICAL', logging.CRITICAL), + (None, 'WARNING', logging.WARNING, 'INFO', logging.INFO), + ('INFO', 'WARNING', logging.INFO, 'VDEBUG', logging.VDEBUG), + ('WARNING', 'INFO', logging.WARNING, 'CRITICAL', logging.CRITICAL), + ]) + def test_init_from_config(self, mocker, console_cli, console_conf, + console_expected, ram_conf, ram_expected, args, + config_stub): + args.debug = False + args.loglevel = console_cli + log.init_log(args) -@pytest.mark.parametrize( - 'console_cli,console_conf,console_expected,ram_conf,ram_expected', - [ - (None, None, logging.INFO, None, logging.NOTSET), - (None, None, logging.INFO, 'CRITICAL', logging.CRITICAL), - (None, 'WARNING', logging.WARNING, 'INFO', logging.INFO), - ('INFO', 'WARNING', logging.INFO, 'VDEBUG', logging.VDEBUG), - ('WARNING', 'INFO', logging.WARNING, 'CRITICAL', logging.CRITICAL), - ]) -def test_init_from_config(mocker, console_cli, console_conf, console_expected, - ram_conf, ram_expected): - args = argparse.Namespace(debug=False, loglevel=console_cli, color=True, - loglines=10, logfilter="", force_color=False, - json_logging=False, debug_flags=set()) - log.init_log(args) - - conf = mocker.Mock() - conf.logging.level.ram = ram_conf - conf.logging.level.console = console_conf - log.init_from_config(conf) - assert log.ram_handler.level == ram_expected - assert log.console_handler.level == console_expected + config_stub.val.logging.level.ram = ram_conf + config_stub.val.logging.level.console = console_conf + log.init_from_config(config_stub.val) + assert log.ram_handler.level == ram_expected + assert log.console_handler.level == console_expected class TestHideQtWarning: From ea9a5395487c0ac6120ec6012de1f6e9eef5d3df Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Tue, 19 May 2020 23:03:03 -0400 Subject: [PATCH 016/245] Add custom configtype for loglevel. Avoids the need to duplicate the log level strings. --- qutebrowser/config/configdata.yml | 8 ++------ qutebrowser/config/configtypes.py | 11 ++++++++++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 98ec76fd7..d7988f571 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -3200,20 +3200,16 @@ bindings.commands: logging.level.ram: default: null type: - name: String + name: LogLevel none_ok: true - # levels match those in qutebrowser/utils/log.py - valid_values: ['VDEBUG', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] desc: Level for in-memory logs. logging.level.console: default: null type: - name: String + name: LogLevel none_ok: true - # levels match those in qutebrowser/utils/log.py - valid_values: ['VDEBUG', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] desc: >- Level for console (stdout/stderr) logs. Ignored if the --loglevel or --debug CLI flags are used. diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 6eec13293..7927e0425 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -63,7 +63,7 @@ from PyQt5.QtNetwork import QNetworkProxy from qutebrowser.misc import objects, debugcachestats from qutebrowser.config import configexc, configutils from qutebrowser.utils import (standarddir, utils, qtutils, urlutils, urlmatch, - usertypes) + usertypes, log) from qutebrowser.keyinput import keyutils from qutebrowser.browser.network import pac @@ -2014,6 +2014,15 @@ class NewTabPosition(String): ('last', "At the end.")) +class LogLevel(String): + + """Log level.""" + + def __init__(self, none_ok: bool = False) -> None: + super().__init__(none_ok=none_ok) + self.valid_values = ValidValues(*log.LOG_LEVELS.keys()) + + class Key(BaseType): """A name of a key.""" From da5d153dcd453263f59a80259ebda69b623a480c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 22 May 2020 16:02:26 +0200 Subject: [PATCH 017/245] Add workaround for missing QWebEngineFindTextResult See #5412 and https://www.riverbankcomputing.com/pipermail/pyqt/2020-May/042892.html --- doc/changelog.asciidoc | 2 ++ qutebrowser/browser/webengine/webenginetab.py | 19 ++++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 001089247..776b35e0e 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -59,6 +59,8 @@ Fixed the main thread), qutebrowser did crash or freeze when trying to show its exception handler. This is now fixed. - `:inspector` now works correctly when cookies are disabled globally. +- Added workaround for a (Gentoo?) PyQt/packaging issue related to the + `QWebEngineFindTextResult` handling added in v1.11.0. v1.11.1 (2020-05-07) -------------------- diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 2f16b6ae3..b83a8b2ba 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -183,9 +183,22 @@ class _WebEngineSearchWrapHandler: Args: page: The QtWebEnginePage to connect to this handler. """ - if qtutils.version_check("5.14"): - page.findTextFinished.connect(self._store_match_data) - self._nowrap_available = True + if not qtutils.version_check("5.14"): + return + + try: + from PyQt5.QtWebEngineCore import QWebEngineFindTextResult + except ImportError: + # WORKAROUND for some odd PyQt/packaging bug where the + # findTextResult signal is available, but QWebEngineFindTextResult + # is not. Seems to happen on e.g. Gentoo. + log.webview.warn("Could not import QWebEngineFindTextResult " + "despite running on Qt 5.14. You might need to " + "rebuild PyQtWebEngine.") + return + + page.findTextFinished.connect(self._store_match_data) + self._nowrap_available = True def _store_match_data(self, result): """Store information on the last match. From abe16d5f06407805f7cfda21270cff5b306f9be5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 22 May 2020 16:49:23 +0200 Subject: [PATCH 018/245] Fix lint --- qutebrowser/browser/webengine/webenginetab.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index b83a8b2ba..84d35211a 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -187,14 +187,15 @@ class _WebEngineSearchWrapHandler: return try: + # pylint: disable=unused-import from PyQt5.QtWebEngineCore import QWebEngineFindTextResult except ImportError: # WORKAROUND for some odd PyQt/packaging bug where the # findTextResult signal is available, but QWebEngineFindTextResult # is not. Seems to happen on e.g. Gentoo. - log.webview.warn("Could not import QWebEngineFindTextResult " - "despite running on Qt 5.14. You might need to " - "rebuild PyQtWebEngine.") + log.webview.warning("Could not import QWebEngineFindTextResult " + "despite running on Qt 5.14. You might need " + "to rebuild PyQtWebEngine.") return page.findTextFinished.connect(self._store_match_data) From bae1431703cf53032f14c8c92a701309fe8a6a19 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 22 May 2020 17:12:51 +0200 Subject: [PATCH 019/245] src2asciidoc: Make sure usage lines are generated with fixed width Due to a change in Python 3.8, the output depended on the calling terminal's width. Set a fixed with of 200 (rather than 80) so that we always have the expanded version for the generated documentation. See #5393 and https://github.com/python/cpython/commit/74102c9a5f2327c4fc47feefa072854a53551d1f#diff-837b312b1f3508216ace6adb46492836 --- doc/help/commands.asciidoc | 17 +++++------------ scripts/dev/src2asciidoc.py | 5 +++++ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 7c8da37a8..a6a5c3e87 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -286,8 +286,7 @@ Set all settings back to their default. [[config-cycle]] === config-cycle -Syntax: +:config-cycle [*--pattern* 'pattern'] [*--temp*] [*--print*] - 'option' ['values' ['values' ...]]+ +Syntax: +:config-cycle [*--pattern* 'pattern'] [*--temp*] [*--print*] 'option' ['values' ['values' ...]]+ Cycle an option between multiple values. @@ -608,8 +607,7 @@ Show help about a command or setting. [[hint]] === hint -Syntax: +:hint [*--mode* 'mode'] [*--add-history*] [*--rapid*] [*--first*] - ['group'] ['target'] ['args' ['args' ...]]+ +Syntax: +:hint [*--mode* 'mode'] [*--add-history*] [*--rapid*] [*--first*] ['group'] ['target'] ['args' ['args' ...]]+ Start hinting. @@ -866,8 +864,7 @@ Do nothing. [[open]] === open -Syntax: +:open [*--related*] [*--bg*] [*--tab*] [*--window*] [*--secure*] [*--private*] - ['url']+ +Syntax: +:open [*--related*] [*--bg*] [*--tab*] [*--window*] [*--secure*] [*--private*] ['url']+ Open a URL in the current/[count]th tab. @@ -1197,9 +1194,7 @@ Load a session. [[session-save]] === session-save -Syntax: +:session-save [*--current*] [*--quiet*] [*--force*] [*--only-active-window*] - [*--with-private*] - ['name']+ +Syntax: +:session-save [*--current*] [*--quiet*] [*--force*] [*--only-active-window*] [*--with-private*] ['name']+ Save a session. @@ -1263,9 +1258,7 @@ Set a mark at the current scroll position in the current tab. [[spawn]] === spawn -Syntax: +:spawn [*--userscript*] [*--verbose*] [*--output*] [*--output-messages*] - [*--detach*] - 'cmdline'+ +Syntax: +:spawn [*--userscript*] [*--verbose*] [*--output*] [*--output-messages*] [*--detach*] 'cmdline'+ Spawn an external command. diff --git a/scripts/dev/src2asciidoc.py b/scripts/dev/src2asciidoc.py index 51f4a3633..70df0ebe0 100755 --- a/scripts/dev/src2asciidoc.py +++ b/scripts/dev/src2asciidoc.py @@ -59,6 +59,11 @@ class UsageFormatter(argparse.HelpFormatter): argparse.HelpFormatter while copying 99% of the code :-/ """ + def __init__(self, prog, indent_increment=2, max_help_position=24, + width=200): + """Override __init__ to set a fixed width as default.""" + super().__init__(prog, indent_increment, max_help_position, width) + def _format_usage(self, usage, actions, groups, _prefix): """Override _format_usage to not add the 'usage:' prefix.""" return super()._format_usage(usage, actions, groups, '') From 12a6590364f69d627cddcbe48cc675593827e4c4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 22 May 2020 17:16:34 +0200 Subject: [PATCH 020/245] Add Ctrl+I to bindings.key_mappings See #5455 --- doc/changelog.asciidoc | 1 + doc/help/settings.asciidoc | 1 + qutebrowser/config/configdata.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 776b35e0e..7a97507e5 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -49,6 +49,7 @@ Changed the exact dependencies listed in `misc/requirements/requirements-tests.txt{,-raw}` is supported. - The `:tab-focus` command now has completion for tabs in the current window. +- The `bindings.key_mappings` setting now maps `` to the tab key by default. Fixed ~~~~~ diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index cf6ee2069..8548d834a 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -739,6 +739,7 @@ Default: - +pass:[<Ctrl-6>]+: +pass:[<Ctrl-^>]+ - +pass:[<Ctrl-Enter>]+: +pass:[<Ctrl-Return>]+ +- +pass:[<Ctrl-I>]+: +pass:[<Tab>]+ - +pass:[<Ctrl-J>]+: +pass:[<Return>]+ - +pass:[<Ctrl-M>]+: +pass:[<Return>]+ - +pass:[<Ctrl-[>]+: +pass:[<Escape>]+ diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index b231fccb4..660bd661c 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -2799,6 +2799,7 @@ bindings.key_mappings: : : : + : : : : From 9b8767b64eea1498e383ae9ef790cb377bfb7508 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 22 May 2020 17:46:45 +0200 Subject: [PATCH 021/245] tests: Make sure WidgetContainer.expose() sets focus on widget This is needed to convert the searched string into a selection properly. Fixes #5363 See #5393 --- tests/helpers/fixtures.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py index 51fd98272..eb4186894 100644 --- a/tests/helpers/fixtures.py +++ b/tests/helpers/fixtures.py @@ -64,14 +64,17 @@ class WidgetContainer(QWidget): self._qtbot = qtbot self.vbox = QVBoxLayout(self) qtbot.add_widget(self) + self._widget = None def set_widget(self, widget): self.vbox.addWidget(widget) widget.container = self + self._widget = widget def expose(self): with self._qtbot.waitExposed(self): self.show() + self._widget.setFocus() @pytest.fixture From d2f3bc4bad6b519b972472b9fb32dcb0b4f2f85a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 22 May 2020 18:06:54 +0200 Subject: [PATCH 022/245] tests: Increase timeout for caret.html loading For some odd reason, loading the file sometimes takes >5s on macOS... See https://github.com/qutebrowser/qutebrowser/issues/5390#issuecomment-629269038 --- tests/unit/browser/test_caret.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/browser/test_caret.py b/tests/unit/browser/test_caret.py index e5ab90708..13994a654 100644 --- a/tests/unit/browser/test_caret.py +++ b/tests/unit/browser/test_caret.py @@ -31,7 +31,7 @@ from qutebrowser.utils import usertypes def caret(web_tab, qtbot, mode_manager): web_tab.container.expose() - with qtbot.wait_signal(web_tab.load_finished): + with qtbot.wait_signal(web_tab.load_finished, timeout=10000): web_tab.load_url(QUrl('qute://testdata/data/caret.html')) mode_manager.enter(usertypes.KeyMode.caret) From 2cb1ba1e7c4b5d6b074a5161a46ec2252cf364a4 Mon Sep 17 00:00:00 2001 From: Pedro Lucas Porcellis Date: Thu, 21 May 2020 22:50:53 -0300 Subject: [PATCH 023/245] Allow to pluck tab into a private window instance If the tab is actually located into a private window, it will just behave normally. --- qutebrowser/browser/commands.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 0f42a09d8..9f328c768 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -453,7 +453,7 @@ class CommandDispatcher: @cmdutils.argument('win_id', completion=miscmodels.window) @cmdutils.argument('count', value=cmdutils.Value.count) def tab_give(self, win_id: int = None, keep: bool = False, - count: int = None) -> None: + count: int = None, private: bool = False) -> None: """Give the current tab to a new or existing window if win_id given. If no win_id is given, the tab will get detached into a new window. @@ -462,6 +462,7 @@ class CommandDispatcher: win_id: The window ID of the window to give the current tab to. keep: If given, keep the old tab around. count: Overrides win_id (index starts at 1 for win_id=0). + private: If the tab should be detached into a private instance. """ if config.val.tabs.tabs_are_windows: raise cmdutils.CommandError("Can't give tabs when using " @@ -479,7 +480,7 @@ class CommandDispatcher: "only one tab") tabbed_browser = self._new_tabbed_browser( - private=self._tabbed_browser.is_private) + private=private or self._tabbed_browser.is_private) else: if win_id not in objreg.window_registry: raise cmdutils.CommandError( @@ -488,6 +489,9 @@ class CommandDispatcher: tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) + if private and not tabbed_browser.is_private: + raise cmdutils.CommandError("The window with id {} is not private".format(win_id)) + tabbed_browser.tabopen(self._current_url()) if not keep: self._tabbed_browser.close_tab(self._current_widget(), From 781a68a10e10f559575f23c6ed8faa2fb46ab7d9 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 22 May 2020 18:26:58 +0200 Subject: [PATCH 024/245] tests: make sure webengine_tab/webkit_tab take care of shutdown This still doesn't help with this inside Xvfb: XIO: fatal IO error 0 (Success) on X server ":1001" But at least it prevents an unknown segfault inside QtWebEngine. --- tests/helpers/fixtures.py | 23 ++++++++++++++++++++--- tests/helpers/stubs.py | 4 ++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py index eb4186894..0624ef698 100644 --- a/tests/helpers/fixtures.py +++ b/tests/helpers/fixtures.py @@ -45,11 +45,12 @@ import helpers.stubs as stubsmod from qutebrowser.config import (config, configdata, configtypes, configexc, configfiles, configcache, stylesheet) from qutebrowser.api import config as configapi -from qutebrowser.utils import objreg, standarddir, utils, usertypes +from qutebrowser.utils import objreg, standarddir, utils, usertypes, qtutils from qutebrowser.browser import greasemonkey, history, qutescheme from qutebrowser.browser.webkit import cookies, cache from qutebrowser.misc import savemanager, sql, objects, sessions from qutebrowser.keyinput import modeman +from qutebrowser.qt import sip _qute_scheme_handler = None @@ -207,14 +208,17 @@ def web_tab_setup(qtbot, tab_registry, session_manager_stub, @pytest.fixture def webkit_tab(web_tab_setup, qtbot, cookiejar_and_cache, mode_manager, - widget_container, webpage): + widget_container, download_stub, webpage): webkittab = pytest.importorskip('qutebrowser.browser.webkit.webkittab') tab = webkittab.WebKitTab(win_id=0, mode_manager=mode_manager, private=False) widget_container.set_widget(tab) - return tab + yield tab + + # Make sure the tab shuts itself down properly + tab.private_api.shutdown() @pytest.fixture @@ -230,11 +234,24 @@ def webengine_tab(web_tab_setup, qtbot, redirect_webengine_data, tab = webenginetab.WebEngineTab(win_id=0, mode_manager=mode_manager, private=False) widget_container.set_widget(tab) + yield tab + # If a page is still loading here, _on_load_finished could get called # during teardown when session_manager_stub is already deleted. tab.stop() + # Make sure the tab shuts itself down properly + tab.private_api.shutdown() + + # If we wait for the GC to clean things up, there's a segfault inside + # QtWebEngine sometimes (e.g. if we only run + # tests/unit/browser/test_caret.py). + # However, with Qt < 5.12, doing this here will lead to an immediate + # segfault... + if qtutils.version_check('5.12'): + sip.delete(tab._widget) + @pytest.fixture(params=['webkit', 'webengine']) def web_tab(request): diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index caa7aac3f..f775092f5 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -615,6 +615,10 @@ class FakeDownloadManager: self.downloads.append(download_item) return download_item + def has_downloads_with_nam(self, _nam): + """Needed during WebView.shutdown().""" + return False + class FakeHistoryProgress: From 0f839b848b9e7738d14bce3311e3cacf8e02019f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 22 May 2020 19:12:49 +0200 Subject: [PATCH 025/245] caret: Style improvements for line mode --- qutebrowser/browser/browsertab.py | 1 - qutebrowser/browser/webkit/webkittab.py | 24 ++++++++++-------------- qutebrowser/javascript/caret.js | 2 +- tests/unit/browser/test_caret.py | 25 +++++++++++++------------ 4 files changed, 24 insertions(+), 28 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 16ddeef8b..f37d143b6 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -441,7 +441,6 @@ class AbstractCaret(QObject): """Attribute ``caret`` of AbstractTab for caret browsing.""" #: Signal emitted when the selection was toggled. - #: (argument - whether the selection is now active) selection_toggled = pyqtSignal(SelectionState) #: Emitted when a ``follow_selection`` action is done. follow_selected_done = pyqtSignal() diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index 0a60e073b..e73833f67 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -204,8 +204,7 @@ class WebKitCaret(browsertab.AbstractCaret): def _on_mode_left(self, _mode): settings = self._widget.settings() if settings.testAttribute(QWebSettings.CaretBrowsingEnabled): - if (self.selection_state is not - browsertab.SelectionState.none and + if (self.selection_state is not browsertab.SelectionState.none and self._widget.hasSelection()): # Remove selection if it exists self._widget.triggerPageAction(QWebPage.MoveToNextChar) @@ -414,24 +413,21 @@ class WebKitCaret(browsertab.AbstractCaret): # direction of selection (if anchor is to the left or right # of focus) has to be checked before moving selection # to the end of line - direction = self._js_selection_direction() - if direction: + if self._js_selection_left_to_right(): self._widget.triggerPageAction(QWebPage.SelectEndOfLine) def _select_line_to_start(self): - direction = self._js_selection_direction() - if not direction: + if not self._js_selection_left_to_right(): self._widget.triggerPageAction(QWebPage.SelectStartOfLine) - def _js_selection_direction(self): - # return true if selection's direction - # is left to right else false + def _js_selection_left_to_right(self): + """Return True iff the selection's direction is left to right.""" return self._tab.private_api.run_js_sync(""" - var sel = window.getSelection(); - var position = sel.anchorNode.compareDocumentPosition(sel.focusNode); - (!position && sel.anchorOffset < sel.focusOffset || - position === Node.DOCUMENT_POSITION_FOLLOWING); - """) + var sel = window.getSelection(); + var position = sel.anchorNode.compareDocumentPosition(sel.focusNode); + (!position && sel.anchorOffset < sel.focusOffset || + position === Node.DOCUMENT_POSITION_FOLLOWING); + """) def _follow_selected(self, *, tab=False): if QWebSettings.globalSettings().testAttribute( diff --git a/qutebrowser/javascript/caret.js b/qutebrowser/javascript/caret.js index 51a1cf7cf..e2063a2d4 100644 --- a/qutebrowser/javascript/caret.js +++ b/qutebrowser/javascript/caret.js @@ -1176,7 +1176,7 @@ window._qutebrowser.caret = (function() { }; CaretBrowsing.updateLineSelection = function(direction, granularity) { - if (!(granularity === "character") && !(granularity === "word")) { + if (granularity !== "character" && granularity !== "word") { window. getSelection(). modify("extend", direction, granularity); diff --git a/tests/unit/browser/test_caret.py b/tests/unit/browser/test_caret.py index 13994a654..2b65081f0 100644 --- a/tests/unit/browser/test_caret.py +++ b/tests/unit/browser/test_caret.py @@ -73,7 +73,7 @@ class Selection: def check_multiline(self, expected, *, strip=False): self.check(textwrap.dedent(expected).strip(), strip=strip) - def toggle(self, line=False): + def toggle(self, *, line=False): with self._qtbot.wait_signal(self._caret.selection_toggled): self._caret.toggle_selection(line=line) @@ -396,24 +396,24 @@ class TestReverse: class TestLineSelection: def test_toggle(self, caret, selection): - selection.toggle(True) + selection.toggle(line=True) selection.check("one two three") def test_toggle_untoggle(self, caret, selection): selection.toggle() selection.check("") - selection.toggle(True) + selection.toggle(line=True) selection.check("one two three") selection.toggle() selection.check("one two three") def test_from_center(self, caret, selection): caret.move_to_next_char(4) - selection.toggle(True) + selection.toggle(line=True) selection.check("one two three") def test_more_lines(self, caret, selection): - selection.toggle(True) + selection.toggle(line=True) caret.move_to_next_line(2) selection.check_multiline(""" one two three @@ -423,26 +423,26 @@ class TestLineSelection: """, strip=True) def test_not_selecting_char(self, caret, selection): - selection.toggle(True) + selection.toggle(line=True) caret.move_to_next_char() selection.check("one two three") caret.move_to_prev_char() selection.check("one two three") def test_selecting_prev_next_word(self, caret, selection): - selection.toggle(True) + selection.toggle(line=True) caret.move_to_next_word() selection.check("one two three") caret.move_to_prev_word() selection.check("one two three") def test_selecting_end_word(self, caret, selection): - selection.toggle(True) + selection.toggle(line=True) caret.move_to_end_of_word() selection.check("one two three") def test_selecting_prev_next_line(self, caret, selection): - selection.toggle(True) + selection.toggle(line=True) caret.move_to_next_line() selection.check_multiline(""" one two three @@ -452,14 +452,14 @@ class TestLineSelection: selection.check("one two three") def test_not_selecting_start_end_line(self, caret, selection): - selection.toggle(True) + selection.toggle(line=True) caret.move_to_end_of_line() selection.check("one two three") caret.move_to_start_of_line() selection.check("one two three") def test_selecting_block(self, caret, selection): - selection.toggle(True) + selection.toggle(line=True) caret.move_to_end_of_next_block() selection.check_multiline(""" one two three @@ -467,7 +467,7 @@ class TestLineSelection: """, strip=True) def test_selecting_start_end_document(self, caret, selection): - selection.toggle(True) + selection.toggle(line=True) caret.move_to_end_of_document() selection.check_multiline(""" one two three @@ -476,5 +476,6 @@ class TestLineSelection: four five six vier fünf sechs """, strip=True) + caret.move_to_start_of_document() selection.check("one two three") From 118882b861105428fc698d2c5cea6aecbd59b2ee Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 22 May 2020 19:13:31 +0200 Subject: [PATCH 026/245] tests: Skip failing caret test on macOS See #5459 --- tests/unit/browser/test_caret.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/browser/test_caret.py b/tests/unit/browser/test_caret.py index 2b65081f0..3aff8cce1 100644 --- a/tests/unit/browser/test_caret.py +++ b/tests/unit/browser/test_caret.py @@ -466,6 +466,8 @@ class TestLineSelection: eins zwei drei """, strip=True) + @pytest.mark.not_mac( + reason='https://github.com/qutebrowser/qutebrowser/issues/5459') def test_selecting_start_end_document(self, caret, selection): selection.toggle(line=True) caret.move_to_end_of_document() From 374d28de77d926d207b9658b23cd16dd1990a9fc Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 22 May 2020 19:50:32 +0200 Subject: [PATCH 027/245] webkittab: Make selection_state private The previous selection_enabled boolean was public, but didn't need to be since e50068021d084cf01507cd20693318189193e073. --- qutebrowser/browser/browsertab.py | 1 - qutebrowser/browser/webkit/webkittab.py | 89 ++++++++++++++----------- 2 files changed, 49 insertions(+), 41 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index f37d143b6..d11ec2fa1 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -452,7 +452,6 @@ class AbstractCaret(QObject): super().__init__(parent) self._tab = tab self._widget = typing.cast(QWidget, None) - self.selection_state = SelectionState.none self._mode_manager = mode_manager mode_manager.entered.connect(self._on_mode_entered) mode_manager.left.connect(self._on_mode_left) diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index e73833f67..ac10539e8 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -25,6 +25,7 @@ import xml.etree.ElementTree from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QPoint, QTimer, QSizeF, QSize from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import QWidget from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame from PyQt5.QtWebKit import QWebSettings from PyQt5.QtPrintSupport import QPrinter @@ -34,6 +35,7 @@ from qutebrowser.browser.webkit import (webview, tabhistory, webkitelem, webkitsettings) from qutebrowser.utils import qtutils, usertypes, utils, log, debug from qutebrowser.qt import sip +from qutebrowser.keyinput import modeman class WebKitAction(browsertab.AbstractAction): @@ -172,16 +174,23 @@ class WebKitCaret(browsertab.AbstractCaret): """QtWebKit implementations related to moving the cursor/selection.""" + def __init__(self, + tab: browsertab.AbstractTab, + mode_manager: modeman.ModeManager, + parent: QWidget = None) -> None: + super().__init__(tab, mode_manager, parent) + self._selection_state = browsertab.SelectionState.none + @pyqtSlot(usertypes.KeyMode) def _on_mode_entered(self, mode): if mode != usertypes.KeyMode.caret: return if self._widget.hasSelection(): - self.selection_state = browsertab.SelectionState.normal + self._selection_state = browsertab.SelectionState.normal else: - self.selection_state = browsertab.SelectionState.none - self.selection_toggled.emit(self.selection_state) + self._selection_state = browsertab.SelectionState.none + self.selection_toggled.emit(self._selection_state) settings = self._widget.settings() settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True) @@ -196,7 +205,7 @@ class WebKitCaret(browsertab.AbstractCaret): # # Note: We can't use hasSelection() here, as that's always # true in caret mode. - if self.selection_state is browsertab.SelectionState.none: + if self._selection_state is browsertab.SelectionState.none: self._widget.page().currentFrame().evaluateJavaScript( utils.read_file('javascript/position_caret.js')) @@ -204,37 +213,37 @@ class WebKitCaret(browsertab.AbstractCaret): def _on_mode_left(self, _mode): settings = self._widget.settings() if settings.testAttribute(QWebSettings.CaretBrowsingEnabled): - if (self.selection_state is not browsertab.SelectionState.none and + if (self._selection_state is not browsertab.SelectionState.none and self._widget.hasSelection()): # Remove selection if it exists self._widget.triggerPageAction(QWebPage.MoveToNextChar) settings.setAttribute(QWebSettings.CaretBrowsingEnabled, False) - self.selection_state = browsertab.SelectionState.none + self._selection_state = browsertab.SelectionState.none def move_to_next_line(self, count=1): - if self.selection_state is not browsertab.SelectionState.none: + if self._selection_state is not browsertab.SelectionState.none: act = QWebPage.SelectNextLine else: act = QWebPage.MoveToNextLine for _ in range(count): self._widget.triggerPageAction(act) - if self.selection_state is browsertab.SelectionState.line: + if self._selection_state is browsertab.SelectionState.line: self._select_line_to_end() def move_to_prev_line(self, count=1): - if self.selection_state is not browsertab.SelectionState.none: + if self._selection_state is not browsertab.SelectionState.none: act = QWebPage.SelectPreviousLine else: act = QWebPage.MoveToPreviousLine for _ in range(count): self._widget.triggerPageAction(act) - if self.selection_state is browsertab.SelectionState.line: + if self._selection_state is browsertab.SelectionState.line: self._select_line_to_start() def move_to_next_char(self, count=1): - if self.selection_state is browsertab.SelectionState.normal: + if self._selection_state is browsertab.SelectionState.normal: act = QWebPage.SelectNextChar - elif self.selection_state is browsertab.SelectionState.line: + elif self._selection_state is browsertab.SelectionState.line: return else: act = QWebPage.MoveToNextChar @@ -242,9 +251,9 @@ class WebKitCaret(browsertab.AbstractCaret): self._widget.triggerPageAction(act) def move_to_prev_char(self, count=1): - if self.selection_state is browsertab.SelectionState.normal: + if self._selection_state is browsertab.SelectionState.normal: act = QWebPage.SelectPreviousChar - elif self.selection_state is browsertab.SelectionState.line: + elif self._selection_state is browsertab.SelectionState.line: return else: act = QWebPage.MoveToPreviousChar @@ -252,11 +261,11 @@ class WebKitCaret(browsertab.AbstractCaret): self._widget.triggerPageAction(act) def move_to_end_of_word(self, count=1): - if self.selection_state is browsertab.SelectionState.normal: + if self._selection_state is browsertab.SelectionState.normal: act = [QWebPage.SelectNextWord] if utils.is_windows: # pragma: no cover act.append(QWebPage.SelectPreviousChar) - elif self.selection_state is browsertab.SelectionState.line: + elif self._selection_state is browsertab.SelectionState.line: return else: act = [QWebPage.MoveToNextWord] @@ -267,11 +276,11 @@ class WebKitCaret(browsertab.AbstractCaret): self._widget.triggerPageAction(a) def move_to_next_word(self, count=1): - if self.selection_state is browsertab.SelectionState.normal: + if self._selection_state is browsertab.SelectionState.normal: act = [QWebPage.SelectNextWord] if not utils.is_windows: # pragma: no branch act.append(QWebPage.SelectNextChar) - elif self.selection_state is browsertab.SelectionState.line: + elif self._selection_state is browsertab.SelectionState.line: return else: act = [QWebPage.MoveToNextWord] @@ -282,9 +291,9 @@ class WebKitCaret(browsertab.AbstractCaret): self._widget.triggerPageAction(a) def move_to_prev_word(self, count=1): - if self.selection_state is browsertab.SelectionState.normal: + if self._selection_state is browsertab.SelectionState.normal: act = QWebPage.SelectPreviousWord - elif self.selection_state is browsertab.SelectionState.line: + elif self._selection_state is browsertab.SelectionState.line: return else: act = QWebPage.MoveToPreviousWord @@ -292,25 +301,25 @@ class WebKitCaret(browsertab.AbstractCaret): self._widget.triggerPageAction(act) def move_to_start_of_line(self): - if self.selection_state is browsertab.SelectionState.normal: + if self._selection_state is browsertab.SelectionState.normal: act = QWebPage.SelectStartOfLine - elif self.selection_state is browsertab.SelectionState.line: + elif self._selection_state is browsertab.SelectionState.line: return else: act = QWebPage.MoveToStartOfLine self._widget.triggerPageAction(act) def move_to_end_of_line(self): - if self.selection_state is browsertab.SelectionState.normal: + if self._selection_state is browsertab.SelectionState.normal: act = QWebPage.SelectEndOfLine - elif self.selection_state is browsertab.SelectionState.line: + elif self._selection_state is browsertab.SelectionState.line: return else: act = QWebPage.MoveToEndOfLine self._widget.triggerPageAction(act) def move_to_start_of_next_block(self, count=1): - if self.selection_state is not browsertab.SelectionState.none: + if self._selection_state is not browsertab.SelectionState.none: act = [QWebPage.SelectNextLine, QWebPage.SelectStartOfBlock] else: @@ -319,11 +328,11 @@ class WebKitCaret(browsertab.AbstractCaret): for _ in range(count): for a in act: self._widget.triggerPageAction(a) - if self.selection_state is browsertab.SelectionState.line: + if self._selection_state is browsertab.SelectionState.line: self._select_line_to_end() def move_to_start_of_prev_block(self, count=1): - if self.selection_state is not browsertab.SelectionState.none: + if self._selection_state is not browsertab.SelectionState.none: act = [QWebPage.SelectPreviousLine, QWebPage.SelectStartOfBlock] else: @@ -332,11 +341,11 @@ class WebKitCaret(browsertab.AbstractCaret): for _ in range(count): for a in act: self._widget.triggerPageAction(a) - if self.selection_state is browsertab.SelectionState.line: + if self._selection_state is browsertab.SelectionState.line: self._select_line_to_start() def move_to_end_of_next_block(self, count=1): - if self.selection_state is not browsertab.SelectionState.none: + if self._selection_state is not browsertab.SelectionState.none: act = [QWebPage.SelectNextLine, QWebPage.SelectEndOfBlock] else: @@ -345,31 +354,31 @@ class WebKitCaret(browsertab.AbstractCaret): for _ in range(count): for a in act: self._widget.triggerPageAction(a) - if self.selection_state is browsertab.SelectionState.line: + if self._selection_state is browsertab.SelectionState.line: self._select_line_to_end() def move_to_end_of_prev_block(self, count=1): - if self.selection_state is not browsertab.SelectionState.none: + if self._selection_state is not browsertab.SelectionState.none: act = [QWebPage.SelectPreviousLine, QWebPage.SelectEndOfBlock] else: act = [QWebPage.MoveToPreviousLine, QWebPage.MoveToEndOfBlock] for _ in range(count): for a in act: self._widget.triggerPageAction(a) - if self.selection_state is browsertab.SelectionState.line: + if self._selection_state is browsertab.SelectionState.line: self._select_line_to_start() def move_to_start_of_document(self): - if self.selection_state is not browsertab.SelectionState.none: + if self._selection_state is not browsertab.SelectionState.none: act = QWebPage.SelectStartOfDocument else: act = QWebPage.MoveToStartOfDocument self._widget.triggerPageAction(act) - if self.selection_state is browsertab.SelectionState.line: + if self._selection_state is browsertab.SelectionState.line: self._select_line() def move_to_end_of_document(self): - if self.selection_state is not browsertab.SelectionState.none: + if self._selection_state is not browsertab.SelectionState.none: act = QWebPage.SelectEndOfDocument else: act = QWebPage.MoveToEndOfDocument @@ -377,16 +386,16 @@ class WebKitCaret(browsertab.AbstractCaret): def toggle_selection(self, line=False): if line: - self.selection_state = browsertab.SelectionState.line + self._selection_state = browsertab.SelectionState.line self._select_line() self.reverse_selection() self._select_line() self.reverse_selection() - elif self.selection_state is not browsertab.SelectionState.normal: - self.selection_state = browsertab.SelectionState.normal + elif self._selection_state is not browsertab.SelectionState.normal: + self._selection_state = browsertab.SelectionState.normal else: - self.selection_state = browsertab.SelectionState.none - self.selection_toggled.emit(self.selection_state) + self._selection_state = browsertab.SelectionState.none + self.selection_toggled.emit(self._selection_state) def drop_selection(self): self._widget.triggerPageAction(QWebPage.MoveToNextChar) From 0770ef11ebeac8072158a639c9297d5c4ef14dfb Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 22 May 2020 20:01:08 +0200 Subject: [PATCH 028/245] tests: Make sure entering caret mode is finished Otherwise, the caret JS code could still be initializing while the tests start to run. --- tests/unit/browser/test_caret.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/browser/test_caret.py b/tests/unit/browser/test_caret.py index 3aff8cce1..830dda0ff 100644 --- a/tests/unit/browser/test_caret.py +++ b/tests/unit/browser/test_caret.py @@ -34,7 +34,8 @@ def caret(web_tab, qtbot, mode_manager): with qtbot.wait_signal(web_tab.load_finished, timeout=10000): web_tab.load_url(QUrl('qute://testdata/data/caret.html')) - mode_manager.enter(usertypes.KeyMode.caret) + with qtbot.wait_signal(web_tab.caret.selection_toggled): + mode_manager.enter(usertypes.KeyMode.caret) return web_tab.caret From 7a0cbf54fce491a48a5518c6c7422ab5d998d0a2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 22 May 2020 20:01:51 +0200 Subject: [PATCH 029/245] caret: Fix toggling behavior with QtWebEngine The behavior when pressing `v` in line selection mode was different between QtWebKit and QtWebEngine: With QtWebKit, normal selection mode was entered, while with QtWebEngine, selection mode was left. Do the former with QtWebEngine as well, as that's also what vim does. --- qutebrowser/javascript/caret.js | 2 +- tests/unit/browser/test_caret.py | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/qutebrowser/javascript/caret.js b/qutebrowser/javascript/caret.js index e2063a2d4..2d0bced84 100644 --- a/qutebrowser/javascript/caret.js +++ b/qutebrowser/javascript/caret.js @@ -1452,7 +1452,7 @@ window._qutebrowser.caret = (function() { CaretBrowsing.SelectionState.LINE; CaretBrowsing.selectLine(); CaretBrowsing.finishMove(); - } else if (CaretBrowsing.selectionState === CaretBrowsing.SelectionState.NONE) { + } else if (CaretBrowsing.selectionState !== CaretBrowsing.SelectionState.NORMAL) { CaretBrowsing.selectionState = CaretBrowsing.SelectionState.NORMAL; } else { CaretBrowsing.selectionState = CaretBrowsing.SelectionState.NONE; diff --git a/tests/unit/browser/test_caret.py b/tests/unit/browser/test_caret.py index 830dda0ff..7d1325612 100644 --- a/tests/unit/browser/test_caret.py +++ b/tests/unit/browser/test_caret.py @@ -25,6 +25,7 @@ import pytest from PyQt5.QtCore import QUrl from qutebrowser.utils import usertypes +from qutebrowser.browser import browsertab @pytest.fixture @@ -75,8 +76,10 @@ class Selection: self.check(textwrap.dedent(expected).strip(), strip=strip) def toggle(self, *, line=False): - with self._qtbot.wait_signal(self._caret.selection_toggled): + """Toggle the selection and return the new selection state.""" + with self._qtbot.wait_signal(self._caret.selection_toggled) as blocker: self._caret.toggle_selection(line=line) + return blocker.args[0] @pytest.fixture @@ -84,6 +87,18 @@ def selection(qtbot, caret): return Selection(qtbot, caret) +def test_toggle(caret, selection, qtbot): + """Make sure calling toggleSelection produces the correct callback values. + + This also makes sure that the SelectionState enum in JS lines up with the + Python browsertab.SelectionState enum. + """ + assert selection.toggle() == browsertab.SelectionState.normal + assert selection.toggle(line=True) == browsertab.SelectionState.line + assert selection.toggle() == browsertab.SelectionState.normal + assert selection.toggle() == browsertab.SelectionState.none + + class TestDocument: def test_selecting_entire_document(self, caret, selection): From d57fe79f5f568113f97392c91087702bddf0fec3 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 22 May 2020 20:05:38 +0200 Subject: [PATCH 030/245] caret: Use strings instead of ints for enums --- qutebrowser/browser/browsertab.py | 7 +++++-- qutebrowser/browser/webengine/webenginetab.py | 4 ++-- qutebrowser/javascript/caret.js | 10 ++++++---- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index d11ec2fa1..ba758abc7 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -427,9 +427,12 @@ class AbstractZoom(QObject): self._set_factor_internal(self._zoom_factor) -class SelectionState(enum.IntEnum): +class SelectionState(enum.Enum): - """Possible states of selection in Caret mode.""" + """Possible states of selection in caret mode. + + NOTE: Names need to line up with SelectionState in caret.js! + """ none = 1 normal = 2 diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 5b0721c18..33db6e631 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -504,8 +504,8 @@ class WebEngineCaret(browsertab.AbstractCaret): code = javascript.assemble('caret', command, *args) self._tab.run_js_async(code, callback) - def _toggle_sel_translate(self, state_int): - state = browsertab.SelectionState(state_int) + def _toggle_sel_translate(self, state_str): + state = browsertab.SelectionState[state_str] self.selection_toggled.emit(state) diff --git a/qutebrowser/javascript/caret.js b/qutebrowser/javascript/caret.js index 2d0bced84..d7ba88fe6 100644 --- a/qutebrowser/javascript/caret.js +++ b/qutebrowser/javascript/caret.js @@ -706,13 +706,15 @@ window._qutebrowser.caret = (function() { CaretBrowsing.isCaretVisible = false; /** - * selection modes + * Selection modes. + * NOTE: Values need to line up with SelectionState in browsertab.py! + * * @type {enum} */ CaretBrowsing.SelectionState = { - "NONE": 1, - "NORMAL": 2, - "LINE": 3, + "NONE": "none", + "NORMAL": "normal", + "LINE": "line", }; /** From dcab5eb11b8b018bb296475c24e5716a2ac415ca Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 22 May 2020 17:12:51 +0200 Subject: [PATCH 031/245] src2asciidoc: Make sure usage lines are generated with fixed width Due to a change in Python 3.8, the output depended on the calling terminal's width. Set a fixed with of 200 (rather than 80) so that we always have the expanded version for the generated documentation. See #5393 and https://github.com/python/cpython/commit/74102c9a5f2327c4fc47feefa072854a53551d1f#diff-837b312b1f3508216ace6adb46492836 --- doc/help/commands.asciidoc | 17 +++++------------ scripts/dev/src2asciidoc.py | 5 +++++ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 565b87ac8..3266c7153 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -285,8 +285,7 @@ Set all settings back to their default. [[config-cycle]] === config-cycle -Syntax: +:config-cycle [*--pattern* 'pattern'] [*--temp*] [*--print*] - 'option' ['values' ['values' ...]]+ +Syntax: +:config-cycle [*--pattern* 'pattern'] [*--temp*] [*--print*] 'option' ['values' ['values' ...]]+ Cycle an option between multiple values. @@ -597,8 +596,7 @@ Show help about a command or setting. [[hint]] === hint -Syntax: +:hint [*--mode* 'mode'] [*--add-history*] [*--rapid*] [*--first*] - ['group'] ['target'] ['args' ['args' ...]]+ +Syntax: +:hint [*--mode* 'mode'] [*--add-history*] [*--rapid*] [*--first*] ['group'] ['target'] ['args' ['args' ...]]+ Start hinting. @@ -855,8 +853,7 @@ Do nothing. [[open]] === open -Syntax: +:open [*--related*] [*--bg*] [*--tab*] [*--window*] [*--secure*] [*--private*] - ['url']+ +Syntax: +:open [*--related*] [*--bg*] [*--tab*] [*--window*] [*--secure*] [*--private*] ['url']+ Open a URL in the current/[count]th tab. @@ -1186,9 +1183,7 @@ Load a session. [[session-save]] === session-save -Syntax: +:session-save [*--current*] [*--quiet*] [*--force*] [*--only-active-window*] - [*--with-private*] - ['name']+ +Syntax: +:session-save [*--current*] [*--quiet*] [*--force*] [*--only-active-window*] [*--with-private*] ['name']+ Save a session. @@ -1252,9 +1247,7 @@ Set a mark at the current scroll position in the current tab. [[spawn]] === spawn -Syntax: +:spawn [*--userscript*] [*--verbose*] [*--output*] [*--output-messages*] - [*--detach*] - 'cmdline'+ +Syntax: +:spawn [*--userscript*] [*--verbose*] [*--output*] [*--output-messages*] [*--detach*] 'cmdline'+ Spawn an external command. diff --git a/scripts/dev/src2asciidoc.py b/scripts/dev/src2asciidoc.py index b82ede3e1..1d3ad8bf4 100755 --- a/scripts/dev/src2asciidoc.py +++ b/scripts/dev/src2asciidoc.py @@ -59,6 +59,11 @@ class UsageFormatter(argparse.HelpFormatter): argparse.HelpFormatter while copying 99% of the code :-/ """ + def __init__(self, prog, indent_increment=2, max_help_position=24, + width=200): + """Override __init__ to set a fixed width as default.""" + super().__init__(prog, indent_increment, max_help_position, width) + def _format_usage(self, usage, actions, groups, _prefix): """Override _format_usage to not add the 'usage:' prefix.""" return super()._format_usage(usage, actions, groups, '') From 3d950c7611e5f45d6fc5373c85f64899d51a8111 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 22 May 2020 21:29:24 +0200 Subject: [PATCH 032/245] Update docs --- doc/changelog.asciidoc | 1 + doc/help/commands.asciidoc | 5 +++++ doc/help/settings.asciidoc | 1 + 3 files changed, 7 insertions(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 3d39eec85..510ffeef2 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -35,6 +35,7 @@ Added - New `:debug-keytester` command, which shows a "key tester" widget. Previously, that was only available as a separate application via `python3 -m scripts.keytester`. +- New line selection mode (`:toggle-selection --line`), bound to `Shift-V` in caret mode. Fixed ~~~~~ diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 3266c7153..608329fe8 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -1885,8 +1885,13 @@ This acts like readline's yank. [[toggle-selection]] === toggle-selection +Syntax: +:toggle-selection [*--line*]+ + Toggle caret selection mode. +==== optional arguments +* +*-l*+, +*--line*+: Enables line-selection. + == Debugging commands These commands are mainly intended for debugging. They are hidden if qutebrowser was started without the `--debug`-flag. diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 5182968a6..ca1f41fb9 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -444,6 +444,7 @@ Default: * +pass:[J]+: +pass:[scroll down]+ * +pass:[K]+: +pass:[scroll up]+ * +pass:[L]+: +pass:[scroll right]+ +* +pass:[V]+: +pass:[toggle-selection --line]+ * +pass:[Y]+: +pass:[yank selection -s]+ * +pass:[[]+: +pass:[move-to-start-of-prev-block]+ * +pass:[]]+: +pass:[move-to-start-of-next-block]+ From 57bc2b49c6ff6c67a0a8a6967389f03a12473e9f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 22 May 2020 21:29:33 +0200 Subject: [PATCH 033/245] Fix segfault with test_webenginetab and Qt 5.9 --- tests/helpers/fixtures.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py index 0624ef698..aed243b4b 100644 --- a/tests/helpers/fixtures.py +++ b/tests/helpers/fixtures.py @@ -223,7 +223,8 @@ def webkit_tab(web_tab_setup, qtbot, cookiejar_and_cache, mode_manager, @pytest.fixture def webengine_tab(web_tab_setup, qtbot, redirect_webengine_data, - tabbed_browser_stubs, mode_manager, widget_container): + tabbed_browser_stubs, mode_manager, widget_container, + monkeypatch): tabwidget = tabbed_browser_stubs[0].widget tabwidget.current_index = 0 tabwidget.index_of = 0 @@ -249,6 +250,7 @@ def webengine_tab(web_tab_setup, qtbot, redirect_webengine_data, # tests/unit/browser/test_caret.py). # However, with Qt < 5.12, doing this here will lead to an immediate # segfault... + monkeypatch.undo() # version_check could be patched if qtutils.version_check('5.12'): sip.delete(tab._widget) From 399ff092dc3880ebc56b80cd6d8dcdd00ff341a5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 22 May 2020 22:25:40 +0200 Subject: [PATCH 034/245] Fix lint --- qutebrowser/browser/commands.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 9f328c768..f115501d3 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -453,7 +453,7 @@ class CommandDispatcher: @cmdutils.argument('win_id', completion=miscmodels.window) @cmdutils.argument('count', value=cmdutils.Value.count) def tab_give(self, win_id: int = None, keep: bool = False, - count: int = None, private: bool = False) -> None: + count: int = None, private: bool = False) -> None: """Give the current tab to a new or existing window if win_id given. If no win_id is given, the tab will get detached into a new window. @@ -490,7 +490,8 @@ class CommandDispatcher: window=win_id) if private and not tabbed_browser.is_private: - raise cmdutils.CommandError("The window with id {} is not private".format(win_id)) + raise cmdutils.CommandError( + "The window with id {} is not private".format(win_id)) tabbed_browser.tabopen(self._current_url()) if not keep: From 1463fc4e35910defe3ddb30ae292c9db37dbe4d4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 22 May 2020 22:26:09 +0200 Subject: [PATCH 035/245] Update docs --- doc/changelog.asciidoc | 1 + doc/help/commands.asciidoc | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 46c72e4e2..535a2f16b 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -51,6 +51,7 @@ Changed `misc/requirements/requirements-tests.txt{,-raw}` is supported. - The `:tab-focus` command now has completion for tabs in the current window. - The `bindings.key_mappings` setting now maps `` to the tab key by default. +- `:tab-give --private` now detaches a tab into a new private window. Fixed ~~~~~ diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 8cd8a62c8..dc40c76a6 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -1341,7 +1341,7 @@ The tab index to focus, starting with 1. [[tab-give]] === tab-give -Syntax: +:tab-give [*--keep*] ['win-id']+ +Syntax: +:tab-give [*--keep*] [*--private*] ['win-id']+ Give the current tab to a new or existing window if win_id given. @@ -1352,6 +1352,7 @@ If no win_id is given, the tab will get detached into a new window. ==== optional arguments * +*-k*+, +*--keep*+: If given, keep the old tab around. +* +*-p*+, +*--private*+: If the tab should be detached into a private instance. ==== count Overrides win_id (index starts at 1 for win_id=0). From ea9f2fcf49fb16ce90422096cd097cd54b2592f8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 22 May 2020 22:42:53 +0200 Subject: [PATCH 036/245] Handle None in _toggle_sel_translate This is the equivalent of the previous: TypeError: AbstractCaret.selection_toggled[bool].emit(): argument 1 has unexpected type 'NoneType' When e.g. doing: qutebrowser --temp-basedir ':later 100 enter-mode caret' ':later 110 toggle-selection' Handle this case and show an error instead of crashing. --- qutebrowser/browser/webengine/webenginetab.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index cd305d11a..709c5c1b5 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -531,6 +531,9 @@ class WebEngineCaret(browsertab.AbstractCaret): self._tab.run_js_async(code, callback) def _toggle_sel_translate(self, state_str): + if state_str is None: + message.error("Error toggling caret selection") + return state = browsertab.SelectionState[state_str] self.selection_toggled.emit(state) From e471b6aa299e01af4d4c341a949017e16467b60b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 22 May 2020 23:04:45 +0200 Subject: [PATCH 037/245] Update changelog --- doc/changelog.asciidoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 535a2f16b..741558ec1 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -64,6 +64,8 @@ Fixed - `:inspector` now works correctly when cookies are disabled globally. - Added workaround for a (Gentoo?) PyQt/packaging issue related to the `QWebEngineFindTextResult` handling added in v1.11.0. +- When entering caret selection mode (`v, v`) very early before a page is + loaded, an error is now shown instead of a crash happening. v1.11.1 (2020-05-07) -------------------- From fc4cdb26b20f33a09ddfa3d421a12de4929f589f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 22 May 2020 23:06:11 +0200 Subject: [PATCH 038/245] Fix imports after merge --- qutebrowser/browser/webkit/webkittab.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index db97af153..1e9276265 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -28,7 +28,6 @@ from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QWidget from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame from PyQt5.QtWebKit import QWebSettings -from PyQt5.QtWidgets import QWidget from PyQt5.QtPrintSupport import QPrinter from qutebrowser.browser import browsertab, shared @@ -37,7 +36,6 @@ from qutebrowser.browser.webkit import (webview, tabhistory, webkitelem, from qutebrowser.utils import qtutils, usertypes, utils, log, debug from qutebrowser.keyinput import modeman from qutebrowser.qt import sip -from qutebrowser.keyinput import modeman class WebKitAction(browsertab.AbstractAction): From 6b1d1bd2e112350671b15af1aa7a2c949d63a9a6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 22 May 2020 23:07:54 +0200 Subject: [PATCH 039/245] eslint: Turn off no-negated-condition --- qutebrowser/javascript/.eslintrc.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/qutebrowser/javascript/.eslintrc.yaml b/qutebrowser/javascript/.eslintrc.yaml index 4fdd43854..23456e801 100644 --- a/qutebrowser/javascript/.eslintrc.yaml +++ b/qutebrowser/javascript/.eslintrc.yaml @@ -62,3 +62,4 @@ rules: max-params: "off" prefer-named-capture-group: "off" function-call-argument-newline: "off" + no-negated-condition: "off" From 8803d77985a854733de81ba86c3a1c34e7db8dfc Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Fri, 22 May 2020 18:01:57 -0400 Subject: [PATCH 040/245] Remove unused mocker argument. Co-authored-by: Florian Bruhin --- tests/unit/utils/test_log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/utils/test_log.py b/tests/unit/utils/test_log.py index c8b778149..e3fc1e2ee 100644 --- a/tests/unit/utils/test_log.py +++ b/tests/unit/utils/test_log.py @@ -263,7 +263,7 @@ class TestInitLog: ('INFO', 'WARNING', logging.INFO, 'VDEBUG', logging.VDEBUG), ('WARNING', 'INFO', logging.WARNING, 'CRITICAL', logging.CRITICAL), ]) - def test_init_from_config(self, mocker, console_cli, console_conf, + def test_init_from_config(self, console_cli, console_conf, console_expected, ram_conf, ram_expected, args, config_stub): args.debug = False From 92e9de19ebb21702cbcc124a8c2286c3d5d08f84 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Fri, 22 May 2020 18:07:38 -0400 Subject: [PATCH 041/245] Don't configure loglevels for nil handlers. ram_handler might be None if --loglines=0 is passed. console_handler is only None if sys.stderr is None, which probably never happens, but let's just check anyways. --- qutebrowser/utils/log.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 20a3dd4fb..73ef1b042 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -540,15 +540,13 @@ def init_from_config(conf: 'configmodule.ConfigContainer') -> None: This is passed rather than accessed via the module to avoid a cyclic import. """ - assert ram_handler is not None - assert console_handler is not None assert _args is not None ramlevel = conf.logging.level.ram consolelevel = conf.logging.level.console - if ramlevel: + if ram_handler and ramlevel: init.info("Configuring RAM loglevel to %s", ramlevel) ram_handler.setLevel(LOG_LEVELS[ramlevel]) - if consolelevel: + if console_handler and consolelevel: if _args.loglevel: init.info("--loglevel flag overrides logging.level.console") else: From abe2c4b351dd10ac06531b8bbf74b56e149ea34a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 23 May 2020 13:50:51 +0200 Subject: [PATCH 042/245] Clarify effect of content.cookies.accept --- doc/help/settings.asciidoc | 1 + qutebrowser/config/configdata.yml | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index d7b07db34..d0ca82515 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -1762,6 +1762,7 @@ This setting is only available with the QtWebEngine backend. [[content.cookies.accept]] === content.cookies.accept Which cookies to accept. +With QtWebEngine, this setting also controls other features with tracking capabilities similar to those of cookies; including IndexedDB, DOM storage, filesystem API, service workers, and AppCache. Note that with QtWebKit, only `all` and `never` are supported as per-domain values. Setting `no-3rdparty` or `no-unknown-3rdparty` per-domain on QtWebKit will have the same effect as `all`. This setting supports URL patterns. diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 01e9fe64b..1ea93793b 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -365,6 +365,10 @@ content.cookies.accept: desc: >- Which cookies to accept. + With QtWebEngine, this setting also controls other features with tracking + capabilities similar to those of cookies; including IndexedDB, DOM storage, + filesystem API, service workers, and AppCache. + Note that with QtWebKit, only `all` and `never` are supported as per-domain values. Setting `no-3rdparty` or `no-unknown-3rdparty` per-domain on QtWebKit will have the same effect as `all`. From 1a97a1b5c74cb33ef2e0160e4a9100a59cd27d15 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 25 May 2020 15:45:34 +0200 Subject: [PATCH 043/245] caret: Add V binding in normal mode See #5264 --- doc/help/settings.asciidoc | 1 + qutebrowser/config/configdata.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index d0ca82515..4c20126c3 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -591,6 +591,7 @@ Default: * +pass:[Sq]+: +pass:[open qute://bookmarks]+ * +pass:[Ss]+: +pass:[open qute://settings]+ * +pass:[T]+: +pass:[tab-focus]+ +* +pass:[V]+: +pass:[enter-mode caret ;; toggle-selection --line]+ * +pass:[ZQ]+: +pass:[quit]+ * +pass:[ZZ]+: +pass:[quit --save]+ * +pass:[[[]+: +pass:[navigate prev]+ diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 1ea93793b..3700e5e1d 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -2901,6 +2901,7 @@ bindings.default: N: search-prev i: enter-mode insert v: enter-mode caret + V: enter-mode caret ;; toggle-selection --line "`": enter-mode set_mark "'": enter-mode jump_mark yy: yank From a17bfea32fc0b4b607977403510d1541742c6d2a Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Mon, 25 May 2020 10:27:30 -0400 Subject: [PATCH 044/245] Lowercase loglevel config values Co-authored-by: Florian Bruhin --- qutebrowser/config/configtypes.py | 3 ++- qutebrowser/utils/log.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 7927e0425..98926ca5e 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -2020,7 +2020,8 @@ class LogLevel(String): def __init__(self, none_ok: bool = False) -> None: super().__init__(none_ok=none_ok) - self.valid_values = ValidValues(*log.LOG_LEVELS.keys()) + self.valid_values = ValidValues(*[level.lower() + for level in log.LOG_LEVELS]) class Key(BaseType): diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 73ef1b042..4b19a9a84 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -545,13 +545,13 @@ def init_from_config(conf: 'configmodule.ConfigContainer') -> None: consolelevel = conf.logging.level.console if ram_handler and ramlevel: init.info("Configuring RAM loglevel to %s", ramlevel) - ram_handler.setLevel(LOG_LEVELS[ramlevel]) + ram_handler.setLevel(LOG_LEVELS[ramlevel].upper()) if console_handler and consolelevel: if _args.loglevel: init.info("--loglevel flag overrides logging.level.console") else: init.info("Configuring console loglevel to %s", consolelevel) - console_handler.setLevel(LOG_LEVELS[consolelevel]) + console_handler.setLevel(LOG_LEVELS[consolelevel].upper()) class QtWarningFilter(logging.Filter): From 1a8afe05103a443f0675e5683cebe4ca9ee0f072 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Mon, 25 May 2020 10:27:53 -0400 Subject: [PATCH 045/245] Make loglevel docstring consistent Co-authored-by: Florian Bruhin --- qutebrowser/config/configtypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 98926ca5e..546d90c38 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -2016,7 +2016,7 @@ class NewTabPosition(String): class LogLevel(String): - """Log level.""" + """A logging level.""" def __init__(self, none_ok: bool = False) -> None: super().__init__(none_ok=none_ok) From e28e707930e23004def77f96bf12104ff4789bf0 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Mon, 25 May 2020 10:35:23 -0400 Subject: [PATCH 046/245] Use defaults for loglevel configs. There's no reason for them to be None, use defaults so the behavior is better documented.. --- qutebrowser/config/configdata.yml | 6 ++---- qutebrowser/utils/log.py | 12 ++++++------ tests/unit/utils/test_log.py | 10 +++++----- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index d7988f571..6d8078ad2 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -3198,18 +3198,16 @@ bindings.commands: ## logging logging.level.ram: - default: null + default: debug type: name: LogLevel - none_ok: true desc: Level for in-memory logs. logging.level.console: - default: null + default: info type: name: LogLevel - none_ok: true desc: >- Level for console (stdout/stderr) logs. Ignored if the --loglevel or --debug CLI flags are used. diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 4b19a9a84..635fccabb 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -541,17 +541,17 @@ def init_from_config(conf: 'configmodule.ConfigContainer') -> None: cyclic import. """ assert _args is not None - ramlevel = conf.logging.level.ram - consolelevel = conf.logging.level.console - if ram_handler and ramlevel: + if ram_handler: + ramlevel = conf.logging.level.ram init.info("Configuring RAM loglevel to %s", ramlevel) - ram_handler.setLevel(LOG_LEVELS[ramlevel].upper()) - if console_handler and consolelevel: + ram_handler.setLevel(LOG_LEVELS[ramlevel.upper()]) + if console_handler: + consolelevel = conf.logging.level.console if _args.loglevel: init.info("--loglevel flag overrides logging.level.console") else: init.info("Configuring console loglevel to %s", consolelevel) - console_handler.setLevel(LOG_LEVELS[consolelevel].upper()) + console_handler.setLevel(LOG_LEVELS[consolelevel.upper()]) class QtWarningFilter(logging.Filter): diff --git a/tests/unit/utils/test_log.py b/tests/unit/utils/test_log.py index e3fc1e2ee..8ad870f73 100644 --- a/tests/unit/utils/test_log.py +++ b/tests/unit/utils/test_log.py @@ -257,11 +257,11 @@ class TestInitLog: @pytest.mark.parametrize( 'console_cli, console_conf, console_expected, ram_conf, ram_expected', [ - (None, None, logging.INFO, None, logging.NOTSET), - (None, None, logging.INFO, 'CRITICAL', logging.CRITICAL), - (None, 'WARNING', logging.WARNING, 'INFO', logging.INFO), - ('INFO', 'WARNING', logging.INFO, 'VDEBUG', logging.VDEBUG), - ('WARNING', 'INFO', logging.WARNING, 'CRITICAL', logging.CRITICAL), + (None, 'info', logging.INFO, 'debug', logging.DEBUG), + (None, 'info', logging.INFO, 'critical', logging.CRITICAL), + (None, 'warning', logging.WARNING, 'info', logging.INFO), + ('info', 'warning', logging.INFO, 'vdebug', logging.VDEBUG), + ('warning', 'info', logging.WARNING, 'critical', logging.CRITICAL), ]) def test_init_from_config(self, console_cli, console_conf, console_expected, ram_conf, ram_expected, args, From dc20806ab2e68d2201f801e5c659b16b9442e063 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Mon, 25 May 2020 10:40:05 -0400 Subject: [PATCH 047/245] Split up init_from_config test. The RAM and console configuration are independent, and splitting them into two separate tests makes the tests easier to read, as there aren't an overwhelming number of arguments and permutations. --- tests/unit/utils/test_log.py | 40 ++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/tests/unit/utils/test_log.py b/tests/unit/utils/test_log.py index 8ad870f73..eb634d76a 100644 --- a/tests/unit/utils/test_log.py +++ b/tests/unit/utils/test_log.py @@ -254,27 +254,37 @@ class TestInitLog: with pytest.raises(PendingDeprecationWarning): warnings.warn("test warning", PendingDeprecationWarning) - @pytest.mark.parametrize( - 'console_cli, console_conf, console_expected, ram_conf, ram_expected', + @pytest.mark.parametrize('cli, conf, expected', [ - (None, 'info', logging.INFO, 'debug', logging.DEBUG), - (None, 'info', logging.INFO, 'critical', logging.CRITICAL), - (None, 'warning', logging.WARNING, 'info', logging.INFO), - ('info', 'warning', logging.INFO, 'vdebug', logging.VDEBUG), - ('warning', 'info', logging.WARNING, 'critical', logging.CRITICAL), + (None, 'info', logging.INFO), + (None, 'warning', logging.WARNING), + ('info', 'warning', logging.INFO), + ('warning', 'info', logging.WARNING), ]) - def test_init_from_config(self, console_cli, console_conf, - console_expected, ram_conf, ram_expected, args, - config_stub): + def test_init_from_config_console(self, cli, conf, expected, args, + config_stub): args.debug = False - args.loglevel = console_cli + args.loglevel = cli log.init_log(args) - config_stub.val.logging.level.ram = ram_conf - config_stub.val.logging.level.console = console_conf + config_stub.val.logging.level.console = conf log.init_from_config(config_stub.val) - assert log.ram_handler.level == ram_expected - assert log.console_handler.level == console_expected + assert log.console_handler.level == expected + + @pytest.mark.parametrize('conf, expected', + [ + ('vdebug', logging.VDEBUG), + ('debug', logging.DEBUG), + ('info', logging.INFO), + ('critical', logging.CRITICAL), + ]) + def test_init_from_config_ram(self, conf, expected, args, config_stub): + args.debug = False + log.init_log(args) + + config_stub.val.logging.level.ram = conf + log.init_from_config(config_stub.val) + assert log.ram_handler.level == expected class TestHideQtWarning: From e2475b9f23f3e38cf306fb2773dcab8200af3150 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 25 May 2020 17:56:17 +0200 Subject: [PATCH 048/245] tests: Don't suppress output if on CI --- tests/end2end/fixtures/testprocess.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/end2end/fixtures/testprocess.py b/tests/end2end/fixtures/testprocess.py index 3c19b86ef..08f9754db 100644 --- a/tests/end2end/fixtures/testprocess.py +++ b/tests/end2end/fixtures/testprocess.py @@ -74,7 +74,10 @@ def _render_log(data, *, verbose, threshold=100): data = [str(d) for d in data] is_exception = any('Traceback (most recent call last):' in line or 'Uncaught exception' in line for line in data) - if len(data) > threshold and not verbose and not is_exception: + if (len(data) > threshold and + not verbose and + not is_exception and + not utils.ON_CI): msg = '[{} lines suppressed, use -v to show]'.format( len(data) - threshold) data = [msg] + data[-threshold:] From 27c737599556110e486bb7dbf2a7b9c7d7b9fa47 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 25 May 2020 21:11:09 +0200 Subject: [PATCH 049/245] Update codecov from 2.1.0 to 2.1.3 --- misc/requirements/requirements-codecov.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-codecov.txt b/misc/requirements/requirements-codecov.txt index 54d5bdfb4..0a38e12b5 100644 --- a/misc/requirements/requirements-codecov.txt +++ b/misc/requirements/requirements-codecov.txt @@ -2,7 +2,7 @@ certifi==2020.4.5.1 chardet==3.0.4 -codecov==2.1.0 +codecov==2.1.3 coverage==5.1 idna==2.9 requests==2.23.0 From 06396a7d2f4bafc9c5f4b0a3e42db76b25833256 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 25 May 2020 21:11:10 +0200 Subject: [PATCH 050/245] Update lxml from 4.5.0 to 4.5.1 --- misc/requirements/requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt index 1c790abc4..83ba9a82e 100644 --- a/misc/requirements/requirements-dev.txt +++ b/misc/requirements/requirements-dev.txt @@ -11,7 +11,7 @@ github3.py==1.3.0 hunter==3.1.3 idna==2.9 jwcrypto==0.7 -lxml==4.5.0 +lxml==4.5.1 manhole==1.6.0 packaging==20.3 pycparser==2.20 From a728480328cb7ce3a2b7b61891f4ec3c8288761e Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 25 May 2020 21:11:11 +0200 Subject: [PATCH 051/245] Update packaging from 20.3 to 20.4 --- misc/requirements/requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt index 83ba9a82e..cf40bd5bb 100644 --- a/misc/requirements/requirements-dev.txt +++ b/misc/requirements/requirements-dev.txt @@ -13,7 +13,7 @@ idna==2.9 jwcrypto==0.7 lxml==4.5.1 manhole==1.6.0 -packaging==20.3 +packaging==20.4 pycparser==2.20 Pympler==0.8 pyparsing==2.4.7 From 357faf1bcabb30c38edaea6f49178449d7d6fbaf Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 25 May 2020 21:11:12 +0200 Subject: [PATCH 052/245] Update packaging from 20.3 to 20.4 --- misc/requirements/requirements-pip.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-pip.txt b/misc/requirements/requirements-pip.txt index 92b8cf057..6d2ee85ca 100644 --- a/misc/requirements/requirements-pip.txt +++ b/misc/requirements/requirements-pip.txt @@ -1,7 +1,7 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py appdirs==1.4.4 -packaging==20.3 +packaging==20.4 pyparsing==2.4.7 setuptools==46.4.0 six==1.14.0 From a2150d9c4a35cb597ec8b2856e908c942c2f033f Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 25 May 2020 21:11:13 +0200 Subject: [PATCH 053/245] Update packaging from 20.3 to 20.4 --- misc/requirements/requirements-sphinx.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt index 1556e1d93..8fb334a72 100644 --- a/misc/requirements/requirements-sphinx.txt +++ b/misc/requirements/requirements-sphinx.txt @@ -9,7 +9,7 @@ idna==2.9 imagesize==1.2.0 Jinja2==2.11.2 MarkupSafe==1.1.1 -packaging==20.3 +packaging==20.4 Pygments==2.6.1 pyparsing==2.4.7 pytz==2020.1 From 7303e52a41ee6b4a1e85e39f99dc6573ad4c220a Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 25 May 2020 21:11:14 +0200 Subject: [PATCH 054/245] Update packaging from 20.3 to 20.4 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 8e94f36d6..87c4b2881 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -18,7 +18,7 @@ Mako==1.1.2 manhole==1.6.0 # MarkupSafe==1.1.1 more-itertools==8.3.0 -packaging==20.3 +packaging==20.4 parse==1.15.0 parse-type==0.5.2 pluggy==0.13.1 From 6e009ba0a79d99d8c426411a1ae9cb4ad680e5bb Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 25 May 2020 21:11:15 +0200 Subject: [PATCH 055/245] Update packaging from 20.3 to 20.4 --- misc/requirements/requirements-tox.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt index 33bcd072b..78da631cb 100644 --- a/misc/requirements/requirements-tox.txt +++ b/misc/requirements/requirements-tox.txt @@ -3,7 +3,7 @@ appdirs==1.4.4 distlib==0.3.0 filelock==3.0.12 -packaging==20.3 +packaging==20.4 pluggy==0.13.1 py==1.8.1 pyparsing==2.4.7 From e3ba806cfcd74429d20927652889c57a011a2469 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 25 May 2020 21:11:17 +0200 Subject: [PATCH 056/245] Update six from 1.14.0 to 1.15.0 --- misc/requirements/requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt index cf40bd5bb..620e5772c 100644 --- a/misc/requirements/requirements-dev.txt +++ b/misc/requirements/requirements-dev.txt @@ -21,7 +21,7 @@ PyQt-builder==1.3.2 python-dateutil==2.8.1 requests==2.23.0 sip==5.2.0 -six==1.14.0 +six==1.15.0 toml==0.10.1 uritemplate==3.0.1 urllib3==1.25.9 From 55041090651633d97d4ae419056b0e23829b069a Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 25 May 2020 21:11:18 +0200 Subject: [PATCH 057/245] Update six from 1.14.0 to 1.15.0 --- misc/requirements/requirements-flake8.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt index e4b59b368..e636b3abc 100644 --- a/misc/requirements/requirements-flake8.txt +++ b/misc/requirements/requirements-flake8.txt @@ -20,5 +20,5 @@ pep8-naming==0.10.0 pycodestyle==2.6.0 pydocstyle==5.0.2 pyflakes==2.2.0 -six==1.14.0 +six==1.15.0 snowballstemmer==2.0.0 From 749af343cc99afd952af938d9a549f8a4dad4b68 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 25 May 2020 21:11:19 +0200 Subject: [PATCH 058/245] Update six from 1.14.0 to 1.15.0 --- misc/requirements/requirements-pip.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-pip.txt b/misc/requirements/requirements-pip.txt index 6d2ee85ca..3a71ad0a8 100644 --- a/misc/requirements/requirements-pip.txt +++ b/misc/requirements/requirements-pip.txt @@ -4,5 +4,5 @@ appdirs==1.4.4 packaging==20.4 pyparsing==2.4.7 setuptools==46.4.0 -six==1.14.0 +six==1.15.0 wheel==0.34.2 From 3acadbffc11d25f1bc11af7805be7a070e37bd01 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 25 May 2020 21:11:20 +0200 Subject: [PATCH 059/245] Update six from 1.14.0 to 1.15.0 --- misc/requirements/requirements-pylint.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index 1f5552a97..e80ddbf50 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -16,7 +16,7 @@ pylint==2.4.4 # rq.filter: < 2.5 python-dateutil==2.8.1 ./scripts/dev/pylint_checkers requests==2.23.0 -six==1.14.0 +six==1.15.0 typed-ast==1.4.1 ; python_version<"3.8" uritemplate==3.0.1 urllib3==1.25.9 From 2191a258bd6bd7ff1af893e4fe11ecb503101530 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 25 May 2020 21:11:21 +0200 Subject: [PATCH 060/245] Update six from 1.14.0 to 1.15.0 --- misc/requirements/requirements-sphinx.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt index 8fb334a72..e4ca26a6a 100644 --- a/misc/requirements/requirements-sphinx.txt +++ b/misc/requirements/requirements-sphinx.txt @@ -14,7 +14,7 @@ Pygments==2.6.1 pyparsing==2.4.7 pytz==2020.1 requests==2.23.0 -six==1.14.0 +six==1.15.0 snowballstemmer==2.0.0 Sphinx==3.0.3 sphinxcontrib-applehelp==1.0.2 From 3ecfc815c8563d0464aaacc459bc25b67ddc7d13 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 25 May 2020 21:11:22 +0200 Subject: [PATCH 061/245] Update six from 1.14.0 to 1.15.0 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 87c4b2881..22663c1a8 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -38,7 +38,7 @@ pytest-rerunfailures==9.0 pytest-travis-fold==1.3.0 pytest-xvfb==1.2.0 PyVirtualDisplay==0.2.5 -six==1.14.0 +six==1.15.0 sortedcontainers==2.1.0 soupsieve==2.0.1 vulture==1.4 From 3f4495651b17430b8bba85098d07861d4eb174b2 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 25 May 2020 21:11:23 +0200 Subject: [PATCH 062/245] Update six from 1.14.0 to 1.15.0 --- misc/requirements/requirements-tox.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt index 78da631cb..a19a0b794 100644 --- a/misc/requirements/requirements-tox.txt +++ b/misc/requirements/requirements-tox.txt @@ -7,7 +7,7 @@ packaging==20.4 pluggy==0.13.1 py==1.8.1 pyparsing==2.4.7 -six==1.14.0 +six==1.15.0 toml==0.10.1 tox==3.15.0 tox-pip-version==0.0.7 From 4008b53ada5c17f619add27c404ac5acb2e309c2 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 25 May 2020 21:11:24 +0200 Subject: [PATCH 063/245] Update flake8 from 3.8.1 to 3.8.2 --- misc/requirements/requirements-flake8.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt index e636b3abc..d0292a4eb 100644 --- a/misc/requirements/requirements-flake8.txt +++ b/misc/requirements/requirements-flake8.txt @@ -1,7 +1,7 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py attrs==19.3.0 -flake8==3.8.1 +flake8==3.8.2 flake8-bugbear==20.1.4 flake8-builtins==1.5.3 flake8-comprehensions==3.2.2 From bc8ba23710a7f6daf63dd1c78103187b836c9556 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 25 May 2020 21:11:25 +0200 Subject: [PATCH 064/245] Update hypothesis from 5.14.0 to 5.15.1 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 22663c1a8..b9db85998 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -10,7 +10,7 @@ EasyProcess==0.3 Flask==1.1.2 glob2==0.7 hunter==3.1.3 -hypothesis==5.14.0 +hypothesis==5.15.1 itsdangerous==1.1.0 jaraco.functools==3.0.1 ; python_version>="3.6" # Jinja2==2.11.2 From fe1559dc39bda31466502d87cfe9b6c5830e0b61 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 25 May 2020 21:11:26 +0200 Subject: [PATCH 065/245] Update pytest-cov from 2.8.1 to 2.9.0 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index b9db85998..1c0c9acde 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -29,7 +29,7 @@ pyparsing==2.4.7 pytest==5.4.2 pytest-bdd==3.3.0 pytest-benchmark==3.2.3 -pytest-cov==2.8.1 +pytest-cov==2.9.0 pytest-instafail==0.4.1.post0 pytest-mock==3.1.0 pytest-qt==3.3.0 From fbad4a3a822c55eb8971e21f871db9607855b6a8 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 25 May 2020 21:11:27 +0200 Subject: [PATCH 066/245] Update tox from 3.15.0 to 3.15.1 --- misc/requirements/requirements-tox.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt index a19a0b794..4c6795e53 100644 --- a/misc/requirements/requirements-tox.txt +++ b/misc/requirements/requirements-tox.txt @@ -9,7 +9,7 @@ py==1.8.1 pyparsing==2.4.7 six==1.15.0 toml==0.10.1 -tox==3.15.0 +tox==3.15.1 tox-pip-version==0.0.7 tox-venv==0.4.0 virtualenv==20.0.20 From a91f68b6ef92a0025a395283204fdac39db7b639 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 25 May 2020 21:11:28 +0200 Subject: [PATCH 067/245] Update virtualenv from 20.0.20 to 20.0.21 --- misc/requirements/requirements-tox.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt index 4c6795e53..3b4c93bb4 100644 --- a/misc/requirements/requirements-tox.txt +++ b/misc/requirements/requirements-tox.txt @@ -12,4 +12,4 @@ toml==0.10.1 tox==3.15.1 tox-pip-version==0.0.7 tox-venv==0.4.0 -virtualenv==20.0.20 +virtualenv==20.0.21 From 0e7b3c91a0470e49decebb10c3a361e379c46f26 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Tue, 26 May 2020 07:42:34 -0400 Subject: [PATCH 068/245] Ensure log configs consistent with defaults. Previously the RAM handler defaulted to logging.NOTSET, which will allow all loglevels to pass through (since the root logger is also NOTSET). There was a separate check that omitted VDEBUG logs. Let's explicitly set the RAMHandler to our desired loglevel instead. --- qutebrowser/utils/log.py | 6 ++---- tests/unit/utils/test_log.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 635fccabb..683ee92f4 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -296,7 +296,7 @@ def _init_handlers( ram_handler = None else: ram_handler = RAMHandler(capacity=ram_capacity) - ram_handler.setLevel(logging.NOTSET) + ram_handler.setLevel(logging.DEBUG) ram_handler.setFormatter(ram_fmt) ram_handler.html_formatter = html_fmt @@ -629,9 +629,7 @@ class RAMHandler(logging.Handler): self._data = collections.deque() def emit(self, record: logging.LogRecord) -> None: - if record.levelno >= logging.DEBUG: - # We don't log VDEBUG to RAM. - self._data.append(record) + self._data.append(record) def dump_log(self, html: bool = False, level: str = 'vdebug') -> str: """Dump the complete formatted log data as string. diff --git a/tests/unit/utils/test_log.py b/tests/unit/utils/test_log.py index eb634d76a..2ced94c42 100644 --- a/tests/unit/utils/test_log.py +++ b/tests/unit/utils/test_log.py @@ -30,6 +30,7 @@ import pytest import _pytest.logging from PyQt5 import QtCore +from qutebrowser import qutebrowser from qutebrowser.utils import log from qutebrowser.misc import utilcmds @@ -286,6 +287,19 @@ class TestInitLog: log.init_from_config(config_stub.val) assert log.ram_handler.level == expected + def test_init_from_config_consistent_default(self, config_stub): + """Ensure config defaults are consistent with the builtin defaults""" + args = qutebrowser.get_argparser().parse_args([]) + log.init_log(args) + + assert log.ram_handler.level == logging.DEBUG + assert log.console_handler.level == logging.INFO + + log.init_from_config(config_stub.val) + + assert log.ram_handler.level == logging.DEBUG + assert log.console_handler.level == logging.INFO + class TestHideQtWarning: From 98965f5c083a8b2859d721b7ea0e8e7170e7c847 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 26 May 2020 14:14:02 +0200 Subject: [PATCH 069/245] tests: Improve IDs in test_configtypes --- tests/unit/config/test_configtypes.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index e49075500..abd79820c 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -216,13 +216,21 @@ class TestAll: pass elif (member is configtypes.List or member is configtypes.ListOrValue): - yield functools.partial(member, valtype=configtypes.Int()) - yield functools.partial(member, valtype=configtypes.Url()) + yield pytest.param( + functools.partial(member, valtype=configtypes.Int()), + id=member.__name__ + '-Int') + yield pytest.param( + functools.partial(member, valtype=configtypes.Url()), + id=member.__name__ + '-Url') elif member is configtypes.Dict: - yield functools.partial(member, keytype=configtypes.String(), - valtype=configtypes.String()) + yield pytest.param( + functools.partial(member, keytype=configtypes.String(), + valtype=configtypes.String()), + id=member.__name__) elif member is configtypes.FormatString: - yield functools.partial(member, fields=['a', 'b']) + yield pytest.param( + functools.partial(member, fields=['a', 'b']), + id=member.__name__) elif issubclass(member, configtypes.BaseType): yield member From 84fcdd51c0ac9baa93e4481999cc93864beb7e60 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 26 May 2020 14:23:01 +0200 Subject: [PATCH 070/245] tests: Return early in hypothesis test for List configtype See https://github.com/qutebrowser/qutebrowser/issues/5390#issuecomment-622881094 --- tests/unit/config/test_configtypes.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index abd79820c..18019fb4f 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -271,8 +271,10 @@ class TestAll: ]: return elif (isinstance(typ, functools.partial) and - isinstance(typ.func, configtypes.ListOrValue)): - # "- /" -> "/" + isinstance(typ.func, (configtypes.ListOrValue, + configtypes.List))): + # ListOrValue: "- /" -> "/" + # List: "- /" -> ["/"] return elif (isinstance(typ, configtypes.ListOrValue) and isinstance(typ.valtype, configtypes.Int)): From aa0d0ccb5db7f5278b8dbeb2115a6e9bc4fb9b25 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 26 May 2020 14:29:59 +0200 Subject: [PATCH 071/245] tests: Try increasing file_updated timeout See https://github.com/qutebrowser/qutebrowser/issues/5390#issuecomment-631066927 --- tests/unit/misc/test_editor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/misc/test_editor.py b/tests/unit/misc/test_editor.py index edd32b5a5..5fb4478a7 100644 --- a/tests/unit/misc/test_editor.py +++ b/tests/unit/misc/test_editor.py @@ -167,7 +167,7 @@ class TestFileHandling: def test_backup(self, qtbot, message_mock): editor = editormod.ExternalEditor(watch=True) editor.edit('foo') - with qtbot.wait_signal(editor.file_updated): + with qtbot.wait_signal(editor.file_updated, timeout=5000): _update_file(editor._filename, 'bar') editor.backup() From 286c91c01e94da742b45fb6cffeb0ae5825519fe Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 26 May 2020 15:27:15 +0200 Subject: [PATCH 072/245] Log exception traceback for init errors Also improve error handling in test_err_windows --- qutebrowser/utils/error.py | 1 + tests/unit/utils/test_error.py | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/qutebrowser/utils/error.py b/qutebrowser/utils/error.py index cfc3c3f5a..4cba06a10 100644 --- a/qutebrowser/utils/error.py +++ b/qutebrowser/utils/error.py @@ -63,6 +63,7 @@ def handle_fatal_exc(exc: BaseException, ] log.misc.exception('\n'.join(lines)) else: + log.misc.exception("Fatal exception:") if pre_text: msg_text = '{}: {}'.format(pre_text, exc) else: diff --git a/tests/unit/utils/test_error.py b/tests/unit/utils/test_error.py index 82ca3ec55..a1a7ea6de 100644 --- a/tests/unit/utils/test_error.py +++ b/tests/unit/utils/test_error.py @@ -73,10 +73,11 @@ def test_no_err_windows(caplog, exc, name, exc_text): ('foo', 'bar', 'foo: exception\n\nbar'), ('', 'bar', 'exception\n\nbar'), ], ids=repr) -def test_err_windows(qtbot, qapp, pre_text, post_text, expected): +def test_err_windows(qtbot, qapp, pre_text, post_text, expected, caplog): def err_window_check(): w = qapp.activeModalWidget() + assert w is not None try: qtbot.add_widget(w) if not utils.is_mac: @@ -87,7 +88,9 @@ def test_err_windows(qtbot, qapp, pre_text, post_text, expected): finally: w.close() - QTimer.singleShot(0, err_window_check) - error.handle_fatal_exc(ValueError("exception"), 'title', - pre_text=pre_text, post_text=post_text, - no_err_windows=False) + QTimer.singleShot(10, err_window_check) + + with caplog.at_level(logging.ERROR): + error.handle_fatal_exc(ValueError("exception"), 'title', + pre_text=pre_text, post_text=post_text, + no_err_windows=False) From e5c2c0fb799c4a2837aeedaabd366e0c7c299575 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 26 May 2020 16:31:00 +0200 Subject: [PATCH 073/245] Fix indent --- tests/unit/utils/test_error.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/utils/test_error.py b/tests/unit/utils/test_error.py index a1a7ea6de..f8847d39a 100644 --- a/tests/unit/utils/test_error.py +++ b/tests/unit/utils/test_error.py @@ -92,5 +92,5 @@ def test_err_windows(qtbot, qapp, pre_text, post_text, expected, caplog): with caplog.at_level(logging.ERROR): error.handle_fatal_exc(ValueError("exception"), 'title', - pre_text=pre_text, post_text=post_text, - no_err_windows=False) + pre_text=pre_text, post_text=post_text, + no_err_windows=False) From 093a454bf4e386eda8715bc3ea2b217760d5f070 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 26 May 2020 16:48:00 +0200 Subject: [PATCH 074/245] Update Qt 5.15 session workaround for lazy_restore See #5359 --- doc/changelog.asciidoc | 3 +++ qutebrowser/browser/webengine/webenginetab.py | 6 +++++- qutebrowser/html/warning-sessions.html | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 741558ec1..d6e42e2b6 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -66,6 +66,9 @@ Fixed `QWebEngineFindTextResult` handling added in v1.11.0. - When entering caret selection mode (`v, v`) very early before a page is loaded, an error is now shown instead of a crash happening. +- The workaround for session loading with Qt 5.15 now handles + `sessions.lazy_restore` so that the saved page is loaded instead of the + "stub" page with no possibility to get to the web page. v1.11.1 (2020-05-07) -------------------- diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 709c5c1b5..5fd4a9e11 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -685,7 +685,11 @@ class WebEngineHistoryPrivate(browsertab.AbstractHistoryPrivate): if qtutils.version_check('5.15', compiled=False): # WORKAROUND for https://github.com/qutebrowser/qutebrowser/issues/5359 if items: - self._tab.load_url(items[-1].url) + url = items[-1].url + if ((url.scheme(), url.host()) == ('qute', 'back') and + len(items) >= 2): + url = items[-2].url + self._tab.load_url(url) return if items: diff --git a/qutebrowser/html/warning-sessions.html b/qutebrowser/html/warning-sessions.html index f93971c6a..dd0c4127b 100644 --- a/qutebrowser/html/warning-sessions.html +++ b/qutebrowser/html/warning-sessions.html @@ -15,6 +15,7 @@ qute://warning/sessions to show it again at a later time.
  • Loading a session with this release will only load the most recently opened page for every tab. As a result, the back/forward-history of every tab will be lost as soon as the session is saved again.
  • +
  • Due to that, the session.lazy_restore setting does not have any effect.
  • A one-time backup of the session folder has been created at {{ datadir }}{{ sep }}sessions{{ sep }}before-qt-515.
From 0d191a47bb34959102ddc2e1ecc07896dab3eda6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 26 May 2020 16:55:02 +0200 Subject: [PATCH 075/245] Update completions for content.headers.user_agent --- qutebrowser/config/configdata.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 3700e5e1d..46b745089 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -562,10 +562,10 @@ content.headers.user_agent: completions: # See https://techblog.willshouse.com/2012/01/03/most-common-user-agents/ - - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, - like Gecko) Chrome/80.0.3987.163 Safari/537.36" + like Gecko) Chrome/81.0.4044.129 Safari/537.36" - Chrome 80 Win10 - - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like - Gecko) Chrome/80.0.3987.149 Safari/537.36 " + Gecko) Chrome/81.0.4044.138 Safari/537.36" - Chrome 80 Linux supports_pattern: true desc: | From e8b0ce7597627a5b5312a214a84ff2aed9bcdfe0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 26 May 2020 20:20:56 +0200 Subject: [PATCH 076/245] Add filename if QtOSError is used with QFileDevice If we have a filename available, let's add it to the error message. This also effectively reverts 00747be9d3790534e8b32464605d1b5b6c2d6627 since that's not needed anymore (Qt 5.7 is the oldest supported release). --- qutebrowser/utils/qtutils.py | 19 ++++++++++--------- tests/unit/utils/test_qtutils.py | 6 +++--- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 3e8afae3f..109d2dfed 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -35,7 +35,7 @@ import typing import pkg_resources from PyQt5.QtCore import (qVersion, QEventLoop, QDataStream, QByteArray, - QIODevice, QSaveFile, QT_VERSION_STR, + QIODevice, QFileDevice, QSaveFile, QT_VERSION_STR, PYQT_VERSION_STR, QObject, QUrl) from PyQt5.QtGui import QColor from PyQt5.QtWidgets import QApplication @@ -44,9 +44,6 @@ try: except ImportError: # pragma: no cover qWebKitVersion = None # type: ignore[assignment] # noqa: N816 -if typing.TYPE_CHECKING: - from PyQt5.QtCore import QFileDevice - from qutebrowser.misc import objects from qutebrowser.utils import usertypes @@ -74,13 +71,17 @@ class QtOSError(OSError): if msg is None: msg = dev.errorString() + self.qt_errno = None # type: typing.Optional[QFileDevice.FileError] + if isinstance(dev, QFileDevice): + msg = self._init_filedev(dev, msg) + super().__init__(msg) - self.qt_errno = None # type: typing.Optional[QFileDevice.FileError] - try: - self.qt_errno = dev.error() - except AttributeError: - pass + def _init_filedev(self, dev: QFileDevice, msg: str) -> str: + self.qt_errno = dev.error() + filename = dev.fileName() + msg += ": {!r}".format(filename) + return msg def version_check(version: str, diff --git a/tests/unit/utils/test_qtutils.py b/tests/unit/utils/test_qtutils.py index 150a03f6e..81d198946 100644 --- a/tests/unit/utils/test_qtutils.py +++ b/tests/unit/utils/test_qtutils.py @@ -467,9 +467,9 @@ class TestSavefileOpen: with pytest.raises(OSError) as excinfo: with qtutils.savefile_open(str(filename)): pass - errors = ["Filename refers to a directory", # Qt >= 5.4 - "Commit failed!"] # older Qt versions - assert str(excinfo.value) in errors + + msg = "Filename refers to a directory: {!r}".format(str(filename)) + assert str(excinfo.value) == msg assert tmpdir.listdir() == [filename] def test_failing_flush(self, tmpdir): From 64ffce27f9348e14836528c0313d3048669a29c7 Mon Sep 17 00:00:00 2001 From: Julin S <48789920+ju-sh@users.noreply.github.com> Date: Wed, 27 May 2020 11:12:45 +0530 Subject: [PATCH 077/245] fix error in asciidoc2html.py script --- scripts/asciidoc2html.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/asciidoc2html.py b/scripts/asciidoc2html.py index 90b94f014..2f64eac52 100755 --- a/scripts/asciidoc2html.py +++ b/scripts/asciidoc2html.py @@ -121,7 +121,7 @@ class AsciiDoc: src = root / filename assert self._website is not None # for mypy dst = pathlib.Path(self._website) - dst = src.parent.relative_to('.') / (src.stem + ".html") + dst = dst / src.parent.relative_to('.') / (src.stem + ".html") dst.parent.mkdir(exist_ok=True) assert self._tempdir is not None # for mypy From 8ab4133bfc5bae1656f74c87a56d2d5f45a1c788 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 27 May 2020 12:43:40 +0200 Subject: [PATCH 078/245] configtypes: Fix handling of Unset in _Numeric with bounds --- qutebrowser/config/configtypes.py | 18 +++++++++++++----- tests/unit/config/test_configtypes.py | 4 ++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 6eec13293..cfcc5f13a 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -797,11 +797,16 @@ class _Numeric(BaseType): # pylint: disable=abstract-method assert isinstance(bound, (int, float)), bound return bound - def _validate_bounds(self, value: typing.Union[None, int, float], - suffix: str = '') -> None: + def _validate_bounds( + self, + value: typing.Union[None, int, float, usertypes.Unset], + suffix: str = '' + ) -> None: """Validate self.minval and self.maxval.""" if value is None: return + elif isinstance(value, usertypes.Unset): + return elif self.minval is not None and value < self.minval: raise configexc.ValidationError( value, "must be {}{} or bigger!".format(self.minval, suffix)) @@ -837,7 +842,10 @@ class Int(_Numeric): self.to_py(intval) return intval - def to_py(self, value: typing.Optional[int]) -> typing.Optional[int]: + def to_py( + self, + value: typing.Union[None, int, usertypes.Unset] + ) -> typing.Union[None, int, usertypes.Unset]: self._basic_py_validation(value, int) self._validate_bounds(value) return value @@ -861,8 +869,8 @@ class Float(_Numeric): def to_py( self, - value: typing.Union[None, int, float], - ) -> typing.Union[None, int, float]: + value: typing.Union[None, int, float, usertypes.Unset], + ) -> typing.Union[None, int, float, usertypes.Unset]: self._basic_py_validation(value, (int, float)) self._validate_bounds(value) return value diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index 18019fb4f..841892ef2 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -1047,6 +1047,10 @@ class TestInt: converted = typ.from_str(text) assert typ.to_str(converted) == text + def test_bounds_handling_unset(self, klass): + typ = klass(minval=1, maxval=2) + assert typ.to_py(usertypes.UNSET) is usertypes.UNSET + class TestFloat: From 70625c3f8718124137e087b52db66983cf4df0ef Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 27 May 2020 12:49:04 +0200 Subject: [PATCH 079/245] configtypes: Add alias for "Union[None, usertypes.Unset]" --- qutebrowser/config/configtypes.py | 52 +++++++++++++------------------ 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index cfcc5f13a..61284c34e 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -82,7 +82,8 @@ BOOLEAN_STATES = {'1': True, 'yes': True, 'true': True, 'on': True, _Completions = typing.Optional[typing.Iterable[typing.Tuple[str, str]]] _StrUnset = typing.Union[str, usertypes.Unset] -_StrUnsetNone = typing.Union[None, str, usertypes.Unset] +_UnsetNone = typing.Union[None, usertypes.Unset] +_StrUnsetNone = typing.Union[str, _UnsetNone] class ValidValues: @@ -797,11 +798,9 @@ class _Numeric(BaseType): # pylint: disable=abstract-method assert isinstance(bound, (int, float)), bound return bound - def _validate_bounds( - self, - value: typing.Union[None, int, float, usertypes.Unset], - suffix: str = '' - ) -> None: + def _validate_bounds(self, + value: typing.Union[int, float, _UnsetNone], + suffix: str = '') -> None: """Validate self.minval and self.maxval.""" if value is None: return @@ -844,8 +843,8 @@ class Int(_Numeric): def to_py( self, - value: typing.Union[None, int, usertypes.Unset] - ) -> typing.Union[None, int, usertypes.Unset]: + value: typing.Union[int, _UnsetNone] + ) -> typing.Union[int, _UnsetNone]: self._basic_py_validation(value, int) self._validate_bounds(value) return value @@ -869,8 +868,8 @@ class Float(_Numeric): def to_py( self, - value: typing.Union[None, int, float, usertypes.Unset], - ) -> typing.Union[None, int, float, usertypes.Unset]: + value: typing.Union[int, float, _UnsetNone], + ) -> typing.Union[int, float, _UnsetNone]: self._basic_py_validation(value, (int, float)) self._validate_bounds(value) return value @@ -882,8 +881,8 @@ class Perc(_Numeric): def to_py( self, - value: typing.Union[None, float, int, str, usertypes.Unset] - ) -> typing.Union[None, float, int, usertypes.Unset]: + value: typing.Union[float, int, str, _UnsetNone] + ) -> typing.Union[float, int, _UnsetNone]: self._basic_py_validation(value, (float, int, str)) if isinstance(value, usertypes.Unset): return value @@ -1078,8 +1077,7 @@ class QtColor(BaseType): except ValueError: raise configexc.ValidationError(val, "must be a valid color value") - def to_py(self, value: _StrUnset) -> typing.Union[usertypes.Unset, - None, QColor]: + def to_py(self, value: _StrUnset) -> typing.Union[_UnsetNone, QColor]: self._basic_py_validation(value, str) if isinstance(value, usertypes.Unset): return value @@ -1362,8 +1360,7 @@ class QtFont(FontBase): else: # pragma: no cover font.setFamily(families.to_str(quote=False)) - def to_py(self, value: _StrUnset) -> typing.Union[usertypes.Unset, - None, QFont]: + def to_py(self, value: _StrUnset) -> typing.Union[_UnsetNone, QFont]: self._basic_py_validation(value, str) if isinstance(value, usertypes.Unset): return value @@ -1442,7 +1439,7 @@ class Regex(BaseType): def to_py( self, value: typing.Union[str, typing.Pattern[str], usertypes.Unset] - ) -> typing.Union[usertypes.Unset, None, typing.Pattern[str]]: + ) -> typing.Union[_UnsetNone, typing.Pattern[str]]: """Get a compiled regex from either a string or a regex object.""" self._basic_py_validation(value, (str, self._regex_type)) if isinstance(value, usertypes.Unset): @@ -1533,7 +1530,7 @@ class Dict(BaseType): def to_py( self, - value: typing.Union[typing.Dict, usertypes.Unset, None] + value: typing.Union[typing.Dict, _UnsetNone] ) -> typing.Union[typing.Dict, usertypes.Unset]: self._basic_py_validation(value, dict) if isinstance(value, usertypes.Unset): @@ -1732,8 +1729,7 @@ class Proxy(BaseType): def to_py( self, value: _StrUnset - ) -> typing.Union[usertypes.Unset, None, - QNetworkProxy, _SystemProxy, pac.PACFetcher]: + ) -> typing.Union[_UnsetNone, QNetworkProxy, _SystemProxy, pac.PACFetcher]: self._basic_py_validation(value, str) if isinstance(value, usertypes.Unset): return value @@ -1803,10 +1799,7 @@ class FuzzyUrl(BaseType): """A URL which gets interpreted as search if needed.""" - def to_py( - self, - value: _StrUnset - ) -> typing.Union[None, QUrl, usertypes.Unset]: + def to_py(self, value: _StrUnset) -> typing.Union[QUrl, _UnsetNone]: self._basic_py_validation(value, str) if isinstance(value, usertypes.Unset): return value @@ -1844,7 +1837,7 @@ class Padding(Dict): def to_py( # type: ignore[override] self, - value: typing.Union[usertypes.Unset, typing.Dict, None], + value: typing.Union[typing.Dict, _UnsetNone], ) -> typing.Union[usertypes.Unset, PaddingValues]: d = super().to_py(value) if isinstance(d, usertypes.Unset): @@ -1916,10 +1909,7 @@ class Url(BaseType): """A URL as a string.""" - def to_py( - self, - value: _StrUnset - ) -> typing.Union[usertypes.Unset, None, QUrl]: + def to_py(self, value: _StrUnset) -> typing.Union[_UnsetNone, QUrl]: self._basic_py_validation(value, str) if isinstance(value, usertypes.Unset): return value @@ -2033,7 +2023,7 @@ class Key(BaseType): def to_py( self, value: _StrUnset - ) -> typing.Union[usertypes.Unset, None, keyutils.KeySequence]: + ) -> typing.Union[_UnsetNone, keyutils.KeySequence]: self._basic_py_validation(value, str) if isinstance(value, usertypes.Unset): return value @@ -2057,7 +2047,7 @@ class UrlPattern(BaseType): def to_py( self, value: _StrUnset - ) -> typing.Union[usertypes.Unset, None, urlmatch.UrlPattern]: + ) -> typing.Union[_UnsetNone, urlmatch.UrlPattern]: self._basic_py_validation(value, str) if isinstance(value, usertypes.Unset): return value From 85aee23639bb78c07151618af031317c9ecd3408 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 27 May 2020 15:02:44 +0200 Subject: [PATCH 080/245] Add dark mode settings Closes #5394 See #2377 --- doc/changelog.asciidoc | 3 + doc/help/settings.asciidoc | 163 +++++++++++++++++++++++++++ qutebrowser/config/configdata.yml | 163 +++++++++++++++++++++++++++ qutebrowser/config/configinit.py | 89 +++++++++++++++ scripts/dev/misc_checks.py | 2 +- tests/unit/config/test_configinit.py | 116 ++++++++++++++++++- 6 files changed, 534 insertions(+), 2 deletions(-) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index d6e42e2b6..a6aead789 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -41,6 +41,9 @@ Added - New `colors.contextmenu.disabled.{fg,bg}` settings to customize colors for disabled items in the context menu. - New line selection mode (`:toggle-selection --line`), bound to `Shift-V` in caret mode. +- New `colors.webpage.darkmode.*` settings to control Chromium's dark mode. + Note that those settings only work with QtWebEngine on Qt >= 5.14 and require + a restart of qutebrowser. Changed ~~~~~~~ diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 4c20126c3..3787238a9 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -111,6 +111,15 @@ |<>|Background color of selected odd tabs. |<>|Foreground color of selected odd tabs. |<>|Background color for webpages if unset (or empty to use the theme's color). +|<>|Which algorithm to use for modifying how colors are rendered with darkmode. +|<>|Contrast for dark mode. +|<>|Render all web contents using a dark theme. +|<>|Render all colors as grayscale. +|<>|Desaturation factor for images in dark mode. +|<>|Which images to apply dark mode to. +|<>|Which pages to apply dark mode to. +|<>|Threshold for inverting background elements with dark mode. +|<>|Threshold for inverting text with dark mode. |<>|Force `prefers-color-scheme: dark` colors for websites. |<>|Number of commands to save in the command history. |<>|Delay (in milliseconds) before updating completions after typing a character. @@ -1534,6 +1543,160 @@ Type: <> Default: +pass:[white]+ +[[colors.webpage.darkmode.algorithm]] +=== colors.webpage.darkmode.algorithm +Which algorithm to use for modifying how colors are rendered with darkmode. +This setting requires a restart. + +Type: <> + +Valid values: + + * +lightness-cielab+: Modify colors by converting them to CIELAB color space and inverting the L value. + * +lightness-hsl+: Modify colors by converting them to the HSL color space and inverting the lightness (i.e. the "L" in HSL). + * +brightness-rgb+: Modify colors by subtracting each of r, g, and b from their maximum value. + +Default: +pass:[lightness-cielab]+ + +On QtWebEngine, this setting requires Qt 5.14 or newer. + +On QtWebKit, this setting is unavailable. + +[[colors.webpage.darkmode.contrast]] +=== colors.webpage.darkmode.contrast +Contrast for dark mode. +This only has an effect when `colors.webpage.darkmode.algorithm` is set to `lightness-hsl` or `brightness-rgb`. +This setting requires a restart. + +Type: <> + +Default: +pass:[0.0]+ + +On QtWebEngine, this setting requires Qt 5.14 or newer. + +On QtWebKit, this setting is unavailable. + +[[colors.webpage.darkmode.enabled]] +=== colors.webpage.darkmode.enabled +Render all web contents using a dark theme. +Example configurations from Chromium's `chrome://flags`: + +- "With simple HSL/CIELAB/RGB-based inversion": Set + `colors.webpage.darkmode.algorithm` accordingly. + +- "With selective image inversion": Set + `colors.webpage.darkmode.policy.images` to `smart`. + +- "With selective inversion of non-image elements": Set + `colors.webpage.darkmode.threshold.text` to 150 and + `colors.webpage.darkmode.threshold.background` to 205. + +- "With selective inversion of everything": Combines the two variants + above. +This setting requires a restart. + +Type: <> + +Default: +pass:[false]+ + +On QtWebEngine, this setting requires Qt 5.14 or newer. + +On QtWebKit, this setting is unavailable. + +[[colors.webpage.darkmode.grayscale.all]] +=== colors.webpage.darkmode.grayscale.all +Render all colors as grayscale. +This only has an effect when `colors.webpage.darkmode.algorithm` is set to `lightness-hsl` or `brightness-rgb`. +This setting requires a restart. + +Type: <> + +Default: +pass:[false]+ + +On QtWebEngine, this setting requires Qt 5.14 or newer. + +On QtWebKit, this setting is unavailable. + +[[colors.webpage.darkmode.grayscale.images]] +=== colors.webpage.darkmode.grayscale.images +Desaturation factor for images in dark mode. +If set to 0, images are left as-is. If set to 1, images are completely grayscale. Values between 0 and 1 desaturate the colors accordingly. +This setting requires a restart. + +Type: <> + +Default: +pass:[0.0]+ + +On QtWebEngine, this setting requires Qt 5.14 or newer. + +On QtWebKit, this setting is unavailable. + +[[colors.webpage.darkmode.policy.images]] +=== colors.webpage.darkmode.policy.images +Which images to apply dark mode to. +This setting requires a restart. + +Type: <> + +Valid values: + + * +always+: Apply dark mode filter to all images. + * +never+: Never apply dark mode filter to any images. + * +smart+: Apply dark mode based on image content. + +Default: +pass:[never]+ + +On QtWebEngine, this setting requires Qt 5.14 or newer. + +On QtWebKit, this setting is unavailable. + +[[colors.webpage.darkmode.policy.page]] +=== colors.webpage.darkmode.policy.page +Which pages to apply dark mode to. +This setting requires a restart. + +Type: <> + +Valid values: + + * +always+: Apply dark mode filter to all frames, regardless of content. + * +smart+: Apply dark mode filter to frames based on background color. + +Default: +pass:[smart]+ + +On QtWebEngine, this setting requires Qt 5.14 or newer. + +On QtWebKit, this setting is unavailable. + +[[colors.webpage.darkmode.threshold.background]] +=== colors.webpage.darkmode.threshold.background +Threshold for inverting background elements with dark mode. +Background elements with brightness above this threshold will be inverted, and below it will be left as in the original, non-dark-mode page. Set to 256 to never invert the color or to 0 to always invert it. +Note: This behavior is the opposite of `colors.webpage.darkmode.threshold.text`! +This setting requires a restart. + +Type: <> + +Default: +pass:[0]+ + +On QtWebEngine, this setting requires Qt 5.14 or newer. + +On QtWebKit, this setting is unavailable. + +[[colors.webpage.darkmode.threshold.text]] +=== colors.webpage.darkmode.threshold.text +Threshold for inverting text with dark mode. +Text colors with brightness below this threshold will be inverted, and above it will be left as in the original, non-dark-mode page. Set to 256 to always invert text color or to 0 to never invert text color. +This setting requires a restart. + +Type: <> + +Default: +pass:[256]+ + +On QtWebEngine, this setting requires Qt 5.14 or newer. + +On QtWebKit, this setting is unavailable. + [[colors.webpage.prefers_color_scheme_dark]] === colors.webpage.prefers_color_scheme_dark Force `prefers-color-scheme: dark` colors for websites. diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 46b745089..dc49e4cdd 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -2612,6 +2612,169 @@ colors.webpage.prefers_color_scheme_dark: QtWebEngine: Qt 5.14 QtWebKit: false +## dark mode + +# darkModeClassifierType is not exposed, as the icon classifier isn't actually +# implemented in Chromium: +# +# https://source.chromium.org/chromium/chromium/src/+/master:third_party/blink/renderer/platform/graphics/dark_mode_icon_classifier.cc + +colors.webpage.darkmode.enabled: + default: false + type: Bool + desc: >- + Render all web contents using a dark theme. + + Example configurations from Chromium's `chrome://flags`: + + + - "With simple HSL/CIELAB/RGB-based inversion": Set + `colors.webpage.darkmode.algorithm` accordingly. + + - "With selective image inversion": Set + `colors.webpage.darkmode.policy.images` to `smart`. + + - "With selective inversion of non-image elements": Set + `colors.webpage.darkmode.threshold.text` to 150 and + `colors.webpage.darkmode.threshold.background` to 205. + + - "With selective inversion of everything": Combines the two variants + above. + restart: true + backend: + QtWebEngine: Qt 5.14 + QtWebKit: false + +colors.webpage.darkmode.algorithm: + default: lightness-cielab + desc: "Which algorithm to use for modifying how colors are rendered with + darkmode." + type: + name: String + valid_values: + - lightness-cielab: Modify colors by converting them to CIELAB color + space and inverting the L value. + - lightness-hsl: Modify colors by converting them to the HSL color space + and inverting the lightness (i.e. the "L" in HSL). + - brightness-rgb: Modify colors by subtracting each of r, g, and b from + their maximum value. + # kSimpleInvertForTesting is not exposed, as it's equivalent to + # kInvertBrightness without gamma correction, and only available for + # Chromium's automated tests + restart: true + backend: + QtWebEngine: Qt 5.14 + QtWebKit: false + +colors.webpage.darkmode.contrast: + default: 0.0 + type: + name: Float + minval: -1.0 + maxval: 1.0 + desc: >- + Contrast for dark mode. + + This only has an effect when `colors.webpage.darkmode.algorithm` is set to + `lightness-hsl` or `brightness-rgb`. + restart: true + backend: + QtWebEngine: Qt 5.14 + QtWebKit: false + +colors.webpage.darkmode.policy.images: + default: never + type: + name: String + valid_values: + - always: Apply dark mode filter to all images. + - never: Never apply dark mode filter to any images. + - smart: Apply dark mode based on image content. + desc: Which images to apply dark mode to. + restart: true + backend: + QtWebEngine: Qt 5.14 + QtWebKit: false + +colors.webpage.darkmode.policy.page: + default: smart + type: + name: String + valid_values: + - always: Apply dark mode filter to all frames, regardless of content. + - smart: Apply dark mode filter to frames based on background color. + desc: Which pages to apply dark mode to. + restart: true + backend: + QtWebEngine: Qt 5.14 + QtWebKit: false + +colors.webpage.darkmode.threshold.text: + default: 256 + type: + name: Int + minval: 0 + maxval: 256 + desc: >- + Threshold for inverting text with dark mode. + + Text colors with brightness below this threshold will be inverted, and + above it will be left as in the original, non-dark-mode page. Set to 256 + to always invert text color or to 0 to never invert text color. + restart: true + backend: + QtWebEngine: Qt 5.14 + QtWebKit: false + +colors.webpage.darkmode.threshold.background: + default: 0 + type: + name: Int + minval: 0 + maxval: 256 + desc: >- + Threshold for inverting background elements with dark mode. + + Background elements with brightness above this threshold will be inverted, + and below it will be left as in the original, non-dark-mode page. Set to + 256 to never invert the color or to 0 to always invert it. + + Note: This behavior is the opposite of + `colors.webpage.darkmode.threshold.text`! + restart: true + backend: + QtWebEngine: Qt 5.14 + QtWebKit: false + +colors.webpage.darkmode.grayscale.all: + default: false + type: Bool + desc: >- + Render all colors as grayscale. + + This only has an effect when `colors.webpage.darkmode.algorithm` is set to + `lightness-hsl` or `brightness-rgb`. + restart: true + backend: + QtWebEngine: Qt 5.14 + QtWebKit: false + +colors.webpage.darkmode.grayscale.images: + default: 0.0 + type: + name: Float + minval: 0.0 + maxval: 1.0 + desc: >- + Desaturation factor for images in dark mode. + + If set to 0, images are left as-is. If set to 1, images are completely + grayscale. Values between 0 and 1 desaturate the colors accordingly. + restart: true + backend: + QtWebEngine: Qt 5.14 + QtWebKit: false + # emacs: ' ## fonts diff --git a/qutebrowser/config/configinit.py b/qutebrowser/config/configinit.py index b15225210..3c80cfe1b 100644 --- a/qutebrowser/config/configinit.py +++ b/qutebrowser/config/configinit.py @@ -199,6 +199,90 @@ def qt_args(namespace: argparse.Namespace) -> typing.List[str]: return argv +def _darkmode_settings() -> typing.Iterator[typing.Tuple[str, str]]: + """Get necessary blink settings to configure dark mode for QtWebEngine.""" + if not config.val.colors.webpage.darkmode.enabled: + return + + # Mapping from a colors.webpage.darkmode.algorithm setting value to + # Chromium's DarkModeInversionAlgorithm enum values. + algorithms = { + # 0: kOff (not exposed) + # 1: kSimpleInvertForTesting (not exposed) + 'brightness-rgb': 2, # kInvertBrightness + 'lightness-hsl': 3, # kInvertLightness + 'lightness-cielab': 4, # kInvertLightnessLAB + } + + # Mapping from a colors.webpage.darkmode.policy.images setting value to + # Chromium's DarkModeImagePolicy enum values. + image_policies = { + 'always': 0, # kFilterAll + 'never': 1, # kFilterNone + 'smart': 2, # kFilterSmart + } + + # Mapping from a colors.webpage.darkmode.policy.page setting value to + # Chromium's DarkModePagePolicy enum values. + page_policies = { + 'always': 0, # kFilterAll + 'smart': 1, # kFilterByBackground + } + + bools = { + True: 'true', + False: 'false', + } + + _setting_description_type = typing.Tuple[ + str, # qutebrowser option name + str, # darkmode setting name + # Mapping from the config value to a string (or something convertable + # to a string) which gets passed to Chromium. + typing.Optional[typing.Mapping[typing.Any, typing.Union[str, int]]], + ] + if qtutils.version_check('5.15', compiled=False): + settings = [ + ('enabled', 'Enabled', bools), + ('algorithm', 'InversionAlgorithm', algorithms), + ] # type: typing.List[_setting_description_type] + mandatory_setting = 'enabled' + else: + settings = [ + ('algorithm', '', algorithms), + ] + mandatory_setting = 'algorithm' + + settings += [ + ('contrast', 'Contrast', None), + ('policy.images', 'ImagePolicy', image_policies), + ('policy.page', 'PagePolicy', page_policies), + ('threshold.text', 'TextBrightnessThreshold', None), + ('threshold.background', 'BackgroundBrightnessThreshold', None), + ('grayscale.all', 'Grayscale', bools), + ('grayscale.images', 'ImageGrayscale', None), + ] + + for setting, key, mapping in settings: + # To avoid blowing up the commandline length, we only pass modified + # settings to Chromium, as our defaults line up with Chromium's. + # However, we always pass enabled/algorithm to make sure dark mode gets + # actually turned on. + value = config.instance.get( + 'colors.webpage.darkmode.' + setting, + fallback=setting == mandatory_setting) + if isinstance(value, usertypes.Unset): + continue + + if mapping is not None: + value = mapping[value] + + # FIXME: This is "forceDarkMode" starting with Chromium 83 + prefix = 'darkMode' + + yield prefix + key, str(value) + + def _qtwebengine_args(namespace: argparse.Namespace) -> typing.Iterator[str]: """Get the QtWebEngine arguments to use based on the config.""" if not qtutils.version_check('5.11', compiled=False): @@ -224,6 +308,11 @@ def _qtwebengine_args(namespace: argparse.Namespace) -> typing.Iterator[str]: yield '--enable-logging' yield '--v=1' + blink_settings = list(_darkmode_settings()) + if blink_settings: + yield '--blink-settings=' + ','.join('{}={}'.format(k, v) + for k, v in blink_settings) + settings = { 'qt.force_software_rendering': { 'software-opengl': None, diff --git a/scripts/dev/misc_checks.py b/scripts/dev/misc_checks.py index 24c3a1ddc..6bf411bba 100644 --- a/scripts/dev/misc_checks.py +++ b/scripts/dev/misc_checks.py @@ -91,7 +91,7 @@ def check_spelling(): '[Mm]ininum', '[Rr]esett?ed', '[Rr]ecieved', '[Rr]egularily', '[Uu]nderlaying', '[Ii]nexistant', '[Ee]lipsis', 'commiting', 'existant', '[Rr]esetted', '[Ss]imilarily', '[Ii]nformations', - '[Aa]n [Uu][Rr][Ll]'} + '[Aa]n [Uu][Rr][Ll]', '[Tt]reshold'} # Words which look better when splitted, but might need some fine tuning. words |= {'[Ww]ebelements', '[Mm]ouseevent', '[Kk]eysequence', diff --git a/tests/unit/config/test_configinit.py b/tests/unit/config/test_configinit.py index 731c62a66..25c6ce85e 100644 --- a/tests/unit/config/test_configinit.py +++ b/tests/unit/config/test_configinit.py @@ -28,7 +28,7 @@ import pytest from qutebrowser import qutebrowser from qutebrowser.config import (config, configexc, configfiles, configinit, configdata, configtypes) -from qutebrowser.utils import objreg, usertypes +from qutebrowser.utils import objreg, usertypes, version from helpers import utils @@ -705,6 +705,120 @@ class TestQtArgs: assert ('--force-dark-mode' in args) == added + def test_blink_settings(self, config_stub, monkeypatch, parser): + monkeypatch.setattr(configinit.objects, 'backend', + usertypes.Backend.QtWebEngine) + monkeypatch.setattr(configinit.qtutils, 'version_check', + lambda version, exact=False, compiled=True: + True) + + config_stub.val.colors.webpage.darkmode.enabled = True + + parsed = parser.parse_args([]) + args = configinit.qt_args(parsed) + + assert '--blink-settings=darkModeEnabled=true' in args + + +class TestDarkMode: + + @pytest.mark.parametrize('settings, new_qt, expected', [ + # Disabled + ({}, True, []), + ({}, False, []), + + # Enabled without customization + ( + {'enabled': True}, + True, + [('darkModeEnabled', 'true')] + ), + ( + {'enabled': True}, + False, + [('darkMode', '4')] + ), + + # Algorithm + ( + {'enabled': True, 'algorithm': 'brightness-rgb'}, + True, + [('darkModeEnabled', 'true'), + ('darkModeInversionAlgorithm', '2')], + ), + ( + {'enabled': True, 'algorithm': 'brightness-rgb'}, + False, + [('darkMode', '2')], + ), + + ]) + @utils.qt514 + def test_basics(self, config_stub, monkeypatch, + settings, new_qt, expected): + for k, v in settings.items(): + config_stub.set_obj('colors.webpage.darkmode.' + k, v) + monkeypatch.setattr(configinit.qtutils, 'version_check', + lambda version, exact=False, compiled=True: + new_qt) + + assert list(configinit._darkmode_settings()) == expected + + @pytest.mark.parametrize('setting, value, exp_key, exp_val', [ + ('contrast', -0.5, + 'darkModeContrast', '-0.5'), + ('policy.page', 'smart', + 'darkModePagePolicy', '1'), + ('policy.images', 'smart', + 'darkModeImagePolicy', '2'), + ('threshold.text', 100, + 'darkModeTextBrightnessThreshold', '100'), + ('threshold.background', 100, + 'darkModeBackgroundBrightnessThreshold', '100'), + ('grayscale.all', True, + 'darkModeGrayscale', 'true'), + ('grayscale.images', 0.5, + 'darkModeImageGrayscale', '0.5'), + ]) + def test_customization(self, config_stub, monkeypatch, + setting, value, exp_key, exp_val): + config_stub.val.colors.webpage.darkmode.enabled = True + config_stub.set_obj('colors.webpage.darkmode.' + setting, value) + monkeypatch.setattr(configinit.qtutils, 'version_check', + lambda version, exact=False, compiled=True: + True) + + expected = [('darkModeEnabled', 'true'), (exp_key, exp_val)] + assert list(configinit._darkmode_settings()) == expected + + @utils.qt514 + def test_new_chromium(self): + """Fail if we encounter an unknown Chromium version. + + Dark mode in Chromium currently is undergoing various changes (as it's + relatively recent), and Qt 5.15 is supposed to update the underlying + Chromium at some point. + + Make this test fail deliberately with newer Chromium versions, so that + we can test whether dark mode still works manually, and adjust if not. + """ + assert version._chromium_version() in [ + 'unavailable', # QtWebKit + '77.0.3865.129', # Qt 5.14 + '80.0.3987.163', # Qt 5.15 + ] + + def test_options(self, configdata_init): + """Make sure all darkmode options have the right attributes set.""" + for name, opt in configdata.DATA.items(): + if not name.startswith('colors.webpage.darkmode.'): + continue + + backends = {'QtWebEngine': 'Qt 5.14', 'QtWebKit': False} + assert not opt.supports_pattern, name + assert opt.restart, name + assert opt.raw_backends == backends, name + @pytest.mark.parametrize('arg, confval, used', [ # overridden by commandline arg From ef3a4b00f0de7a2a2f5f013faa9e3df09611d60e Mon Sep 17 00:00:00 2001 From: Christopher Crockett Date: Wed, 27 May 2020 09:23:49 -0400 Subject: [PATCH 081/245] Corrected "c.tabs.possition" typo possition -> position --- doc/help/configuring.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/help/configuring.asciidoc b/doc/help/configuring.asciidoc index 575104fc1..02a77059c 100644 --- a/doc/help/configuring.asciidoc +++ b/doc/help/configuring.asciidoc @@ -108,7 +108,7 @@ c.completion.shrink = True ---- Note that qutebrowser does some Python magic so it's able to warn you about -mistyped config settings. As an example, if you do `c.tabs.possition = "left"`, +mistyped config settings. As an example, if you do `c.tabs.position = "left"`, you'll get an error when starting. See the link:settings{outfilesuffix}[settings help page] for all available settings. The From 6657d1bfe35c5c2f7626cc4559cfaa2eea4a9e33 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 27 May 2020 16:06:46 +0200 Subject: [PATCH 082/245] tests: Ensure consistency for configdata float values See #5394 --- tests/unit/config/test_configdata.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/unit/config/test_configdata.py b/tests/unit/config/test_configdata.py index 3dd6a588f..321135994 100644 --- a/tests/unit/config/test_configdata.py +++ b/tests/unit/config/test_configdata.py @@ -47,10 +47,17 @@ def test_data(config_stub): # https://github.com/qutebrowser/qutebrowser/issues/3104 # For lists/dicts, don't use None as default if isinstance(option.typ, (configtypes.Dict, configtypes.List)): - assert option.default is not None + assert option.default is not None, option # For ListOrValue, use a list as default if isinstance(option.typ, configtypes.ListOrValue): - assert isinstance(option.default, list) + assert isinstance(option.default, list), option + + # Make sure floats also have floats for defaults/bounds + if isinstance(option.typ, configtypes.Float): + for value in [option.default, + option.typ.minval, + option.typ.maxval]: + assert value is None or isinstance(value, float), option def test_init_benchmark(benchmark): From 723c061271987d76f9043735e98c5f8fbb604068 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 27 May 2020 16:19:43 +0200 Subject: [PATCH 083/245] configdata: Remove double spaces after periods See #5394 --- doc/help/settings.asciidoc | 6 +++--- qutebrowser/config/configdata.yml | 6 +++--- tests/unit/config/test_configdata.py | 3 +++ 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 3787238a9..cc020ea88 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -1671,7 +1671,7 @@ On QtWebKit, this setting is unavailable. [[colors.webpage.darkmode.threshold.background]] === colors.webpage.darkmode.threshold.background Threshold for inverting background elements with dark mode. -Background elements with brightness above this threshold will be inverted, and below it will be left as in the original, non-dark-mode page. Set to 256 to never invert the color or to 0 to always invert it. +Background elements with brightness above this threshold will be inverted, and below it will be left as in the original, non-dark-mode page. Set to 256 to never invert the color or to 0 to always invert it. Note: This behavior is the opposite of `colors.webpage.darkmode.threshold.text`! This setting requires a restart. @@ -1686,7 +1686,7 @@ On QtWebKit, this setting is unavailable. [[colors.webpage.darkmode.threshold.text]] === colors.webpage.darkmode.threshold.text Threshold for inverting text with dark mode. -Text colors with brightness below this threshold will be inverted, and above it will be left as in the original, non-dark-mode page. Set to 256 to always invert text color or to 0 to never invert text color. +Text colors with brightness below this threshold will be inverted, and above it will be left as in the original, non-dark-mode page. Set to 256 to always invert text color or to 0 to never invert text color. This setting requires a restart. Type: <> @@ -4029,7 +4029,7 @@ characters in the search terms are replaced by safe characters (called The search engine named `DEFAULT` is used when `url.auto_search` is turned on and something else than a URL was entered to be opened. Other search engines can be used by prepending the search engine name to the search -term, e.g. `:open google qutebrowser`. +term, e.g. `:open google qutebrowser`. Type: <> diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index dc49e4cdd..664f9facf 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -1975,7 +1975,7 @@ url.searchengines: The search engine named `DEFAULT` is used when `url.auto_search` is turned on and something else than a URL was entered to be opened. Other search engines can be used by prepending the search engine name to the search - term, e.g. `:open google qutebrowser`. + term, e.g. `:open google qutebrowser`. url.start_pages: type: @@ -2719,7 +2719,7 @@ colors.webpage.darkmode.threshold.text: Threshold for inverting text with dark mode. Text colors with brightness below this threshold will be inverted, and - above it will be left as in the original, non-dark-mode page. Set to 256 + above it will be left as in the original, non-dark-mode page. Set to 256 to always invert text color or to 0 to never invert text color. restart: true backend: @@ -2736,7 +2736,7 @@ colors.webpage.darkmode.threshold.background: Threshold for inverting background elements with dark mode. Background elements with brightness above this threshold will be inverted, - and below it will be left as in the original, non-dark-mode page. Set to + and below it will be left as in the original, non-dark-mode page. Set to 256 to never invert the color or to 0 to always invert it. Note: This behavior is the opposite of diff --git a/tests/unit/config/test_configdata.py b/tests/unit/config/test_configdata.py index 321135994..4ea5ffe6d 100644 --- a/tests/unit/config/test_configdata.py +++ b/tests/unit/config/test_configdata.py @@ -59,6 +59,9 @@ def test_data(config_stub): option.typ.maxval]: assert value is None or isinstance(value, float), option + # No double spaces after dots + assert '. ' not in option.description, option + def test_init_benchmark(benchmark): benchmark(configdata.init) From fec1b303bfb92402d0f154360d0f23d659ddb8c6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 27 May 2020 17:13:30 +0200 Subject: [PATCH 084/245] Make test_configinit work again with all platforms --- tests/unit/config/test_configinit.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/unit/config/test_configinit.py b/tests/unit/config/test_configinit.py index 25c6ce85e..2e149fa70 100644 --- a/tests/unit/config/test_configinit.py +++ b/tests/unit/config/test_configinit.py @@ -705,6 +705,7 @@ class TestQtArgs: assert ('--force-dark-mode' in args) == added + @utils.qt514 def test_blink_settings(self, config_stub, monkeypatch, parser): monkeypatch.setattr(configinit.objects, 'backend', usertypes.Backend.QtWebEngine) @@ -722,6 +723,13 @@ class TestQtArgs: class TestDarkMode: + pytestmark = utils.qt514 + + @pytest.fixture(autouse=True) + def patch_backend(self, monkeypatch): + monkeypatch.setattr(configinit.objects, 'backend', + usertypes.Backend.QtWebEngine) + @pytest.mark.parametrize('settings, new_qt, expected', [ # Disabled ({}, True, []), @@ -753,7 +761,6 @@ class TestDarkMode: ), ]) - @utils.qt514 def test_basics(self, config_stub, monkeypatch, settings, new_qt, expected): for k, v in settings.items(): @@ -791,7 +798,6 @@ class TestDarkMode: expected = [('darkModeEnabled', 'true'), (exp_key, exp_val)] assert list(configinit._darkmode_settings()) == expected - @utils.qt514 def test_new_chromium(self): """Fail if we encounter an unknown Chromium version. From a31e93c69289223c1f6db3366b5c66db02e67c7b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 27 May 2020 19:48:42 +0200 Subject: [PATCH 085/245] Revert "Corrected "c.tabs.possition" typo" This reverts commit ef3a4b00f0de7a2a2f5f013faa9e3df09611d60e. Reverts #5469. That misspelling is intentional (see context). --- doc/help/configuring.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/help/configuring.asciidoc b/doc/help/configuring.asciidoc index 02a77059c..575104fc1 100644 --- a/doc/help/configuring.asciidoc +++ b/doc/help/configuring.asciidoc @@ -108,7 +108,7 @@ c.completion.shrink = True ---- Note that qutebrowser does some Python magic so it's able to warn you about -mistyped config settings. As an example, if you do `c.tabs.position = "left"`, +mistyped config settings. As an example, if you do `c.tabs.possition = "left"`, you'll get an error when starting. See the link:settings{outfilesuffix}[settings help page] for all available settings. The From 5205ba12318a44ec40da4033fd99fb2a5829fefb Mon Sep 17 00:00:00 2001 From: Samir Benmendil Date: Sat, 23 May 2020 23:31:00 +0100 Subject: [PATCH 086/245] Add bindings to toggle cookies for current url/host --- qutebrowser/config/configdata.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 664f9facf..7e874f2aa 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -3168,6 +3168,12 @@ bindings.default: tIH: config-cycle -p -u *://*.{url:host}/* content.images ;; reload tiu: config-cycle -p -t -u {url} content.images ;; reload tIu: config-cycle -p -u {url} content.images ;; reload + tch: config-cycle -p -t -u *://{url:host}/* content.cookies.accept all no-3rdparty never ;; reload + tCh: config-cycle -p -u *://{url:host}/* content.cookies.accept all no-3rdparty never ;; reload + tcH: config-cycle -p -t -u *://*.{url:host}/* content.cookies.accept all no-3rdparty never ;; reload + tCH: config-cycle -p -u *://*.{url:host}/* content.cookies.accept all no-3rdparty never ;; reload + tcu: config-cycle -p -t -u {url} content.cookies.accept all no-3rdparty never ;; reload + tCu: config-cycle -p -u {url} content.cookies.accept all no-3rdparty never ;; reload insert: : open-editor : insert-text -- {primary} From ac7a3ce861b9c564749fa6961ee43b98dbca462b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 28 May 2020 18:08:53 +0200 Subject: [PATCH 087/245] tests: Don't use ";;" for caret tests If we chain two commands, the end2end test code will still only wait for "command called: *" once. This causes all future waits to be "shifted" by one, which can cause flaky tests on Windows. All other usages of command chaining in tests actually *need* the second command to run as soon as possible after the original one. However, for the caret tests, we only need to run two commands, see 2b0870084b9185b8f8a12639d238c12b202d3284. Because pytest-bdd doesn't allow us to re-use "Given" steps, and "Background:" only accepts "Given", let's add a second "Given" step as an ugly but acceptable hack. See https://github.com/pytest-dev/pytest-bdd/issues/157 See https://github.com/qutebrowser/qutebrowser/issues/5390#issuecomment-622885572 --- tests/end2end/features/caret.feature | 3 ++- tests/end2end/features/conftest.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/end2end/features/caret.feature b/tests/end2end/features/caret.feature index e540bafcb..ec45efaea 100644 --- a/tests/end2end/features/caret.feature +++ b/tests/end2end/features/caret.feature @@ -5,7 +5,8 @@ Feature: Caret mode Background: Given I open data/caret.html - And I run :tab-only ;; enter-mode caret + And I run :tab-only + And I also run :enter-mode caret # :yank selection diff --git a/tests/end2end/features/conftest.py b/tests/end2end/features/conftest.py index 6ac5f281d..c1e7e32ae 100644 --- a/tests/end2end/features/conftest.py +++ b/tests/end2end/features/conftest.py @@ -152,6 +152,17 @@ def run_command_given(quteproc, command): quteproc.send_cmd(command) +@bdd.given(bdd.parsers.parse("I also run {command}")) +def run_command_given_2(quteproc, command): + """Run a qutebrowser command. + + Separate from the above as a hack to run two commands in a Background + without having to use ";;". This is needed because pytest-bdd doesn't allow + re-using a Given step... + """ + quteproc.send_cmd(command) + + @bdd.given("I have a fresh instance") def fresh_instance(quteproc): """Restart qutebrowser instance for tests needing a fresh state.""" From 73e08124d858b2da9041d944b256947b77b2cfcd Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 29 May 2020 15:31:18 +0200 Subject: [PATCH 088/245] Add site-specific quirk for Google Drive Fixes #5472 See #4805, #4810 --- doc/changelog.asciidoc | 3 +++ qutebrowser/browser/webengine/webenginesettings.py | 1 + 2 files changed, 4 insertions(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index a6aead789..b982c8763 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -72,6 +72,9 @@ Fixed - The workaround for session loading with Qt 5.15 now handles `sessions.lazy_restore` so that the saved page is loaded instead of the "stub" page with no possibility to get to the web page. +- A site specific quirk to allow typing accented characters on Google + Docs was active for docs.google.com, but not drive.google.com. It is + now applied for both subdomains. v1.11.1 (2020-05-07) -------------------- diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index d5d654dbf..c76a1c90a 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -403,6 +403,7 @@ def _init_site_specific_quirks(): 'https://accounts.google.com/*': firefox_ua, 'https://*.slack.com/*': new_chrome_ua, 'https://docs.google.com/*': firefox_ua, + 'https://drive.google.com/*': firefox_ua, } if not qtutils.version_check('5.9'): From 6356e80ecc3c94c1f2f4098105d3bdf4089f3c06 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 29 May 2020 16:16:09 +0200 Subject: [PATCH 089/245] Remove unused import in utils.message Not needed anymore since 16d98a4137762dfb2731d8bc185549de721d3ca6 - for some odd reason, the pylint failure only came up on CI now, and I don't see it locally... --- qutebrowser/utils/message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py index a8c7c8d85..2754d87e7 100644 --- a/qutebrowser/utils/message.py +++ b/qutebrowser/utils/message.py @@ -25,7 +25,7 @@ import traceback import typing -from PyQt5.QtCore import pyqtSignal, QObject, QUrl +from PyQt5.QtCore import pyqtSignal, QObject from qutebrowser.utils import usertypes, log, utils From 6aeb3d81859a53b3ac779b881d6ab0e8035939fa Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 29 May 2020 20:53:21 +0200 Subject: [PATCH 090/245] Refactor how we get OpenGL info This allows us to get the version string in addition to the vendor. We also show that version string in the version info output. See #5313 --- qutebrowser/misc/backendproblem.py | 16 ++---- qutebrowser/utils/utils.py | 15 ++++++ qutebrowser/utils/version.py | 70 ++++++++++++++++++++++--- tests/conftest.py | 6 +-- tests/unit/utils/test_version.py | 82 ++++++++++++++++++++++++++++-- 5 files changed, 159 insertions(+), 30 deletions(-) diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py index 2ed273547..73044cd99 100644 --- a/qutebrowser/misc/backendproblem.py +++ b/qutebrowser/misc/backendproblem.py @@ -23,8 +23,6 @@ import os import sys import functools import html -import ctypes -import ctypes.util import enum import shutil import typing @@ -201,19 +199,10 @@ class _BackendProblemChecker: def _nvidia_shader_workaround(self) -> None: """Work around QOpenGLShaderProgram issues. - NOTE: This needs to be called before _handle_nouveau_graphics, or some - setups will segfault in version.opengl_vendor(). - See https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826 """ self._assert_backend(usertypes.Backend.QtWebEngine) - - if os.environ.get('QUTE_SKIP_LIBGL_WORKAROUND'): - return - - libgl = ctypes.util.find_library("GL") - if libgl is not None: - ctypes.CDLL(libgl, mode=ctypes.RTLD_GLOBAL) + utils.libgl_workaround() def _handle_nouveau_graphics(self) -> None: """Force software rendering when using the Nouveau driver. @@ -231,7 +220,8 @@ class _BackendProblemChecker: if qtutils.version_check('5.10', compiled=False): return - if version.opengl_vendor() != 'nouveau': + opengl_info = version.opengl_info() + if opengl_info is None or opengl_info.vendor != 'nouveau': return if (os.environ.get('LIBGL_ALWAYS_SOFTWARE') == '1' or diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 368cb0ab6..39d46add8 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -36,6 +36,8 @@ import shlex import glob import mimetypes import typing +import ctypes +import ctypes.util from PyQt5.QtCore import QUrl from PyQt5.QtGui import QColor, QClipboard, QDesktopServices @@ -776,3 +778,16 @@ def ceil_log(number: int, base: int) -> int: result += 1 accum *= base return result + + +def libgl_workaround(): + """Work around QOpenGLShaderProgram issues, especially for Nvidia. + + See https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826 + """ + if os.environ.get('QUTE_SKIP_LIBGL_WORKAROUND'): + return + + libgl = ctypes.util.find_library("GL") + if libgl is not None: + ctypes.CDLL(libgl, mode=ctypes.RTLD_GLOBAL) diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 1ad8b22cf..8dc4ec593 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -31,6 +31,7 @@ import enum import datetime import getpass import typing +import functools import attr import pkg_resources @@ -442,8 +443,8 @@ def version() -> str: if qapp: style = qapp.style() lines.append('Style: {}'.format(style.metaObject().className())) - platform_name = qapp.platformName() - lines.append('Platform plugin: {}'.format(platform_name)) + lines.append('Platform plugin: {}'.format(qapp.platformName())) + lines.append('OpenGL: {}'.format(opengl_info())) importpath = os.path.dirname(os.path.abspath(qutebrowser.__file__)) @@ -487,7 +488,55 @@ def version() -> str: return '\n'.join(lines) -def opengl_vendor() -> typing.Optional[str]: # pragma: no cover +@attr.s +class OpenGLInfo: + + """Information about the OpenGL setup in use.""" + + # If we're using OpenGL ES. If so, no further information is available. + gles = attr.ib(False) # type: bool + + # The name of the vendor. Examples: + # - nouveau + # - "Intel Open Source Technology Center", "Intel", "Intel Inc." + vendor = attr.ib(None) # type: typing.Optional[str] + + # The OpenGL version as a string. See tests for examples. + version_str = attr.ib(None) # type: typing.Optional[str] + + # The parsed version as a (major, minor) tuple of ints + version = attr.ib(None) # type: typing.Optional[typing.Tuple[int, ...]] + + # The vendor specific information following the version number + vendor_specific = attr.ib(None) # type: typing.Optional[str] + + def __str__(self) -> str: + if self.gles: + return 'OpenGL ES' + return '{}, {}'.format(self.vendor, self.version_str) + + @classmethod + def parse(cls, *, vendor: str, version: str) -> 'OpenGLInfo': + if ' ' not in version: + log.misc.warning("Failed to parse OpenGL version (missing space): " + "{}".format(version)) + return cls(vendor=vendor, version_str=version) + + num_str, vendor_specific = version.split(' ', maxsplit=1) + + try: + parsed_version = tuple(int(i) for i in num_str.split('.')) + except ValueError: + log.misc.warning("Failed to parse OpenGL version (parsing int): " + "{}".format(version)) + return cls(vendor=vendor, version_str=version) + + return cls(vendor=vendor, version_str=version, + version=parsed_version, vendor_specific=vendor_specific) + + +@functools.lru_cache(maxsize=1) +def opengl_info() -> typing.Optional[OpenGLInfo]: # pragma: no cover """Get the OpenGL vendor used. This returns a string such as 'nouveau' or @@ -496,10 +545,14 @@ def opengl_vendor() -> typing.Optional[str]: # pragma: no cover """ assert QApplication.instance() - override = os.environ.get('QUTE_FAKE_OPENGL_VENDOR') + # Some setups can segfault in here if we don't do this. + utils.libgl_workaround() + + override = os.environ.get('QUTE_FAKE_OPENGL') if override is not None: log.init.debug("Using override {}".format(override)) - return override + vendor, version = override.split(', ', maxsplit=1) + return OpenGLInfo.parse(vendor=vendor, version=version) old_context = typing.cast(typing.Optional[QOpenGLContext], QOpenGLContext.currentContext()) @@ -522,7 +575,7 @@ def opengl_vendor() -> typing.Optional[str]: # pragma: no cover try: if ctx.isOpenGLES(): # Can't use versionFunctions there - return None + return OpenGLInfo(gles=True) vp = QOpenGLVersionProfile() vp.setVersion(2, 0) @@ -537,7 +590,10 @@ def opengl_vendor() -> typing.Optional[str]: # pragma: no cover log.init.debug("Getting version functions failed!") return None - return vf.glGetString(vf.GL_VENDOR) + vendor = vf.glGetString(vf.GL_VENDOR) + version = vf.glGetString(vf.GL_VERSION) + + return OpenGLInfo.parse(vendor=vendor, version=version) finally: ctx.doneCurrent() if old_context and old_surface: diff --git a/tests/conftest.py b/tests/conftest.py index c6b6c2efc..e698bde74 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,8 +25,6 @@ import os import sys import warnings import pathlib -import ctypes -import ctypes.util import pytest import hypothesis @@ -258,9 +256,7 @@ def set_backend(monkeypatch, request): @pytest.fixture(autouse=True, scope='session') def apply_libgl_workaround(): """Make sure we load libGL early so QtWebEngine tests run properly.""" - libgl = ctypes.util.find_library("GL") - if libgl is not None: - ctypes.CDLL(libgl, mode=ctypes.RTLD_GLOBAL) + utils.libgl_workaround() @pytest.fixture(autouse=True) diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py index 0a3c5e4aa..903585b62 100644 --- a/tests/unit/utils/test_version.py +++ b/tests/unit/utils/test_version.py @@ -35,6 +35,8 @@ import datetime import attr import pkg_resources import pytest +import hypothesis +import hypothesis.strategies import qutebrowser from qutebrowser.config import config @@ -956,11 +958,15 @@ def test_version_output(params, stubs, monkeypatch, config_stub): 'config.instance.yaml_loaded': params.autoconfig_loaded, } + version.opengl_info.cache_clear() + monkeypatch.setenv('QUTE_FAKE_OPENGL', 'VENDOR, 1.0 VERSION') + substitutions = { 'git_commit': '\nGit commit: GIT COMMIT' if params.git_commit else '', 'style': '\nStyle: STYLE' if params.qapp else '', 'platform_plugin': ('\nPlatform plugin: PLATFORM' if params.qapp else ''), + 'opengl': '\nOpenGL: VENDOR, 1.0 VERSION' if params.qapp else '', 'qt': 'QT VERSION', 'frozen': str(params.frozen), 'import_path': import_path, @@ -1026,7 +1032,7 @@ def test_version_output(params, stubs, monkeypatch, config_stub): pdf.js: PDFJS VERSION sqlite: SQLITE VERSION QtNetwork SSL: {ssl} - {style}{platform_plugin} + {style}{platform_plugin}{opengl} Platform: PLATFORM, ARCHITECTURE{linuxdist} Frozen: {frozen} Imported from {import_path} @@ -1045,10 +1051,76 @@ def test_version_output(params, stubs, monkeypatch, config_stub): assert version.version() == expected -def test_opengl_vendor(qapp): - """Simply call version.opengl_vendor() and see if it doesn't crash.""" - pytest.importorskip("PyQt5.QtOpenGL") - return version.opengl_vendor() +class TestOpenGLInfo: + + @pytest.fixture(autouse=True) + def cache_clear(self): + """Clear the lru_cache between tests.""" + version.opengl_info.cache_clear() + + def test_func(self, qapp): + """Simply call version.opengl_info() and see if it doesn't crash.""" + pytest.importorskip("PyQt5.QtOpenGL") + version.opengl_info() + + def test_func_fake(self, qapp, monkeypatch): + monkeypatch.setenv('QUTE_FAKE_OPENGL', 'Outtel Inc., 3.0 Messiah 20.0') + info = version.opengl_info() + assert info.vendor == 'Outtel Inc.' + assert info.version_str == '3.0 Messiah 20.0' + assert info.version == (3, 0) + assert info.vendor_specific == 'Messiah 20.0' + + @pytest.mark.parametrize('version_str, reason', [ + ('blah', 'missing space'), + ('2,x blah', 'parsing int'), + ]) + def test_parse_invalid(self, caplog, version_str, reason): + with caplog.at_level(logging.WARNING): + info = version.OpenGLInfo.parse(vendor="vendor", + version=version_str) + + assert info.version is None + assert info.vendor_specific is None + assert info.vendor == 'vendor' + assert info.version_str == version_str + + msg = "Failed to parse OpenGL version ({}): {}".format( + reason, version_str) + assert caplog.messages == [msg] + + @hypothesis.given(vendor=hypothesis.strategies.text(), + version_str=hypothesis.strategies.text()) + def test_parse_hypothesis(self, caplog, vendor, version_str): + with caplog.at_level(logging.WARNING): + info = version.OpenGLInfo.parse(vendor=vendor, version=version_str) + + assert info.vendor == vendor + assert info.version_str == version_str + assert vendor in str(info) + assert version_str in str(info) + + if info.version is not None: + reconstructed = ' '.join(['.'.join(str(part) + for part in info.version), + info.vendor_specific]) + assert reconstructed == info.version_str + + @pytest.mark.parametrize('version_str, expected', [ + ("2.1 INTEL-10.36.26", (2, 1)), + ("4.6 (Compatibility Profile) Mesa 20.0.7", (4, 6)), + ("3.0 Mesa 20.0.7", (3, 0)), + ("3.0 Mesa 20.0.6", (3, 0)), + # Not from the wild, but can happen according to standards + ("3.0.2 Mesa 20.0.6", (3, 0, 2)), + ]) + def test_version(self, version_str, expected): + info = version.OpenGLInfo.parse(vendor='vendor', version=version_str) + assert info.version == expected + + def test_str_gles(self): + info = version.OpenGLInfo(gles=True) + assert str(info) == 'OpenGL ES' @pytest.fixture From ec017f3aa3b58e2d8839b2fd98ea36428d001bb9 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 29 May 2020 20:58:21 +0200 Subject: [PATCH 091/245] backendproblem: Deduplicate text --- qutebrowser/misc/backendproblem.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py index 73044cd99..01cfc988a 100644 --- a/qutebrowser/misc/backendproblem.py +++ b/qutebrowser/misc/backendproblem.py @@ -165,6 +165,14 @@ class _BackendProblemChecker: """Check for various backend-specific issues.""" + SOFTWARE_RENDERING_TEXT = ( + "

Forcing software rendering

" + "

This allows you to use the newer QtWebEngine backend (based on " + "Chromium) but could have noticeable performance impact (depending on " + "your hardware). This sets the qt.force_software_rendering = " + "'chromium' option (if you have a config.py file, you'll " + "need to set this manually).

") + def __init__(self, *, no_err_windows: bool, save_manager: savemanager.SaveManager) -> None: @@ -238,14 +246,8 @@ class _BackendProblemChecker: self._show_dialog( backend=usertypes.Backend.QtWebEngine, because="you're using Nouveau graphics", - text=("

There are two ways to fix this:

" - "

Forcing software rendering

" - "

This allows you to use the newer QtWebEngine backend " - "(based on Chromium) but could have noticeable performance " - "impact (depending on your hardware). This sets the " - "qt.force_software_rendering = 'chromium' option " - "(if you have a config.py file, you'll need to set " - "this manually).

"), + text=("

There are two ways to fix this:

" + + self.SOFTWARE_RENDERING_TEXT), buttons=[button], ) @@ -290,13 +292,7 @@ class _BackendProblemChecker: buttons.append(_Button("Force software rendering", 'qt.force_software_rendering', 'chromium')) - text += ("

Forcing software rendering

" - "

This allows you to use the newer QtWebEngine backend " - "(based on Chromium) but could have noticeable " - "performance impact (depending on your hardware). This " - "sets the qt.force_software_rendering = " - "'chromium' option (if you have a config.py " - "file, you'll need to set this manually).

") + text += self.SOFTWARE_RENDERING_TEXT self._show_dialog(backend=usertypes.Backend.QtWebEngine, because="you're using Wayland", From 081453b29cda86f2db0f2187cd0d3f2e7b16a5fa Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sat, 30 May 2020 07:25:26 -0400 Subject: [PATCH 092/245] Use code quotes for flags in docs. Co-authored-by: Florian Bruhin --- qutebrowser/config/configdata.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 6d8078ad2..b9bdf4594 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -3210,4 +3210,4 @@ logging.level.console: name: LogLevel desc: >- Level for console (stdout/stderr) logs. - Ignored if the --loglevel or --debug CLI flags are used. + Ignored if the `--loglevel` or `--debug` CLI flags are used. From 682c9d1ba1719765f72a37c5ad26b5813d48621b Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sat, 30 May 2020 07:55:27 -0400 Subject: [PATCH 093/245] Ignore log configs if --debug is passed. CLI flags should take priority over configs. If --debug is passed, that is a deliberate request from the user to increase log verbosity above the norm. The e2e tests were breaking because the default configs were overriding the --debug flag and hiding logs the tests wait for. --- qutebrowser/utils/log.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 683ee92f4..58311693f 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -541,6 +541,9 @@ def init_from_config(conf: 'configmodule.ConfigContainer') -> None: cyclic import. """ assert _args is not None + if _args.debug: + init.info("--debug flag overrides log configs") + return if ram_handler: ramlevel = conf.logging.level.ram init.info("Configuring RAM loglevel to %s", ramlevel) From b668034afa9dbbf5e0e847773d6edc1146dc5228 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sat, 30 May 2020 08:00:06 -0400 Subject: [PATCH 094/245] Fix flake8 and lint --- tests/unit/utils/test_log.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/tests/unit/utils/test_log.py b/tests/unit/utils/test_log.py index 2ced94c42..6b770ed44 100644 --- a/tests/unit/utils/test_log.py +++ b/tests/unit/utils/test_log.py @@ -255,13 +255,12 @@ class TestInitLog: with pytest.raises(PendingDeprecationWarning): warnings.warn("test warning", PendingDeprecationWarning) - @pytest.mark.parametrize('cli, conf, expected', - [ - (None, 'info', logging.INFO), - (None, 'warning', logging.WARNING), - ('info', 'warning', logging.INFO), - ('warning', 'info', logging.WARNING), - ]) + @pytest.mark.parametrize('cli, conf, expected', [ + (None, 'info', logging.INFO), + (None, 'warning', logging.WARNING), + ('info', 'warning', logging.INFO), + ('warning', 'info', logging.WARNING), + ]) def test_init_from_config_console(self, cli, conf, expected, args, config_stub): args.debug = False @@ -272,13 +271,12 @@ class TestInitLog: log.init_from_config(config_stub.val) assert log.console_handler.level == expected - @pytest.mark.parametrize('conf, expected', - [ - ('vdebug', logging.VDEBUG), - ('debug', logging.DEBUG), - ('info', logging.INFO), - ('critical', logging.CRITICAL), - ]) + @pytest.mark.parametrize('conf, expected', [ + ('vdebug', logging.VDEBUG), + ('debug', logging.DEBUG), + ('info', logging.INFO), + ('critical', logging.CRITICAL), + ]) def test_init_from_config_ram(self, conf, expected, args, config_stub): args.debug = False log.init_log(args) @@ -288,7 +286,7 @@ class TestInitLog: assert log.ram_handler.level == expected def test_init_from_config_consistent_default(self, config_stub): - """Ensure config defaults are consistent with the builtin defaults""" + """Ensure config defaults are consistent with the builtin defaults.""" args = qutebrowser.get_argparser().parse_args([]) log.init_log(args) From 2da752b47386a847cf74bc7e706d3db6d39cea8a Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sat, 30 May 2020 08:04:25 -0400 Subject: [PATCH 095/245] More pylint fixes --- qutebrowser/config/configtypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 546d90c38..c92e404cf 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -2021,7 +2021,7 @@ class LogLevel(String): def __init__(self, none_ok: bool = False) -> None: super().__init__(none_ok=none_ok) self.valid_values = ValidValues(*[level.lower() - for level in log.LOG_LEVELS]) + for level in log.LOG_LEVELS]) class Key(BaseType): From d8fe8597c0e65154e52627aea7d4d8ecffcafdb6 Mon Sep 17 00:00:00 2001 From: Maxim Baz Date: Mon, 1 Jun 2020 12:35:45 +0200 Subject: [PATCH 096/245] Decrease log verbosity of adblock component By default every blocked request is logged, which seems way too verbose for those who aren't debugging this component --- qutebrowser/components/adblock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/components/adblock.py b/qutebrowser/components/adblock.py index 2ceea2cf7..a683e9190 100644 --- a/qutebrowser/components/adblock.py +++ b/qutebrowser/components/adblock.py @@ -128,7 +128,7 @@ class HostBlocker: """Block the given request if necessary.""" if self._is_blocked(request_url=info.request_url, first_party_url=info.first_party_url): - logger.info("Request to {} blocked by host blocker." + logger.debug("Request to {} blocked by host blocker." .format(info.request_url.host())) info.block() From deba7273785638de7ddd6921db0a50d88771e21c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 1 Jun 2020 13:43:59 +0200 Subject: [PATCH 097/245] backendproblem: Suggest to disable WebGL with older OpenGL versions Also make it a bit clearer that using QtWebKit isn't the preferred solution to those problems. Fixes #5313 --- doc/changelog.asciidoc | 3 + qutebrowser/misc/backendproblem.py | 95 ++++++++++++++++++++++++------ 2 files changed, 79 insertions(+), 19 deletions(-) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index b982c8763..8d8deaec0 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -75,6 +75,9 @@ Fixed - A site specific quirk to allow typing accented characters on Google Docs was active for docs.google.com, but not drive.google.com. It is now applied for both subdomains. +- With older graphics hardware (OpenGL < 4.3) with Qt 5.14 on Wayland, WebGL + causes segfaults. Now qutebrowser detects that combination and suggests to + disable WebGL or use XWayland. v1.11.1 (2020-05-07) -------------------- diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py index 01cfc988a..5c8e1002d 100644 --- a/qutebrowser/misc/backendproblem.py +++ b/qutebrowser/misc/backendproblem.py @@ -79,19 +79,21 @@ def _error_text(because: str, text: str, backend: usertypes.Backend) -> str: if other_backend == usertypes.Backend.QtWebKit: warning = ("Note that QtWebKit hasn't been updated since " "July 2017 (including security updates).") + suffix = " (not recommended)" else: warning = "" + suffix = "" return ("Failed to start with the {backend} backend!" "

qutebrowser tried to start with the {backend} backend but " "failed because {because}.

{text}" - "

Forcing the {other_backend.name} backend

" + "

Forcing the {other_backend.name} backend{suffix}

" "

This forces usage of the {other_backend.name} backend by " "setting the backend = '{other_setting}' option " "(if you have a config.py file, you'll need to set " "this manually). {warning}

".format( backend=backend.name, because=because, text=text, other_backend=other_backend, other_setting=other_setting, - warning=warning)) + warning=warning, suffix=suffix)) class _Dialog(QDialog): @@ -253,23 +255,8 @@ class _BackendProblemChecker: raise utils.Unreachable - def _handle_wayland(self) -> None: - self._assert_backend(usertypes.Backend.QtWebEngine) - - if os.environ.get('QUTE_SKIP_WAYLAND_CHECK'): - return - - platform = QApplication.instance().platformName() - if platform not in ['wayland', 'wayland-egl']: - return - - has_qt511 = qtutils.version_check('5.11', compiled=False) - if has_qt511 and config.val.qt.force_software_rendering == 'chromium': - return - - if qtutils.version_check('5.11.2', compiled=False): - return - + def _xwayland_options(self) -> typing.Tuple[typing.List[_Button], str]: + """Get buttons/text for a possible XWayland solution.""" buttons = [] text = "

You can work around this in one of the following ways:

" @@ -288,6 +275,27 @@ class _BackendProblemChecker: "

This allows you to use the newer QtWebEngine backend " "(based on Chromium). ") + return text, buttons + + def _handle_wayland(self) -> None: + self._assert_backend(usertypes.Backend.QtWebEngine) + + if os.environ.get('QUTE_SKIP_WAYLAND_CHECK'): + return + + platform = QApplication.instance().platformName() + if platform not in ['wayland', 'wayland-egl']: + return + + has_qt511 = qtutils.version_check('5.11', compiled=False) + if has_qt511 and config.val.qt.force_software_rendering == 'chromium': + return + + if qtutils.version_check('5.11.2', compiled=False): + return + + text, buttons = self._xwayland_options() + if has_qt511: buttons.append(_Button("Force software rendering", 'qt.force_software_rendering', @@ -299,6 +307,54 @@ class _BackendProblemChecker: text=text, buttons=buttons) + def _handle_wayland_webgl(self) -> None: + """On older graphic hardware, WebGL on Wayland causes segfaults. + + See https://github.com/qutebrowser/qutebrowser/issues/5313 + """ + self._assert_backend(usertypes.Backend.QtWebEngine) + + if os.environ.get('QUTE_SKIP_WAYLAND_WEBGL_CHECK'): + return + + platform = QApplication.instance().platformName() + if platform not in ['wayland', 'wayland-egl']: + return + + # Only Qt 5.14 should be affected + if not qtutils.version_check('5.14', compiled=False): + return + if qtutils.version_check('5.15', compiled=False): + return + + # Newer graphic hardware isn't affected + opengl_info = version.opengl_info() + if (opengl_info is None or + opengl_info.gles or + opengl_info.version is None or + opengl_info.version >= (4, 3)): + return + + # If WebGL is turned off, we're fine + if not config.val.content.webgl: + return + + text, buttons = self._xwayland_options() + + buttons.append(_Button("Turn off WebGL (recommended)", + 'content.webgl', + False)) + text += ("

Disable WebGL (recommended)

" + "This sets the content.webgl = False option " + "(if you have a config.py file, you'll need to " + "set this manually).

") + + self._show_dialog(backend=usertypes.Backend.QtWebEngine, + because=("of frequent crashes with Qt 5.14 on " + "Wayland with older graphics hardware"), + text=text, + buttons=buttons) + def _try_import_backends(self) -> _BackendImports: """Check whether backends can be imported and return BackendImports.""" # pylint: disable=unused-import @@ -480,6 +536,7 @@ class _BackendProblemChecker: self._handle_ssl_support() self._handle_wayland() self._nvidia_shader_workaround() + self._handle_wayland_webgl() self._handle_nouveau_graphics() self._handle_cache_nuking() self._handle_serviceworker_nuking() From f971cc041f5b3c096ca23e1291005079b8bf3431 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 1 Jun 2020 13:54:58 +0200 Subject: [PATCH 098/245] ci: Switch to Qt 5.15 --- .appveyor.yml | 4 ++-- .travis.yml | 20 ++++++++++++++----- misc/requirements/requirements-pyqt-5.15.txt | 5 +++++ .../requirements-pyqt-5.15.txt-raw | 4 ++++ misc/requirements/requirements-pyqt.txt | 6 +++--- tox.ini | 7 ++++--- 6 files changed, 33 insertions(+), 13 deletions(-) create mode 100644 misc/requirements/requirements-pyqt-5.15.txt create mode 100644 misc/requirements/requirements-pyqt-5.15.txt-raw diff --git a/.appveyor.yml b/.appveyor.yml index 0ee670b37..23a96055c 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -10,8 +10,8 @@ image: environment: PYTHONUNBUFFERED: 1 - PYTHON: C:\Python37-x64\python.exe - TESTENV: py37-pyqt514 + PYTHON: C:\Python38-x64\python.exe + TESTENV: py38-pyqt515 install: - '%PYTHON% --version' diff --git a/.travis.yml b/.travis.yml index 4ca28c375..28ad24af9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -45,7 +45,6 @@ matrix: ### PyQt 5.12 (Python 3.8) - env: TESTENV=py38-pyqt512 - # http://code.qt.io/cgit/qt/qtbase.git/commit/?id=c3a963da1f9e7b1d37e63eedded61da4fbdaaf9a addons: apt: packages: @@ -53,20 +52,31 @@ matrix: ### PyQt 5.13 (Python 3.8) - env: TESTENV=py38-pyqt513 - # http://code.qt.io/cgit/qt/qtbase.git/commit/?id=c3a963da1f9e7b1d37e63eedded61da4fbdaaf9a addons: apt: packages: - libxkbcommon-x11-0 - ### PyQt 5.14 (Python 3.8, with coverage) - - env: TESTENV=py38-pyqt514-cov - # http://code.qt.io/cgit/qt/qtbase.git/commit/?id=c3a963da1f9e7b1d37e63eedded61da4fbdaaf9a + ### PyQt 5.14 (Python 3.8) + - env: TESTENV=py38-pyqt514 addons: apt: packages: - libxkbcommon-x11-0 + ### PyQt 5.15 (Python 3.8, with coverage) + - env: TESTENV=py38-pyqt515-cov + addons: + apt: + packages: + - libxkbcommon-x11-0 + - libxcb-icccm4 + - libxcb-image0 + - libxcb-keysyms1 + - libxcb-randr0 + - libxcb-render-util0 + - libxcb-xinerama0 + ### macOS Mojave (10.14) - os: osx env: TESTENV=py37-pyqt514 OSX=mojave diff --git a/misc/requirements/requirements-pyqt-5.15.txt b/misc/requirements/requirements-pyqt-5.15.txt new file mode 100644 index 000000000..c21b7b742 --- /dev/null +++ b/misc/requirements/requirements-pyqt-5.15.txt @@ -0,0 +1,5 @@ +# This file is automatically generated by scripts/dev/recompile_requirements.py + +PyQt5==5.15.0 # rq.filter: < 6 +PyQt5-sip==12.8.0 +PyQtWebEngine==5.15.0 # rq.filter: < 6 diff --git a/misc/requirements/requirements-pyqt-5.15.txt-raw b/misc/requirements/requirements-pyqt-5.15.txt-raw new file mode 100644 index 000000000..c9eeb9fb7 --- /dev/null +++ b/misc/requirements/requirements-pyqt-5.15.txt-raw @@ -0,0 +1,4 @@ +#@ filter: PyQt5 < 6 +#@ filter: PyQtWebEngine < 6 +PyQt5 >= 5.15, < 6 +PyQtWebEngine >= 5.15, < 6 diff --git a/misc/requirements/requirements-pyqt.txt b/misc/requirements/requirements-pyqt.txt index 90febc2e7..74d86e8d5 100644 --- a/misc/requirements/requirements-pyqt.txt +++ b/misc/requirements/requirements-pyqt.txt @@ -1,5 +1,5 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -PyQt5==5.14.2 -PyQt5-sip==12.7.2 -PyQtWebEngine==5.14.0 +PyQt5==5.15.0 +PyQt5-sip==12.8.0 +PyQtWebEngine==5.15.0 diff --git a/tox.ini b/tox.ini index 00b14bfb0..4e16742cc 100644 --- a/tox.ini +++ b/tox.ini @@ -4,15 +4,15 @@ # and then run "tox" from this directory. [tox] -envlist = py37-pyqt514-cov,misc,vulture,flake8,pylint,pyroma,check-manifest,eslint +envlist = py37-pyqt515-cov,misc,vulture,flake8,pylint,pyroma,check-manifest,eslint distshare = {toxworkdir} skipsdist = true [testenv] setenv = PYTEST_QT_API=pyqt5 - pyqt{,57,59,510,511,512,513,514}: LINK_PYQT_SKIP=true - pyqt{,57,59,510,511,512,513,514}: QUTE_BDD_WEBENGINE=true + pyqt{,57,59,510,511,512,513,514,515}: LINK_PYQT_SKIP=true + pyqt{,57,59,510,511,512,513,514,515}: QUTE_BDD_WEBENGINE=true cov: PYTEST_ADDOPTS=--cov --cov-report xml --cov-report=html --cov-report= passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER CI TRAVIS XDG_* QUTE_* DOCKER QT_QUICK_BACKEND basepython = @@ -32,6 +32,7 @@ deps = pyqt512: -r{toxinidir}/misc/requirements/requirements-pyqt-5.12.txt pyqt513: -r{toxinidir}/misc/requirements/requirements-pyqt-5.13.txt pyqt514: -r{toxinidir}/misc/requirements/requirements-pyqt-5.14.txt + pyqt515: -r{toxinidir}/misc/requirements/requirements-pyqt-5.15.txt commands = {envpython} scripts/link_pyqt.py --tox {envdir} {envpython} -bb -m pytest {posargs:tests} From c59165c92ff6f83986662f6e70e5d0ea423eccd2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 1 Jun 2020 14:09:00 +0200 Subject: [PATCH 099/245] Recompile requirements --- misc/requirements/requirements-dev.txt | 4 ++-- misc/requirements/requirements-pylint.txt | 2 +- misc/requirements/requirements-pyqt-5.12.txt | 2 +- misc/requirements/requirements-pyqt-5.13.txt | 2 +- misc/requirements/requirements-pyqt-5.14.txt | 2 +- misc/requirements/requirements-sphinx.txt | 2 +- misc/requirements/requirements-tests.txt | 8 ++++---- misc/requirements/requirements-tests.txt-raw | 3 +++ misc/requirements/requirements-vulture.txt | 2 +- 9 files changed, 15 insertions(+), 12 deletions(-) diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt index 620e5772c..bb834483f 100644 --- a/misc/requirements/requirements-dev.txt +++ b/misc/requirements/requirements-dev.txt @@ -17,10 +17,10 @@ packaging==20.4 pycparser==2.20 Pympler==0.8 pyparsing==2.4.7 -PyQt-builder==1.3.2 +PyQt-builder==1.4.0 python-dateutil==2.8.1 requests==2.23.0 -sip==5.2.0 +sip==5.3.0 six==1.15.0 toml==0.10.1 uritemplate==3.0.1 diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index e80ddbf50..988b58786 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -20,4 +20,4 @@ six==1.15.0 typed-ast==1.4.1 ; python_version<"3.8" uritemplate==3.0.1 urllib3==1.25.9 -wrapt==1.12.1 +wrapt==1.11.2 diff --git a/misc/requirements/requirements-pyqt-5.12.txt b/misc/requirements/requirements-pyqt-5.12.txt index b1be83265..9b458cd98 100644 --- a/misc/requirements/requirements-pyqt-5.12.txt +++ b/misc/requirements/requirements-pyqt-5.12.txt @@ -1,5 +1,5 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py PyQt5==5.12.3 # rq.filter: < 5.13 -PyQt5-sip==12.7.2 +PyQt5-sip==12.8.0 PyQtWebEngine==5.12.1 # rq.filter: < 5.13 diff --git a/misc/requirements/requirements-pyqt-5.13.txt b/misc/requirements/requirements-pyqt-5.13.txt index dc2f0359a..7c07eac3d 100644 --- a/misc/requirements/requirements-pyqt-5.13.txt +++ b/misc/requirements/requirements-pyqt-5.13.txt @@ -1,5 +1,5 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py PyQt5==5.13.2 # rq.filter: < 5.14 -PyQt5-sip==12.7.2 +PyQt5-sip==12.8.0 PyQtWebEngine==5.13.2 # rq.filter: < 5.14 diff --git a/misc/requirements/requirements-pyqt-5.14.txt b/misc/requirements/requirements-pyqt-5.14.txt index 7640a8adb..c82acedb0 100644 --- a/misc/requirements/requirements-pyqt-5.14.txt +++ b/misc/requirements/requirements-pyqt-5.14.txt @@ -1,5 +1,5 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py PyQt5==5.14.2 # rq.filter: < 5.15 -PyQt5-sip==12.7.2 +PyQt5-sip==12.8.0 PyQtWebEngine==5.14.0 # rq.filter: < 5.15 diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt index e4ca26a6a..c90606b66 100644 --- a/misc/requirements/requirements-sphinx.txt +++ b/misc/requirements/requirements-sphinx.txt @@ -16,7 +16,7 @@ pytz==2020.1 requests==2.23.0 six==1.15.0 snowballstemmer==2.0.0 -Sphinx==3.0.3 +Sphinx==3.0.4 sphinxcontrib-applehelp==1.0.2 sphinxcontrib-devhelp==1.0.2 sphinxcontrib-htmlhelp==1.0.3 diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 1c0c9acde..9a1b261d0 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -10,11 +10,11 @@ EasyProcess==0.3 Flask==1.1.2 glob2==0.7 hunter==3.1.3 -hypothesis==5.15.1 +hypothesis==5.16.0 itsdangerous==1.1.0 jaraco.functools==3.0.1 ; python_version>="3.6" # Jinja2==2.11.2 -Mako==1.1.2 +Mako==1.1.3 manhole==1.6.0 # MarkupSafe==1.1.1 more-itertools==8.3.0 @@ -37,11 +37,11 @@ pytest-repeat==0.8.0 pytest-rerunfailures==9.0 pytest-travis-fold==1.3.0 pytest-xvfb==1.2.0 -PyVirtualDisplay==0.2.5 +PyVirtualDisplay==0.2.5 # rq.filter: < 1.0 six==1.15.0 sortedcontainers==2.1.0 soupsieve==2.0.1 -vulture==1.4 +vulture==1.5 wcwidth==0.1.9 Werkzeug==1.0.1 jaraco.functools==2.0; python_version<"3.6" # rq.filter: <= 2.0 diff --git a/misc/requirements/requirements-tests.txt-raw b/misc/requirements/requirements-tests.txt-raw index 4d6932c82..d5a20dea3 100644 --- a/misc/requirements/requirements-tests.txt-raw +++ b/misc/requirements/requirements-tests.txt-raw @@ -12,6 +12,8 @@ pytest-mock pytest-qt pytest-rerunfailures pytest-xvfb +# https://github.com/The-Compiler/pytest-xvfb/issues/22 +PyVirtualDisplay < 1.0 ## optional: # To test :debug-trace, gets skipped if hunter is not installed @@ -28,3 +30,4 @@ pytest-repeat #@ markers: jaraco.functools python_version>="3.6" #@ add: jaraco.functools==2.0; python_version<"3.6" # rq.filter: <= 2.0 #@ ignore: Jinja2, MarkupSafe, colorama +#@ filter: PyVirtualDisplay < 1.0 diff --git a/misc/requirements/requirements-vulture.txt b/misc/requirements/requirements-vulture.txt index c5c343f9e..32d36560b 100644 --- a/misc/requirements/requirements-vulture.txt +++ b/misc/requirements/requirements-vulture.txt @@ -1,3 +1,3 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -vulture==1.4 +vulture==1.5 From 41a7db811108fa698687bff4dbb085f6af29018f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 1 Jun 2020 14:10:25 +0200 Subject: [PATCH 100/245] Mark only-active-window test as flaky --- tests/end2end/features/private.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/end2end/features/private.feature b/tests/end2end/features/private.feature index 35097f545..6ea9e7b33 100644 --- a/tests/end2end/features/private.feature +++ b/tests/end2end/features/private.feature @@ -153,7 +153,7 @@ Feature: Using private browsing - url: http://localhost:*/data/numbers/1.txt - url: http://localhost:*/data/numbers/2.txt - + @flaky Scenario: Saving a private session with only-active-window When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab From 4787b30f96c8832797fcc3c56457400d3ebb9893 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 1 Jun 2020 14:43:14 +0200 Subject: [PATCH 101/245] Fix lint --- qutebrowser/misc/backendproblem.py | 4 ++-- qutebrowser/utils/utils.py | 2 +- qutebrowser/utils/version.py | 10 ++++++++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py index 5c8e1002d..6e2bd1866 100644 --- a/qutebrowser/misc/backendproblem.py +++ b/qutebrowser/misc/backendproblem.py @@ -57,7 +57,7 @@ class _Button: text = attr.ib() # type: str setting = attr.ib() # type: str - value = attr.ib() # type: str + value = attr.ib() # type: typing.Any default = attr.ib(default=False) # type: bool @@ -255,7 +255,7 @@ class _BackendProblemChecker: raise utils.Unreachable - def _xwayland_options(self) -> typing.Tuple[typing.List[_Button], str]: + def _xwayland_options(self) -> typing.Tuple[str, typing.List[_Button]]: """Get buttons/text for a possible XWayland solution.""" buttons = [] text = "

You can work around this in one of the following ways:

" diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 39d46add8..ccae5a5d3 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -780,7 +780,7 @@ def ceil_log(number: int, base: int) -> int: return result -def libgl_workaround(): +def libgl_workaround() -> None: """Work around QOpenGLShaderProgram issues, especially for Nvidia. See https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826 diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 8dc4ec593..9c8c8f9e8 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -517,6 +517,16 @@ class OpenGLInfo: @classmethod def parse(cls, *, vendor: str, version: str) -> 'OpenGLInfo': + """Parse OpenGL version info from a string. + + The arguments should be the strings returned by OpenGL for GL_VENDOR + and GL_VERSION, respectively. + + According to the OpenGL reference, the version string should have the + following format: + + .[.] + """ if ' ' not in version: log.misc.warning("Failed to parse OpenGL version (missing space): " "{}".format(version)) From fb6594be27be1da68352f64c4d0550254d9e5378 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 1 Jun 2020 14:47:54 +0200 Subject: [PATCH 102/245] Rename version.version() to version_info() To get a less generic name. --- qutebrowser/app.py | 2 +- qutebrowser/browser/qutescheme.py | 2 +- qutebrowser/misc/crashdialog.py | 4 ++-- qutebrowser/utils/version.py | 4 ++-- tests/unit/utils/test_version.py | 12 ++++++------ 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 6d01e0ddd..9621882e9 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -96,7 +96,7 @@ def run(args): q_app.setApplicationVersion(qutebrowser.__version__) if args.version: - print(version.version()) + print(version.version_info()) sys.exit(usertypes.Exit.ok) quitter.init(args) diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index 53004e66a..ec6e19082 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -302,7 +302,7 @@ def qute_spawn_output(_url: QUrl) -> _HandlerRet: def qute_version(_url): """Handler for qute://version.""" src = jinja.render('version.html', title='Version info', - version=version.version(), + version=version.version_info(), copyright=qutebrowser.__copyright__) return 'text/html', src diff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py index 6bd6ec325..0f6d02712 100644 --- a/qutebrowser/misc/crashdialog.py +++ b/qutebrowser/misc/crashdialog.py @@ -246,7 +246,7 @@ class _CrashDialog(QDialog): except Exception: self._crash_info.append(("Launch time", traceback.format_exc())) try: - self._crash_info.append(("Version info", version.version())) + self._crash_info.append(("Version info", version.version_info())) except Exception: self._crash_info.append(("Version info", traceback.format_exc())) try: @@ -650,7 +650,7 @@ def dump_exception_info(exc, pages, cmdhist, qobjects): print(''.join(traceback.format_exception(*exc)), file=sys.stderr) print("\n---- Version info ----", file=sys.stderr) try: - print(version.version(), file=sys.stderr) + print(version.version_info(), file=sys.stderr) except Exception: traceback.print_exc() print("\n---- Config ----", file=sys.stderr) diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 9c8c8f9e8..c0ff7e1ac 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -412,7 +412,7 @@ def _config_py_loaded() -> str: return "no config.py was loaded" -def version() -> str: +def version_info() -> str: """Return a string with various version information.""" lines = ["qutebrowser v{}".format(qutebrowser.__version__)] gitver = _git_str() @@ -646,5 +646,5 @@ def pastebin_version(pbclient: pastebin.PastebinClient = None) -> None: pbclient.paste(getpass.getuser(), "qute version info {}".format(qutebrowser.__version__), - version(), + version_info(), private=True) diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py index 903585b62..f1e0ee0b6 100644 --- a/tests/unit/utils/test_version.py +++ b/tests/unit/utils/test_version.py @@ -927,8 +927,8 @@ class VersionParams: VersionParams('no-autoconfig-loaded', autoconfig_loaded=False), VersionParams('no-config-py-loaded', config_py_loaded=False), ], ids=lambda param: param.name) -def test_version_output(params, stubs, monkeypatch, config_stub): - """Test version.version().""" +def test_version_info(params, stubs, monkeypatch, config_stub): + """Test version.version_info().""" config.instance.config_py_loaded = params.config_py_loaded import_path = os.path.abspath('/IMPORTPATH') @@ -1048,7 +1048,7 @@ def test_version_output(params, stubs, monkeypatch, config_stub): """.lstrip('\n')) expected = template.rstrip('\n').format(**substitutions) - assert version.version() == expected + assert version.version_info() == expected class TestOpenGLInfo: @@ -1133,7 +1133,7 @@ def pbclient(stubs): def test_pastebin_version(pbclient, message_mock, monkeypatch, qtbot): """Test version.pastebin_version() sets the url.""" - monkeypatch.setattr('qutebrowser.utils.version.version', + monkeypatch.setattr('qutebrowser.utils.version.version_info', lambda: "dummy") monkeypatch.setattr('qutebrowser.utils.utils.log_clipboard', True) @@ -1148,7 +1148,7 @@ def test_pastebin_version(pbclient, message_mock, monkeypatch, qtbot): def test_pastebin_version_twice(pbclient, monkeypatch): """Test whether calling pastebin_version twice sends no data.""" - monkeypatch.setattr('qutebrowser.utils.version.version', + monkeypatch.setattr('qutebrowser.utils.version.version_info', lambda: "dummy") version.pastebin_version(pbclient) @@ -1166,7 +1166,7 @@ def test_pastebin_version_twice(pbclient, monkeypatch): def test_pastebin_version_error(pbclient, caplog, message_mock, monkeypatch): """Test version.pastebin_version() with errors.""" - monkeypatch.setattr('qutebrowser.utils.version.version', + monkeypatch.setattr('qutebrowser.utils.version.version_info', lambda: "dummy") version.pastebin_url = None From 46efdb736f2796687db3ed4c6f02fcc4f00d54a6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 1 Jun 2020 14:54:36 +0200 Subject: [PATCH 103/245] Add test for utils.libgl_workaround() --- qutebrowser/utils/utils.py | 2 +- tests/unit/utils/test_utils.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index ccae5a5d3..92ca34a08 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -789,5 +789,5 @@ def libgl_workaround() -> None: return libgl = ctypes.util.find_library("GL") - if libgl is not None: + if libgl is not None: # pragma: no branch ctypes.CDLL(libgl, mode=ctypes.RTLD_GLOBAL) diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index 18abd444e..35f04201e 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -885,3 +885,10 @@ def test_ceil_log_invalid(number, base): math.log(number, base) with pytest.raises(ValueError): utils.ceil_log(number, base) + + +@pytest.mark.parametrize('skip', [True, False]) +def test_libgl_workaround(monkeypatch, skip): + if skip: + monkeypatch.setenv('QUTE_SKIP_LIBGL_WORKAROUND', '1') + utils.libgl_workaround() # Just make sure it doesn't crash. From 56404bc52c80f1ed01a53770e0ba66e1647fc34c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 1 Jun 2020 14:56:46 +0200 Subject: [PATCH 104/245] AppVeyor: Go back to Qt 5.14 See https://github.com/qutebrowser/qutebrowser/issues/5237#issuecomment-636845641 --- .appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.appveyor.yml b/.appveyor.yml index 23a96055c..47ad9964a 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -11,7 +11,7 @@ image: environment: PYTHONUNBUFFERED: 1 PYTHON: C:\Python38-x64\python.exe - TESTENV: py38-pyqt515 + TESTENV: py38-pyqt514 install: - '%PYTHON% --version' From d7db0a4d37df8c68d48ebb879f03b797549b44ec Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 1 Jun 2020 14:59:34 +0200 Subject: [PATCH 105/245] Revert "AppVeyor: Go back to Qt 5.14" This reverts commit 56404bc52c80f1ed01a53770e0ba66e1647fc34c. --- .appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.appveyor.yml b/.appveyor.yml index 47ad9964a..23a96055c 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -11,7 +11,7 @@ image: environment: PYTHONUNBUFFERED: 1 PYTHON: C:\Python38-x64\python.exe - TESTENV: py38-pyqt514 + TESTENV: py38-pyqt515 install: - '%PYTHON% --version' From fde776de20798904f092f7efec4daafa2885650e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 1 Jun 2020 15:01:14 +0200 Subject: [PATCH 106/245] tests: Ignore new Qt 5.15 Chromium error on AppVeyor See https://github.com/qutebrowser/qutebrowser/issues/5237#issuecomment-636845641 --- tests/end2end/fixtures/quteprocess.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py index 5f8263334..626981f13 100644 --- a/tests/end2end/fixtures/quteprocess.py +++ b/tests/end2end/fixtures/quteprocess.py @@ -285,6 +285,11 @@ def is_ignored_chromium_message(line): # [5306:5324:0417/151739.362362:ERROR:address_tracker_linux.cc(171)] # Could not bind NETLINK socket: Address already in use (98) 'Could not bind NETLINK socket: Address already in use (98)', + + # Qt 5.15 with AppVeyor + # [2968:3108:0601/123442.125:ERROR:mf_helpers.cc(14)] Error in + # dxva_video_decode_accelerator_win.cc on line 517 + 'Error in dxva_video_decode_accelerator_win.cc on line 517', ] return any(testutils.pattern_match(pattern=pattern, value=message) for pattern in ignored_messages) From 0bc5b049b56c03301732dfeee8e3ce3d7dda600b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 1 Jun 2020 15:02:06 +0200 Subject: [PATCH 107/245] tests: Ignore QHttpNetworkConnection warning See https://github.com/qutebrowser/qutebrowser/issues/5390#issuecomment-634062762 --- tests/unit/utils/test_urlutils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/utils/test_urlutils.py b/tests/unit/utils/test_urlutils.py index 39a43479b..a9f32161d 100644 --- a/tests/unit/utils/test_urlutils.py +++ b/tests/unit/utils/test_urlutils.py @@ -716,6 +716,9 @@ class TestProxyFromUrl: def test_proxy_from_url_valid(self, url, expected): assert urlutils.proxy_from_url(QUrl(url)) == expected + @pytest.mark.qt_log_ignore( + r'^QHttpNetworkConnectionPrivate::_q_hostLookupFinished could not ' + r'de-queue request, failed to report HostNotFoundError') @pytest.mark.parametrize('scheme', ['pac+http', 'pac+https']) def test_proxy_from_url_pac(self, scheme, qapp): fetcher = urlutils.proxy_from_url(QUrl('{}://foo'.format(scheme))) From 3df4dd58312262915525d4ca322df19e51119b9e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 1 Jun 2020 14:56:46 +0200 Subject: [PATCH 108/245] AppVeyor: Go back to Qt 5.14 See https://github.com/qutebrowser/qutebrowser/issues/5237#issuecomment-636845641 --- .appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.appveyor.yml b/.appveyor.yml index 23a96055c..47ad9964a 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -11,7 +11,7 @@ image: environment: PYTHONUNBUFFERED: 1 PYTHON: C:\Python38-x64\python.exe - TESTENV: py38-pyqt515 + TESTENV: py38-pyqt514 install: - '%PYTHON% --version' From eaff6e82d13fcf30ec4da06513c08afcae794bb4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 1 Jun 2020 15:57:24 +0200 Subject: [PATCH 109/245] Release v1.12.0 --- .bumpversion.cfg | 2 +- doc/changelog.asciidoc | 2 +- misc/org.qutebrowser.qutebrowser.appdata.xml | 1 + qutebrowser/__init__.py | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index c5defd11a..c260a28da 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.11.1 +current_version = 1.12.0 commit = True message = Release v{new_version} tag = True diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 8d8deaec0..6c4588c53 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -15,7 +15,7 @@ breaking changes (such as renamed commands) can happen in minor releases. // `Fixed` for any bug fixes. // `Security` to invite users to upgrade in case of vulnerabilities. -v1.12.0 (unreleased) +v1.12.0 (2020-06-01) -------------------- Removed diff --git a/misc/org.qutebrowser.qutebrowser.appdata.xml b/misc/org.qutebrowser.qutebrowser.appdata.xml index ca58f4bd5..f02fcb00d 100644 --- a/misc/org.qutebrowser.qutebrowser.appdata.xml +++ b/misc/org.qutebrowser.qutebrowser.appdata.xml @@ -44,6 +44,7 @@ + diff --git a/qutebrowser/__init__.py b/qutebrowser/__init__.py index 6d684d6e3..147606f42 100644 --- a/qutebrowser/__init__.py +++ b/qutebrowser/__init__.py @@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2020 Florian Bruhin (The Compiler)" __license__ = "GPL" __maintainer__ = __author__ __email__ = "mail@qutebrowser.org" -__version__ = "1.11.1" +__version__ = "1.12.0" __version_info__ = tuple(int(part) for part in __version__.split('.')) __description__ = "A keyboard-driven, vim-like browser based on PyQt5." From 804099f5dab951e560d4a187e4360bf3d4b59e36 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 1 Jun 2020 17:35:53 +0200 Subject: [PATCH 110/245] Amend changelog for v1.12.0 --- doc/changelog.asciidoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 6c4588c53..4b70c6e0f 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -48,6 +48,8 @@ Added Changed ~~~~~~~ +- Windows and macOS releases now ship Qt 5.15, which is based on Chromium + 80.0.3987.163 with security fixes up to 81.0.4044.138. - The `content.cookies.accept` setting now accepts URL patterns. - Tests are now included in release tarballs. Note that only running them with the exact dependencies listed in From 83796255edb1bfc3ef70270ac1821900b4526649 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 1 Jun 2020 18:42:50 +0200 Subject: [PATCH 111/245] Don't run test_standarddir.test_fake_mac_config on Windows We can't be sure that os.path.expanduser('~') actually uses $HOME there. Fixes #5477 --- tests/unit/utils/test_standarddir.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/utils/test_standarddir.py b/tests/unit/utils/test_standarddir.py index c2d2c6cd7..064c51b30 100644 --- a/tests/unit/utils/test_standarddir.py +++ b/tests/unit/utils/test_standarddir.py @@ -79,6 +79,7 @@ def test_unset_organization_no_qapp(monkeypatch): @pytest.mark.fake_os('mac') +@pytest.mark.posix def test_fake_mac_config(tmpdir, monkeypatch): """Test standardir.config on a fake Mac.""" monkeypatch.setenv('HOME', str(tmpdir)) From 1b6f708814e4bf4953cf69ea7327194eddaeb703 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 1 Jun 2020 19:40:11 +0200 Subject: [PATCH 112/245] README: Update list of alternatives --- README.asciidoc | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.asciidoc b/README.asciidoc index 41d0e7f1b..3a7778def 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -206,8 +206,8 @@ link:doc/backers.asciidoc[crowdfunding campaigns]! Similar projects ---------------- -Many projects with a similar goal as qutebrowser exist. -Most of them were inspirations for qutebrowser in some way, thanks for that! +Various projects with a similar goal like qutebrowser exist. +Many of them were inspirations for qutebrowser in some way, thanks for that! Active ~~~~~~ @@ -215,8 +215,9 @@ Active * https://fanglingsu.github.io/vimb/[vimb] (C, GTK+ with WebKit2) * https://luakit.github.io/luakit/[luakit] (C/Lua, GTK+ with WebKit2) * https://surf.suckless.org/[surf] (C, GTK+ with WebKit1/WebKit2) -* https://next.atlas.engineer/[next] (Lisp, Emacs-like but also offers Vim bindings, various backends - note there was a http://jgkamat.gitlab.io/blog/next-rce.html[critical remote code execution] which was handled quite badly) +* https://next.atlas.engineer/[next] (Lisp, Emacs-like but also offers Vim bindings, QtWebKit or GTK+/WebKit2 - note there was a http://jgkamat.gitlab.io/blog/next-rce.html[critical remote code execution] which was handled quite badly) * https://github.com/parkouss/webmacs/[webmacs] (Python, Emacs-like with QtWebEngine) +* https://vieb.dev/[Vieb] (JavaScript, Electron) * Chrome/Chromium addons: https://vimium.github.io/[Vimium], * Firefox addons (based on WebExtensions): From bed5647930824e5eab94a51fa7ea8235ec8deae0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 1 Jun 2020 19:42:33 +0200 Subject: [PATCH 113/245] Mark second --only-active-window test as flaky --- tests/end2end/features/sessions.feature | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/end2end/features/sessions.feature b/tests/end2end/features/sessions.feature index 494feb0ba..69c58f3c3 100644 --- a/tests/end2end/features/sessions.feature +++ b/tests/end2end/features/sessions.feature @@ -282,6 +282,7 @@ Feature: Saving and loading sessions Then "Saved session quiet_session." should be logged with level debug And the session quiet_session should exist + @flaky Scenario: Saving session with --only-active-window When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab From 03d1e7712309434d02c02203c13e1b407121bbb7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 1 Jun 2020 20:40:06 +0200 Subject: [PATCH 114/245] changelog: Fix typo and redundancy --- doc/changelog.asciidoc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 4b70c6e0f..5da5ef553 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -36,8 +36,7 @@ Added Previously, that was only available as a separate application via `python3 -m scripts.keytester`. - New `:config-diff` command which opens the `qute://configdiff` page. -- New `--debug-flag log-cookies` to log cokies to the debug log for - debugging. +- New `--debug-flag log-cookies` to log cookies to the debug log. - New `colors.contextmenu.disabled.{fg,bg}` settings to customize colors for disabled items in the context menu. - New line selection mode (`:toggle-selection --line`), bound to `Shift-V` in caret mode. From e8d88eb407b7198c4fe80247505b95ec0b9aa212 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 1 Jun 2020 21:29:10 +0200 Subject: [PATCH 115/245] Update setuptools from 46.4.0 to 47.1.1 --- misc/requirements/requirements-pip.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-pip.txt b/misc/requirements/requirements-pip.txt index 3a71ad0a8..db2eb7a02 100644 --- a/misc/requirements/requirements-pip.txt +++ b/misc/requirements/requirements-pip.txt @@ -3,6 +3,6 @@ appdirs==1.4.4 packaging==20.4 pyparsing==2.4.7 -setuptools==46.4.0 +setuptools==47.1.1 six==1.15.0 wheel==0.34.2 From de5a113e90888cc7b5aee9c1f7874087a8e483e2 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 1 Jun 2020 21:29:11 +0200 Subject: [PATCH 116/245] Update wrapt from 1.11.2 to 1.12.1 --- misc/requirements/requirements-pylint.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index 988b58786..e80ddbf50 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -20,4 +20,4 @@ six==1.15.0 typed-ast==1.4.1 ; python_version<"3.8" uritemplate==3.0.1 urllib3==1.25.9 -wrapt==1.11.2 +wrapt==1.12.1 From 895b6aa24fdccd0a459763688dabca9faafcae8f Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 1 Jun 2020 21:29:12 +0200 Subject: [PATCH 117/245] Update wcwidth from 0.1.9 to 0.2.2 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 9a1b261d0..0c96a2af1 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -42,6 +42,6 @@ six==1.15.0 sortedcontainers==2.1.0 soupsieve==2.0.1 vulture==1.5 -wcwidth==0.1.9 +wcwidth==0.2.2 Werkzeug==1.0.1 jaraco.functools==2.0; python_version<"3.6" # rq.filter: <= 2.0 From b5626c19d624ae372bd8093da3a267807ab0262b Mon Sep 17 00:00:00 2001 From: jcromero Date: Tue, 2 Jun 2020 17:33:35 +0200 Subject: [PATCH 118/245] Fix typo in directory option (-d) for certutil command --- doc/faq.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/faq.asciidoc b/doc/faq.asciidoc index 3df5b8e40..651df9665 100644 --- a/doc/faq.asciidoc +++ b/doc/faq.asciidoc @@ -305,13 +305,13 @@ If you ever need to renew any of these certificates, you can take a look at the currently imported certificates using: + ---- -certutil -D "sql:${HOME}/.pki/nssdb" -L +certutil -d "sql:${HOME}/.pki/nssdb" -L ---- + Then remove the expired certificates using: + ---- -certutil -D "sql:${HOME}/.pki/nssdb" -D -n "My Fancy Certificate Nickname" +certutil -d "sql:${HOME}/.pki/nssdb" -D -n "My Fancy Certificate Nickname" ---- + And then import the new and valid certificates using the procedure From 37e59112a5bb218f6202fef2b579631771b0c7ac Mon Sep 17 00:00:00 2001 From: Julin S <48789920+ju-sh@users.noreply.github.com> Date: Wed, 3 Jun 2020 13:45:08 +0530 Subject: [PATCH 119/245] Use tmpdir and pathlib --- tests/unit/scripts/test_check_coverage.py | 15 ++++----- tests/unit/scripts/test_dictcli.py | 37 ++++++++++++----------- tests/unit/scripts/test_importer.py | 18 ++++------- tests/unit/scripts/test_run_vulture.py | 24 +++++++++------ 4 files changed, 48 insertions(+), 46 deletions(-) diff --git a/tests/unit/scripts/test_check_coverage.py b/tests/unit/scripts/test_check_coverage.py index bb23db512..a60c95525 100644 --- a/tests/unit/scripts/test_check_coverage.py +++ b/tests/unit/scripts/test_check_coverage.py @@ -18,8 +18,9 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . +import os import sys -import os.path +import pathlib import pytest @@ -216,15 +217,15 @@ def test_skipped_non_linux(covtest): def _generate_files(): """Get filenames from WHITELISTED_/PERFECT_FILES.""" for src_file in check_coverage.WHITELISTED_FILES: - yield os.path.join('qutebrowser', src_file) + yield pathlib.Path('qutebrowser') / src_file for test_file, src_file in check_coverage.PERFECT_FILES: if test_file is not None: - yield test_file - yield os.path.join('qutebrowser', src_file) + yield pathlib.Path(test_file) + yield pathlib.Path('qutebrowser') / src_file @pytest.mark.parametrize('filename', list(_generate_files())) def test_files_exist(filename): - basedir = os.path.join(os.path.dirname(check_coverage.__file__), - os.pardir, os.pardir) - assert os.path.exists(os.path.join(basedir, filename)) + basedir = (pathlib.Path(check_coverage.__file__).parent / + os.pardir / os.pardir) + assert (basedir / filename).exists() diff --git a/tests/unit/scripts/test_dictcli.py b/tests/unit/scripts/test_dictcli.py index 8f02974d9..9add389d8 100644 --- a/tests/unit/scripts/test_dictcli.py +++ b/tests/unit/scripts/test_dictcli.py @@ -19,7 +19,8 @@ # along with qutebrowser. If not, see . -import py.path # pylint: disable=no-name-in-module +import pathlib + import pytest from qutebrowser.browser.webengine import spell @@ -59,13 +60,13 @@ def configdata_init(): @pytest.fixture(autouse=True) -def dict_tmpdir(tmpdir, monkeypatch): - monkeypatch.setattr(spell, 'dictionary_dir', lambda: str(tmpdir)) - return tmpdir +def dict_tmp_path(tmp_path, monkeypatch): + monkeypatch.setattr(spell, 'dictionary_dir', lambda: str(tmp_path)) + return tmp_path -def test_language(dict_tmpdir): - (dict_tmpdir / 'pl-PL-2-0.bdic').ensure() +def test_language(dict_tmp_path): + (dict_tmp_path / 'pl-PL-2-0.bdic').touch() assert english().local_filename is None assert polish() @@ -82,9 +83,9 @@ def test_latest_yet(): assert dictcli.latest_yet(code2file, 'en-US', 'en-US-8-0.bdic') -def test_available_languages(dict_tmpdir, monkeypatch): +def test_available_languages(dict_tmp_path, monkeypatch): for f in ['pl-PL-2-0.bdic', english().remote_filename]: - (dict_tmpdir / f).ensure() + (dict_tmp_path / f).touch() monkeypatch.setattr(dictcli, 'language_list_from_api', lambda: [ (lang.code, lang.remote_filename) for lang in langs() ]) @@ -118,27 +119,27 @@ def test_filter_languages(): dictcli.filter_languages(langs(), ['pl-PL', 'en-GB']) -def test_install(dict_tmpdir, monkeypatch): +def test_install(dict_tmp_path, monkeypatch): # given monkeypatch.setattr( dictcli, 'download_dictionary', - lambda _url, dest: py.path.local(dest).ensure()) # pylint: disable=no-member + lambda _url, dest: pathlib.Path(dest).touch()) # when dictcli.install(langs()) # then - installed_files = [f.basename for f in dict_tmpdir.listdir()] + installed_files = [f.name for f in dict_tmp_path.glob('*')] expected_files = [lang.remote_filename for lang in langs()] assert sorted(installed_files) == sorted(expected_files) -def test_update(dict_tmpdir, monkeypatch): +def test_update(dict_tmp_path, monkeypatch): # given monkeypatch.setattr( dictcli, 'download_dictionary', - lambda _url, dest: py.path.local(dest).ensure()) # pylint: disable=no-member - (dict_tmpdir / 'pl-PL-2-0.bdic').ensure() + lambda _url, dest: pathlib.Path(dest).touch()) + (dict_tmp_path / 'pl-PL-2-0.bdic').touch() assert polish().local_version < polish().remote_version # when @@ -148,20 +149,20 @@ def test_update(dict_tmpdir, monkeypatch): assert polish().local_version == polish().remote_version -def test_remove_old(dict_tmpdir, monkeypatch): +def test_remove_old(dict_tmp_path, monkeypatch): # given monkeypatch.setattr( dictcli, 'download_dictionary', - lambda _url, dest: py.path.local(dest).ensure()) # pylint: disable=no-member + lambda _url, dest: pathlib.Path(dest).touch()) for f in ['pl-PL-2-0.bdic', polish().remote_filename, english().remote_filename]: - (dict_tmpdir / f).ensure() + (dict_tmp_path / f).touch() # when dictcli.remove_old(langs()) # then - installed_files = [f.basename for f in dict_tmpdir.listdir()] + installed_files = [f.name for f in dict_tmp_path.glob('*')] expected_files = [polish().remote_filename, english().remote_filename] assert sorted(installed_files) == sorted(expected_files) diff --git a/tests/unit/scripts/test_importer.py b/tests/unit/scripts/test_importer.py index 950987afc..4a70ae63e 100644 --- a/tests/unit/scripts/test_importer.py +++ b/tests/unit/scripts/test_importer.py @@ -18,37 +18,31 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -import os +import pathlib import pytest from scripts import importer -_samples = 'tests/unit/scripts/importer_sample' +_samples = pathlib.Path('tests/unit/scripts/importer_sample') def qm_expected(input_format): """Read expected quickmark-formatted output.""" - with open(os.path.join(_samples, input_format, 'quickmarks'), - 'r', encoding='utf-8') as f: - return f.read() + return (_samples / input_format / 'quickmarks').read_text(encoding='utf-8') def bm_expected(input_format): """Read expected bookmark-formatted output.""" - with open(os.path.join(_samples, input_format, 'bookmarks'), - 'r', encoding='utf-8') as f: - return f.read() + return (_samples / input_format / 'bookmarks').read_text(encoding='utf-8') def search_expected(input_format): """Read expected search-formatted (config.py) output.""" - with open(os.path.join(_samples, input_format, 'config_py'), - 'r', encoding='utf-8') as f: - return f.read() + return (_samples / input_format / 'config_py').read_text(encoding='utf-8') def sample_input(input_format): """Get the sample input path.""" - return os.path.join(_samples, input_format, 'input') + return str(_samples / input_format / 'input') def test_opensearch_convert(): diff --git a/tests/unit/scripts/test_run_vulture.py b/tests/unit/scripts/test_run_vulture.py index 25630b9fc..0008f3147 100644 --- a/tests/unit/scripts/test_run_vulture.py +++ b/tests/unit/scripts/test_run_vulture.py @@ -18,6 +18,8 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . +import os +import pathlib import sys import textwrap @@ -41,29 +43,33 @@ class VultureDir: """Fixture similar to pytest's testdir fixture for vulture. Attributes: - _tmpdir: The pytest tmpdir fixture. + _tmp_path: The pytest tmp_path fixture. """ - def __init__(self, tmpdir): - self._tmpdir = tmpdir + def __init__(self, tmp_path): + self._tmp_path = tmp_path def run(self): """Run vulture over all generated files and return the output.""" - files = self._tmpdir.listdir() + #files = self._tmp_path.listdir() + files = list(self._tmp_path.glob('*')) assert files - with self._tmpdir.as_cwd(): - return run_vulture.run([str(e.basename) for e in files]) + old_cwd = pathlib.Path.cwd() + os.chdir(str(self._tmp_path)) + return_value = run_vulture.run([e.name for e in files]) + os.chdir(str(old_cwd)) + return return_value def makepyfile(self, **kwargs): """Create a python file, similar to TestDir.makepyfile.""" for filename, data in kwargs.items(): text = textwrap.dedent(data) - (self._tmpdir / filename + '.py').write_text(text, 'utf-8') + (self._tmp_path / (filename + '.py')).write_text(text, 'utf-8') @pytest.fixture -def vultdir(tmpdir): - return VultureDir(tmpdir) +def vultdir(tmp_path): + return VultureDir(tmp_path) def test_used(vultdir): From 06bad5beb3489c55050a63b41baaeae712e28cea Mon Sep 17 00:00:00 2001 From: Julin S <48789920+ju-sh@users.noreply.github.com> Date: Wed, 3 Jun 2020 17:52:40 +0530 Subject: [PATCH 120/245] use tmp_path and pathlib for tests/unit/misc --- tests/unit/misc/test_editor.py | 56 +++++++++++----------- tests/unit/misc/test_ipc.py | 25 +++++----- tests/unit/misc/test_lineparser.py | 31 ++++++------ tests/unit/misc/test_sessions.py | 77 +++++++++++++++--------------- 4 files changed, 97 insertions(+), 92 deletions(-) diff --git a/tests/unit/misc/test_editor.py b/tests/unit/misc/test_editor.py index 5fb4478a7..96dd558ff 100644 --- a/tests/unit/misc/test_editor.py +++ b/tests/unit/misc/test_editor.py @@ -20,8 +20,8 @@ """Tests for qutebrowser.misc.editor.""" import time +import pathlib import os -import os.path import logging from PyQt5.QtCore import QProcess @@ -76,16 +76,16 @@ class TestFileHandling: def test_ok(self, editor): """Test file handling when closing with an exit status == 0.""" editor.edit("") - filename = editor._filename - assert os.path.exists(filename) - assert os.path.basename(filename).startswith('qutebrowser-editor-') + filename = pathlib.Path(editor._filename) + assert filename.exists() + assert filename.name.startswith('qutebrowser-editor-') editor._proc.finished.emit(0, QProcess.NormalExit) - assert not os.path.exists(filename) + assert not filename.exists() - def test_existing_file(self, editor, tmpdir): + def test_existing_file(self, editor, tmp_path): """Test editing an existing file.""" - path = tmpdir / 'foo.txt' - path.ensure() + path = tmp_path / 'foo.txt' + path.touch() editor.edit_file(str(path)) editor._proc.finished.emit(0, QProcess.NormalExit) @@ -95,62 +95,62 @@ class TestFileHandling: def test_error(self, editor): """Test file handling when closing with an exit status != 0.""" editor.edit("") - filename = editor._filename - assert os.path.exists(filename) + filename = pathlib.Path(editor._filename) + assert filename.exists() editor._proc._proc.exitStatus = lambda: QProcess.CrashExit editor._proc.finished.emit(1, QProcess.NormalExit) - assert os.path.exists(filename) + assert filename.exists() - os.remove(filename) + filename.unlink() def test_crash(self, editor): """Test file handling when closing with a crash.""" editor.edit("") - filename = editor._filename - assert os.path.exists(filename) + filename = pathlib.Path(editor._filename) + assert filename.exists() editor._proc._proc.exitStatus = lambda: QProcess.CrashExit editor._proc.error.emit(QProcess.Crashed) editor._proc.finished.emit(0, QProcess.CrashExit) - assert os.path.exists(filename) + assert filename.exists() - os.remove(filename) + filename.unlink() def test_unreadable(self, message_mock, editor, caplog, qtbot): """Test file handling when closing with an unreadable file.""" editor.edit("") - filename = editor._filename - assert os.path.exists(filename) - os.chmod(filename, 0o277) - if os.access(filename, os.R_OK): + filename = pathlib.Path(editor._filename) + assert filename.exists() + filename.chmod(0o277) + if os.access(str(filename), os.R_OK): # Docker container or similar pytest.skip("File was still readable") with caplog.at_level(logging.ERROR): editor._proc.finished.emit(0, QProcess.NormalExit) - assert not os.path.exists(filename) + assert not filename.exists() msg = message_mock.getmsg(usertypes.MessageLevel.error) assert msg.text.startswith("Failed to read back edited file: ") @pytest.fixture - def unwritable_tmpdir(self, tmpdir): - tmpdir.chmod(0) - if os.access(str(tmpdir), os.W_OK): + def unwritable_tmp_path(self, tmp_path): + tmp_path.chmod(0) + if os.access(str(tmp_path), os.W_OK): # Docker container or similar pytest.skip("File was still writable") - yield tmpdir + yield tmp_path - tmpdir.chmod(0o755) + tmp_path.chmod(0o755) def test_unwritable(self, monkeypatch, message_mock, editor, - unwritable_tmpdir, caplog): + unwritable_tmp_path, caplog): """Test file handling when the initial file is not writable.""" monkeypatch.setattr(editormod.tempfile, 'tempdir', - str(unwritable_tmpdir)) + str(unwritable_tmp_path)) with caplog.at_level(logging.ERROR): editor.edit("") diff --git a/tests/unit/misc/test_ipc.py b/tests/unit/misc/test_ipc.py index 4b94162c6..1da88d3bb 100644 --- a/tests/unit/misc/test_ipc.py +++ b/tests/unit/misc/test_ipc.py @@ -20,6 +20,7 @@ """Tests for qutebrowser.misc.ipc.""" import os +import pathlib import getpass import logging import json @@ -297,10 +298,10 @@ class TestListen: def test_permissions_posix(self, ipc_server): ipc_server.listen() sockfile = ipc_server._server.fullServerName() - sockdir = os.path.dirname(sockfile) + sockdir = pathlib.Path(sockfile).parent file_stat = os.stat(sockfile) - dir_stat = os.stat(sockdir) + dir_stat = sockdir.stat() # pylint: disable=no-member,useless-suppression file_owner_ok = file_stat.st_uid == os.getuid() @@ -504,7 +505,7 @@ class TestSendToRunningInstance: @pytest.mark.parametrize('has_cwd', [True, False]) @pytest.mark.linux(reason="Causes random trouble on Windows and macOS") - def test_normal(self, qtbot, tmpdir, ipc_server, mocker, has_cwd): + def test_normal(self, qtbot, tmp_path, ipc_server, mocker, has_cwd): ipc_server.listen() with qtbot.assertNotEmitted(ipc_server.got_invalid_data): @@ -512,16 +513,18 @@ class TestSendToRunningInstance: timeout=5000) as blocker: with qtbot.waitSignal(ipc_server.got_raw, timeout=5000) as raw_blocker: - with tmpdir.as_cwd(): - if not has_cwd: - m = mocker.patch('qutebrowser.misc.ipc.os') - m.getcwd.side_effect = OSError - sent = ipc.send_to_running_instance( - 'qute-test', ['foo'], None) + old_cwd = pathlib.Path.cwd() + os.chdir(str(tmp_path)) + if not has_cwd: + m = mocker.patch('qutebrowser.misc.ipc.os') + m.getcwd.side_effect = OSError + sent = ipc.send_to_running_instance( + 'qute-test', ['foo'], None) + os.chdir(str(old_cwd)) assert sent - expected_cwd = str(tmpdir) if has_cwd else '' + expected_cwd = str(tmp_path) if has_cwd else '' assert blocker.args == [['foo'], '', expected_cwd] @@ -529,7 +532,7 @@ class TestSendToRunningInstance: 'version': qutebrowser.__version__, 'protocol_version': ipc.PROTOCOL_VERSION} if has_cwd: - raw_expected['cwd'] = str(tmpdir) + raw_expected['cwd'] = str(tmp_path) assert len(raw_blocker.args) == 1 parsed = json.loads(raw_blocker.args[0].decode('utf-8')) diff --git a/tests/unit/misc/test_lineparser.py b/tests/unit/misc/test_lineparser.py index 9ddeaa93e..cdb16d04a 100644 --- a/tests/unit/misc/test_lineparser.py +++ b/tests/unit/misc/test_lineparser.py @@ -19,7 +19,7 @@ """Tests for qutebrowser.misc.lineparser.""" -import os +import pathlib from unittest import mock import pytest @@ -66,7 +66,7 @@ class TestBaseLineParser: lineparser._write(f, [testdata]) open_mock.assert_called_once_with( - os.path.join(self.CONFDIR, self.FILENAME), 'rb') + str(pathlib.Path(self.CONFDIR) / self.FILENAME), 'rb') open_mock().write.assert_has_calls([ mock.call(testdata), @@ -77,30 +77,31 @@ class TestBaseLineParser: class TestLineParser: @pytest.fixture - def lineparser(self, tmpdir): + def lineparser(self, tmp_path): """Fixture to get a LineParser for tests.""" - lp = lineparsermod.LineParser(str(tmpdir), 'file') + lp = lineparsermod.LineParser(str(tmp_path), 'file') lp.save() return lp - def test_init(self, tmpdir): + def test_init(self, tmp_path): """Test if creating a line parser correctly reads its file.""" - (tmpdir / 'file').write('one\ntwo\n') - lineparser = lineparsermod.LineParser(str(tmpdir), 'file') + (tmp_path / 'file').write_text('one\ntwo\n') + lineparser = lineparsermod.LineParser(str(tmp_path), 'file') assert lineparser.data == ['one', 'two'] - (tmpdir / 'file').write_binary(b'\xfe\n\xff\n') - lineparser = lineparsermod.LineParser(str(tmpdir), 'file', binary=True) + (tmp_path / 'file').write_bytes(b'\xfe\n\xff\n') + lineparser = lineparsermod.LineParser(str(tmp_path), 'file', + binary=True) assert lineparser.data == [b'\xfe', b'\xff'] - def test_clear(self, tmpdir, lineparser): + def test_clear(self, tmp_path, lineparser): """Test if clear() empties its file.""" lineparser.data = ['one', 'two'] lineparser.save() - assert (tmpdir / 'file').read() == 'one\ntwo\n' + assert (tmp_path / 'file').read_text() == 'one\ntwo\n' lineparser.clear() assert not lineparser.data - assert (tmpdir / 'file').read() == '' + assert (tmp_path / 'file').read_text() == '' def test_double_open(self, lineparser): """Test if save() bails on an already open file.""" @@ -109,10 +110,10 @@ class TestLineParser: match="Refusing to double-open LineParser."): lineparser.save() - def test_prepare_save(self, tmpdir, lineparser): + def test_prepare_save(self, tmp_path, lineparser): """Test if save() bails when _prepare_save() returns False.""" - (tmpdir / 'file').write('pristine\n') + (tmp_path / 'file').write_text('pristine\n') lineparser.data = ['changed'] lineparser._prepare_save = lambda: False lineparser.save() - assert (tmpdir / 'file').read() == 'pristine\n' + assert (tmp_path / 'file').read_text() == 'pristine\n' diff --git a/tests/unit/misc/test_sessions.py b/tests/unit/misc/test_sessions.py index 4aec19dc5..e052751b5 100644 --- a/tests/unit/misc/test_sessions.py +++ b/tests/unit/misc/test_sessions.py @@ -40,9 +40,9 @@ webengine_refactoring_xfail = pytest.mark.xfail( @pytest.fixture -def sess_man(tmpdir): +def sess_man(tmp_path): """Fixture providing a SessionManager.""" - return sessions.SessionManager(base_path=str(tmpdir)) + return sessions.SessionManager(base_path=str(tmp_path)) class TestInit: @@ -57,11 +57,12 @@ class TestInit: pass @pytest.mark.parametrize('create_dir', [True, False]) - def test_with_standarddir(self, tmpdir, monkeypatch, create_dir): - monkeypatch.setattr(sessions.standarddir, 'data', lambda: str(tmpdir)) - session_dir = tmpdir / 'sessions' + def test_with_standarddir(self, tmp_path, monkeypatch, create_dir): + monkeypatch.setattr(sessions.standarddir, 'data', + lambda: str(tmp_path)) + session_dir = tmp_path / 'sessions' if create_dir: - session_dir.ensure(dir=True) + session_dir.mkdir() sessions.init() @@ -76,14 +77,14 @@ def test_did_not_load(sess_man): class TestExists: @pytest.mark.parametrize('absolute', [True, False]) - def test_existent(self, tmpdir, absolute): - session_dir = tmpdir / 'sessions' - abs_session = tmpdir / 'foo.yml' + def test_existent(self, tmp_path, absolute): + session_dir = tmp_path / 'sessions' + abs_session = tmp_path / 'foo.yml' rel_session = session_dir / 'foo.yml' - session_dir.ensure(dir=True) - abs_session.ensure() - rel_session.ensure() + session_dir.mkdir() + abs_session.touch() + rel_session.touch() man = sessions.SessionManager(str(session_dir)) @@ -95,11 +96,11 @@ class TestExists: assert man.exists(name) @pytest.mark.parametrize('absolute', [True, False]) - def test_inexistent(self, tmpdir, absolute): - man = sessions.SessionManager(str(tmpdir)) + def test_inexistent(self, tmp_path, absolute): + man = sessions.SessionManager(str(tmp_path)) if absolute: - name = str(tmpdir / 'foo') + name = str(tmp_path / 'foo') else: name = 'foo' @@ -208,13 +209,13 @@ class TestSave: objreg.delete('main-window', scope='window', window=0) objreg.delete('tabbed-browser', scope='window', window=0) - def test_no_state_config(self, sess_man, tmpdir, state_config): - session_path = tmpdir / 'foo.yml' + def test_no_state_config(self, sess_man, tmp_path, state_config): + session_path = tmp_path / 'foo.yml' sess_man.save(str(session_path)) assert 'session' not in state_config['general'] - def test_last_window_session_none(self, caplog, sess_man, tmpdir): - session_path = tmpdir / 'foo.yml' + def test_last_window_session_none(self, caplog, sess_man, tmp_path): + session_path = tmp_path / 'foo.yml' with caplog.at_level(logging.ERROR): sess_man.save(str(session_path), last_window=True) @@ -222,9 +223,9 @@ class TestSave: assert caplog.messages == [msg] assert not session_path.exists() - def test_last_window_session(self, sess_man, tmpdir): + def test_last_window_session(self, sess_man, tmp_path): sess_man.save_last_window_session() - session_path = tmpdir / 'foo.yml' + session_path = tmp_path / 'foo.yml' sess_man.save(str(session_path), last_window=True) data = session_path.read_text('utf-8') assert data == 'windows: []\n' @@ -232,24 +233,24 @@ class TestSave: @pytest.mark.parametrize('exception', [ OSError('foo'), UnicodeEncodeError('ascii', '', 0, 2, 'foo'), yaml.YAMLError('foo')]) - def test_fake_exception(self, mocker, sess_man, tmpdir, exception): + def test_fake_exception(self, mocker, sess_man, tmp_path, exception): mocker.patch('qutebrowser.misc.sessions.yaml.dump', side_effect=exception) with pytest.raises(sessions.SessionError, match=str(exception)): - sess_man.save(str(tmpdir / 'foo.yml')) + sess_man.save(str(tmp_path / 'foo.yml')) - assert not tmpdir.listdir() + assert not list(tmp_path.glob('*')) - def test_load_next_time(self, tmpdir, state_config, sess_man): - session_path = tmpdir / 'foo.yml' + def test_load_next_time(self, tmp_path, state_config, sess_man): + session_path = tmp_path / 'foo.yml' sess_man.save(str(session_path), load_next_time=True) assert state_config['general']['session'] == str(session_path) @webengine_refactoring_xfail - def test_utf_8_invalid(self, tmpdir, sess_man, fake_history): + def test_utf_8_invalid(self, tmp_path, sess_man, fake_history): """Make sure data containing invalid UTF8 raises SessionError.""" - session_path = tmpdir / 'foo.yml' + session_path = tmp_path / 'foo.yml' fake_history([Item(QUrl('http://www.qutebrowser.org/'), '\ud800', active=True)]) @@ -356,18 +357,18 @@ class TestLoadTab: class TestListSessions: - def test_no_sessions(self, tmpdir): - sess_man = sessions.SessionManager(str(tmpdir)) + def test_no_sessions(self, tmp_path): + sess_man = sessions.SessionManager(str(tmp_path)) assert not sess_man.list_sessions() - def test_with_sessions(self, tmpdir): - (tmpdir / 'foo.yml').ensure() - (tmpdir / 'bar.yml').ensure() - sess_man = sessions.SessionManager(str(tmpdir)) + def test_with_sessions(self, tmp_path): + (tmp_path / 'foo.yml').touch() + (tmp_path / 'bar.yml').touch() + sess_man = sessions.SessionManager(str(tmp_path)) assert sess_man.list_sessions() == ['bar', 'foo'] - def test_with_other_files(self, tmpdir): - (tmpdir / 'foo.yml').ensure() - (tmpdir / 'bar.html').ensure() - sess_man = sessions.SessionManager(str(tmpdir)) + def test_with_other_files(self, tmp_path): + (tmp_path / 'foo.yml').touch() + (tmp_path / 'bar.html').touch() + sess_man = sessions.SessionManager(str(tmp_path)) assert sess_man.list_sessions() == ['foo'] From 1606246dc106047ceb5cba6d1e9d464402a8a80f Mon Sep 17 00:00:00 2001 From: "J. Nathanael Philipp" Date: Thu, 4 Jun 2020 09:27:02 +0200 Subject: [PATCH 121/245] Add cmd argument for control port. --- misc/userscripts/tor_identity | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/misc/userscripts/tor_identity b/misc/userscripts/tor_identity index 93b6d4136..1631a0b94 100755 --- a/misc/userscripts/tor_identity +++ b/misc/userscripts/tor_identity @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright 2018 jnphilipp +# Copyright 2018-2020 J. Nathanael Philipp (jnphilipp) # # This file is part of qutebrowser. # @@ -30,6 +30,8 @@ import os import sys +from argparse import ArgumentParser + try: from stem import Signal from stem.control import Controller @@ -41,12 +43,19 @@ except ImportError: print('Failed to import stem.') -password = sys.argv[1] -with Controller.from_port(port=9051) as controller: - controller.authenticate(password) - controller.signal(Signal.NEWNYM) - if os.getenv('QUTE_FIFO'): - with open(os.environ['QUTE_FIFO'], 'w') as f: - f.write('message-info "Tor identity changed."') - else: - print('Tor identity changed.') +if __name__ == '__main__': + parser = ArgumentParser(prog='tor_identity') + parser.add_argument('-c', '--control-port', default=9051, + help='Tor control port (default 9051).') + parser.add_argument('-p', '--password', type=str, default=None, + help='Tor control port password.') + args = parser.parse_args() + + with Controller.from_port(port=args.control_port) as controller: + controller.authenticate(args.password) + controller.signal(Signal.NEWNYM) + if os.getenv('QUTE_FIFO'): + with open(os.environ['QUTE_FIFO'], 'w') as f: + f.write('message-info "Tor identity changed."') + else: + print('Tor identity changed.') From 7507825a174a9fddae35bb12794ff397940a7881 Mon Sep 17 00:00:00 2001 From: Nicholas Lantz Date: Sun, 7 Jun 2020 15:02:47 -0600 Subject: [PATCH 122/245] Add setting to disable back and forward buttons on mouse. Resolves #5239 --- doc/help/settings.asciidoc | 9 +++++++++ qutebrowser/browser/eventfilter.py | 5 +++++ qutebrowser/config/configdata.yml | 5 +++++ 3 files changed, 19 insertions(+) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index cc020ea88..ea0d6eb5f 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -251,6 +251,7 @@ |<>|Leave insert mode when starting a new page load. |<>|Switch to insert mode when clicking flash and other plugins. |<>|Include hyperlinks in the keyboard focus chain when tabbing. +|<>|Enable back and forward buttons on the mouse. |<>|Timeout (in milliseconds) for partially typed key bindings. |<>|Enable Opera-like mouse rocker gestures. |<>|Enable spatial navigation. @@ -3194,6 +3195,14 @@ Type: <> Default: +pass:[true]+ +[[input.mouse_backforward]] +=== input.mouse_backforward +Enable back and forward buttons on the mouse. + +Type: <> + +Default: +pass:[true]+ + [[input.partial_timeout]] === input.partial_timeout Timeout (in milliseconds) for partially typed key bindings. diff --git a/qutebrowser/browser/eventfilter.py b/qutebrowser/browser/eventfilter.py index f6901003e..d78b608a5 100644 --- a/qutebrowser/browser/eventfilter.py +++ b/qutebrowser/browser/eventfilter.py @@ -276,6 +276,11 @@ class TabEventFilter(QObject): Return: True if the event should be filtered, False otherwise. """ + if (not config.val.input.mouse_backforward and + e.button() in [Qt.XButton1, Qt.XButton2]): + # Back and forward on mice are disabled + return + if e.button() in [Qt.XButton1, Qt.LeftButton]: # Back button on mice which have it, or rocker gesture if self._tab.history.can_go_back(): diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 664f9facf..ca4506621 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -1399,6 +1399,11 @@ input.rocker_gestures: This disables the context menu. +input.mouse_backforward: + default: true + type: Bool + desc: Enable back and forward buttons on the mouse. + input.spatial_navigation: default: false type: Bool From 9a1f78fc763b79792600408a09806608ab93d079 Mon Sep 17 00:00:00 2001 From: Constantine Theocharis Date: Mon, 8 Jun 2020 09:56:18 +0100 Subject: [PATCH 123/245] Add smarthide property to hide status only on normal mode --- qutebrowser/config/configdata.yml | 7 +++++++ qutebrowser/mainwindow/statusbar/bar.py | 20 ++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 664f9facf..b8af6a9a5 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -1564,6 +1564,13 @@ statusbar.hide: default: false desc: Hide the statusbar unless a message is shown. +statusbar.smarthide: + type: Bool + default: false + desc: >- + If statusbar.hide is true, only hide the statusbar in normal mode, and show + it in other modes. + statusbar.padding: type: Padding default: diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 91bdb0b6e..ca211a200 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -254,10 +254,22 @@ class StatusBar(QWidget): @pyqtSlot() def maybe_hide(self): """Hide the statusbar if it's configured to do so.""" - tab = self._current_tab() hide = config.val.statusbar.hide - if hide or (tab is not None and tab.data.fullscreen): + smarthide = config.val.statusbar.smarthide + tab = self._current_tab() + # smarthide is done on mode change. + if tab is not None and tab.data.fullscreen: self.hide() + elif hide and not smarthide: + self.hide() + elif hide and smarthide: + try: + mode_manager = modeman.instance(self._win_id) + if mode_manager.mode == usertypes.KeyMode.normal: + self.hide() + except KeyError: + # If modeman hasn't been initialised, hide the bar. + self.hide() else: self.show() @@ -336,6 +348,8 @@ class StatusBar(QWidget): def on_mode_entered(self, mode): """Mark certain modes in the commandline.""" mode_manager = modeman.instance(self._win_id) + if config.val.statusbar.smarthide and config.val.statusbar.hide: + self.show() if mode_manager.parsers[mode].passthrough: self._set_mode_text(mode.name) if mode in [usertypes.KeyMode.insert, @@ -350,6 +364,8 @@ class StatusBar(QWidget): def on_mode_left(self, old_mode, new_mode): """Clear marked mode.""" mode_manager = modeman.instance(self._win_id) + if config.val.statusbar.smarthide and config.val.statusbar.hide: + self.hide() if mode_manager.parsers[old_mode].passthrough: if mode_manager.parsers[new_mode].passthrough: self._set_mode_text(new_mode.name) From c3978ac74927397e2b45e33ead5aa5020315c633 Mon Sep 17 00:00:00 2001 From: Constantine Theocharis Date: Mon, 8 Jun 2020 11:07:15 +0100 Subject: [PATCH 124/245] Make Travis happy --- qutebrowser/mainwindow/statusbar/bar.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index ca211a200..cf9e6aa45 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -257,7 +257,6 @@ class StatusBar(QWidget): hide = config.val.statusbar.hide smarthide = config.val.statusbar.smarthide tab = self._current_tab() - # smarthide is done on mode change. if tab is not None and tab.data.fullscreen: self.hide() elif hide and not smarthide: @@ -268,7 +267,7 @@ class StatusBar(QWidget): if mode_manager.mode == usertypes.KeyMode.normal: self.hide() except KeyError: - # If modeman hasn't been initialised, hide the bar. + # If modeman hasn't been initialized, hide the bar. self.hide() else: self.show() From e19c133e1dc790905aafa52e92bca235d2a2ce31 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 8 Jun 2020 15:27:48 +0200 Subject: [PATCH 125/245] Fix -1 focus_stack_size in TabDeque --- qutebrowser/mainwindow/tabbedbrowser.py | 5 +++- tests/unit/mainwindow/test_tabbedbrowser.py | 32 +++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 tests/unit/mainwindow/test_tabbedbrowser.py diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 25b05a036..f9112c6ab 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -64,8 +64,11 @@ class TabDeque: """ def __init__(self) -> None: + size = config.val.tabs.focus_stack_size + if size < 0: + size = None self._stack = collections.deque( - maxlen=config.val.tabs.focus_stack_size + maxlen=size ) # type: typing.Deque[weakref.ReferenceType[QWidget]] # Items that have been removed from the primary stack. self._stack_deleted = [ diff --git a/tests/unit/mainwindow/test_tabbedbrowser.py b/tests/unit/mainwindow/test_tabbedbrowser.py new file mode 100644 index 000000000..a0f772cf9 --- /dev/null +++ b/tests/unit/mainwindow/test_tabbedbrowser.py @@ -0,0 +1,32 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2020 Florian Bruhin (The Compiler) +# +# 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 . + + +import pytest + +from qutebrowser.mainwindow import tabbedbrowser + + +class TestTabDeque: + + @pytest.mark.parametrize('size', [-1, 5]) + def test_size_handling(self, size, config_stub): + config_stub.val.tabs.focus_stack_size = size + dq = tabbedbrowser.TabDeque() + dq.update_size() From eb8681556ff8a4da06002eb455ab3556df3d8cf9 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 8 Jun 2020 17:12:48 +0200 Subject: [PATCH 126/245] pdfjs: Move _SYSTEM_PATHS to module-level This makes it possible to patch them for tests. --- qutebrowser/browser/pdfjs.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/qutebrowser/browser/pdfjs.py b/qutebrowser/browser/pdfjs.py index cca21abcb..af3b368cd 100644 --- a/qutebrowser/browser/pdfjs.py +++ b/qutebrowser/browser/pdfjs.py @@ -30,6 +30,19 @@ from qutebrowser.misc import objects from qutebrowser.config import config +_SYSTEM_PATHS = [ + # Debian pdf.js-common + # Arch Linux pdfjs (AUR) + '/usr/share/pdf.js/', + # Flatpak (Flathub) + '/app/share/pdf.js/', + # Arch Linux pdf.js (AUR) + '/usr/share/javascript/pdf.js/', + # Debian libjs-pdf + '/usr/share/javascript/pdf/', +] + + class PDFJSNotFound(Exception): """Raised when no pdf.js installation is found. @@ -130,16 +143,7 @@ def get_pdfjs_res_and_path(path): content = None file_path = None - system_paths = [ - # Debian pdf.js-common - # Arch Linux pdfjs (AUR) - '/usr/share/pdf.js/', - # Flatpak (Flathub) - '/app/share/pdf.js/', - # Arch Linux pdf.js (AUR) - '/usr/share/javascript/pdf.js/', - # Debian libjs-pdf - '/usr/share/javascript/pdf/', + system_paths = _SYSTEM_PATHS + [ # fallback os.path.join(standarddir.data(), 'pdfjs'), # hardcoded fallback for --temp-basedir From 1303d92c29c19dd42a3747c48cde5f69447cc9fa Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 8 Jun 2020 17:01:06 +0200 Subject: [PATCH 127/245] pdfjs: Also check for web/viewer.html in is_available() If a build/pdf.js file exists, but web/viewer.html does not, we would run into a PDFJSNotFound exception inside of generate_pdfjs_page(). --- qutebrowser/browser/pdfjs.py | 1 + tests/unit/browser/test_pdfjs.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/qutebrowser/browser/pdfjs.py b/qutebrowser/browser/pdfjs.py index af3b368cd..0c6d02501 100644 --- a/qutebrowser/browser/pdfjs.py +++ b/qutebrowser/browser/pdfjs.py @@ -228,6 +228,7 @@ def is_available(): """Return true if a pdfjs installation is available.""" try: get_pdfjs_res('build/pdf.js') + get_pdfjs_res('web/viewer.html') except PDFJSNotFound: return False else: diff --git a/tests/unit/browser/test_pdfjs.py b/tests/unit/browser/test_pdfjs.py index e95f665c7..d05ff1fc0 100644 --- a/tests/unit/browser/test_pdfjs.py +++ b/tests/unit/browser/test_pdfjs.py @@ -52,6 +52,15 @@ def test_generate_pdfjs_page(available, snippet, monkeypatch): assert snippet in content +def test_broken_installation(data_tmpdir, monkeypatch): + """Make sure we don't crash with a broken local installation.""" + monkeypatch.setattr(pdfjs, '_SYSTEM_PATHS', []) + (data_tmpdir / 'pdfjs' / 'pdf.js').ensure() # But no viewer.html + + content = pdfjs.generate_pdfjs_page('example.pdf', QUrl()) + assert '

No pdf.js installation found

' in content + + # Note that we got double protection, once because we use QUrl.FullyEncoded and # because we use qutebrowser.utils.javascript.to_js. Characters like " are # already replaced by QUrl. From e58ba17dd2df200a322b97075af6171f8b17688c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 8 Jun 2020 15:28:21 +0200 Subject: [PATCH 128/245] Update changelog --- doc/changelog.asciidoc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 5da5ef553..fd6932f48 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -15,6 +15,15 @@ breaking changes (such as renamed commands) can happen in minor releases. // `Fixed` for any bug fixes. // `Security` to invite users to upgrade in case of vulnerabilities. +v1.13.0 (unreleased) +-------------------- + +Fixed +~~~~~ + +- Crash when `tabs.focus_stack_size` is set to -1. +- Crash when a `pdf.js` file for PDF.js exists, but `viewer.html` does not. + v1.12.0 (2020-06-01) -------------------- From fe6462f307d6226cda81247e72c4560e0d7233f1 Mon Sep 17 00:00:00 2001 From: Nicholas Lantz Date: Mon, 8 Jun 2020 10:45:35 -0600 Subject: [PATCH 129/245] Changed position of input.mouse_backforward in configdata. It was out of alphabetic order before. --- qutebrowser/config/configdata.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index ca4506621..382a8308e 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -1379,6 +1379,11 @@ input.links_included_in_focus_chain: supports_pattern: true desc: Include hyperlinks in the keyboard focus chain when tabbing. +input.mouse_backforward: + default: true + type: Bool + desc: Enable back and forward buttons on the mouse. + input.partial_timeout: default: 5000 type: @@ -1399,11 +1404,6 @@ input.rocker_gestures: This disables the context menu. -input.mouse_backforward: - default: true - type: Bool - desc: Enable back and forward buttons on the mouse. - input.spatial_navigation: default: false type: Bool From cfdebbbe232708b1e1e9f4f77746d466a7c7d86e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 8 Jun 2020 20:07:16 +0200 Subject: [PATCH 130/245] tests: Stabilize test_qute_settings_persistence The :jseval command triggers an XHR in JS, and we'll need to wait for that to actually finish (async) before the setting is really set. See https://github.com/qutebrowser/qutebrowser/issues/5390#issuecomment-628773737 --- tests/end2end/test_invocations.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py index d61458ef3..a31494634 100644 --- a/tests/end2end/test_invocations.py +++ b/tests/end2end/test_invocations.py @@ -381,6 +381,8 @@ def test_qute_settings_persistence(short_tmpdir, request, quteproc_new): quteproc_new.send_cmd(':jseval --world main ' 'cset("search.ignore_case", "always")') quteproc_new.wait_for(message='No output or error') + quteproc_new.wait_for(category='config', + message='Config option changed: *') assert quteproc_new.get_setting('search.ignore_case') == 'always' From fa85e855524914531cd23abe89e7292859e82591 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 8 Jun 2020 21:01:39 +0200 Subject: [PATCH 131/245] Add quirk for missing globalThis globalThis was introduced in Chrome 71: https://caniuse.com/#feat=mdn-javascript_builtins_globalthis Since those websites are quite common and we probably want to continue supporting Qt 5.12 for a bit, let's bite the bullet and add a small polyfill, because it's trivial. Fixes #5486 --- qutebrowser/browser/webengine/webenginetab.py | 22 ++++++++++++++----- .../javascript/globalthis_quirk.user.js | 9 ++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 qutebrowser/javascript/globalthis_quirk.user.js diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 5fd4a9e11..d1e7e88e6 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -1237,19 +1237,31 @@ class _WebEngineScripts(QObject): """Add site-specific quirk scripts. NOTE: This isn't implemented for Qt 5.7 because of different UserScript - semantics there. We only have a quirk for WhatsApp Web right now. It - looks like that quirk isn't needed for Qt < 5.13. + semantics there. The WhatsApp Web quirk isn't needed for Qt < 5.13. + The globalthis_quirk would be, but let's not keep such old QtWebEngine + versions on life support. """ if not config.val.content.site_specific_quirks: return page_scripts = self._widget.page().scripts() + quirks = [ + ( + 'whatsapp_web_quirk', + QWebEngineScript.DocumentReady, + QWebEngineScript.ApplicationWorld, + ), + ] + if not qtutils.version_check('5.13'): + quirks.append(('globalthis_quirk', + QWebEngineScript.DocumentCreation, + QWebEngineScript.MainWorld)) - for filename in ['whatsapp_web_quirk']: + for filename, injection_point, world in quirks: script = QWebEngineScript() script.setName(filename) - script.setWorldId(QWebEngineScript.ApplicationWorld) - script.setInjectionPoint(QWebEngineScript.DocumentReady) + script.setWorldId(world) + script.setInjectionPoint(injection_point) src = utils.read_file("javascript/{}.user.js".format(filename)) script.setSourceCode(src) page_scripts.insert(script) diff --git a/qutebrowser/javascript/globalthis_quirk.user.js b/qutebrowser/javascript/globalthis_quirk.user.js new file mode 100644 index 000000000..03e74de3c --- /dev/null +++ b/qutebrowser/javascript/globalthis_quirk.user.js @@ -0,0 +1,9 @@ +// ==UserScript== +// @include https://www.reddit.com/* +// @include https://open.spotify.com/* +// ==/UserScript== + +// Polyfill for a failing globalThis with older Qt versions. + +"use strict"; +window.globalThis = window; From d373d12b52ffa1456b06d496affea19d993a4735 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 8 Jun 2020 21:19:09 +0200 Subject: [PATCH 132/245] Update changelog --- doc/changelog.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index fd6932f48..4ba21a4d3 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -23,6 +23,7 @@ Fixed - Crash when `tabs.focus_stack_size` is set to -1. - Crash when a `pdf.js` file for PDF.js exists, but `viewer.html` does not. +- New site-specific quirk for a missing `globalThis` in Qt <= 5.12 on Reddit and Spotify v1.12.0 (2020-06-01) -------------------- From 6047a83b9057ab20e5b559681117188e91400cfa Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 8 Jun 2020 21:30:11 +0200 Subject: [PATCH 133/245] Update certifi from 2020.4.5.1 to 2020.4.5.2 --- misc/requirements/requirements-codecov.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-codecov.txt b/misc/requirements/requirements-codecov.txt index 0a38e12b5..986909ef9 100644 --- a/misc/requirements/requirements-codecov.txt +++ b/misc/requirements/requirements-codecov.txt @@ -1,6 +1,6 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -certifi==2020.4.5.1 +certifi==2020.4.5.2 chardet==3.0.4 codecov==2.1.3 coverage==5.1 From 992f4cb41e7250367d594913860db8723817d16f Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 8 Jun 2020 21:30:12 +0200 Subject: [PATCH 134/245] Update certifi from 2020.4.5.1 to 2020.4.5.2 --- misc/requirements/requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt index bb834483f..2e7939deb 100644 --- a/misc/requirements/requirements-dev.txt +++ b/misc/requirements/requirements-dev.txt @@ -1,7 +1,7 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py bump2version==1.0.0 -certifi==2020.4.5.1 +certifi==2020.4.5.2 cffi==1.14.0 chardet==3.0.4 colorama==0.4.3 From 63070432a718805837325f3ef9627fb38d46f643 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 8 Jun 2020 21:30:13 +0200 Subject: [PATCH 135/245] Update certifi from 2020.4.5.1 to 2020.4.5.2 --- misc/requirements/requirements-pylint.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index e80ddbf50..791ced87c 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -1,7 +1,7 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py astroid==2.3.3 # rq.filter: < 2.4 -certifi==2020.4.5.1 +certifi==2020.4.5.2 cffi==1.14.0 chardet==3.0.4 cryptography==2.9.2 From 0285c0a206d6215c43377c6f9fd395296e7b1fd4 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 8 Jun 2020 21:30:14 +0200 Subject: [PATCH 136/245] Update certifi from 2020.4.5.1 to 2020.4.5.2 --- misc/requirements/requirements-sphinx.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt index c90606b66..6340faddc 100644 --- a/misc/requirements/requirements-sphinx.txt +++ b/misc/requirements/requirements-sphinx.txt @@ -2,7 +2,7 @@ alabaster==0.7.12 Babel==2.8.0 -certifi==2020.4.5.1 +certifi==2020.4.5.2 chardet==3.0.4 docutils==0.16 idna==2.9 From 3284adf5fe4bf2be092da9aa531fafcfb7023a3f Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 8 Jun 2020 21:30:16 +0200 Subject: [PATCH 137/245] Update codecov from 2.1.3 to 2.1.4 --- misc/requirements/requirements-codecov.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-codecov.txt b/misc/requirements/requirements-codecov.txt index 986909ef9..7e869803a 100644 --- a/misc/requirements/requirements-codecov.txt +++ b/misc/requirements/requirements-codecov.txt @@ -2,7 +2,7 @@ certifi==2020.4.5.2 chardet==3.0.4 -codecov==2.1.3 +codecov==2.1.4 coverage==5.1 idna==2.9 requests==2.23.0 From daca14aa3b93290e1ea2b8d34342c145c0551d05 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 8 Jun 2020 21:30:17 +0200 Subject: [PATCH 138/245] Update flake8-comprehensions from 3.2.2 to 3.2.3 --- misc/requirements/requirements-flake8.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt index d0292a4eb..0cd0df369 100644 --- a/misc/requirements/requirements-flake8.txt +++ b/misc/requirements/requirements-flake8.txt @@ -4,7 +4,7 @@ attrs==19.3.0 flake8==3.8.2 flake8-bugbear==20.1.4 flake8-builtins==1.5.3 -flake8-comprehensions==3.2.2 +flake8-comprehensions==3.2.3 flake8-copyright==0.2.2 flake8-debugger==3.2.1 flake8-deprecated==1.3 From 95b2063506a6dcdcb65384113399e24526ec03db Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 8 Jun 2020 21:30:18 +0200 Subject: [PATCH 139/245] Update mypy from 0.770 to 0.780 --- misc/requirements/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt index 3900a8c20..7759f96b8 100644 --- a/misc/requirements/requirements-mypy.txt +++ b/misc/requirements/requirements-mypy.txt @@ -1,6 +1,6 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -mypy==0.770 +mypy==0.780 mypy-extensions==0.4.3 -e git+https://github.com/stlehmann/PyQt5-stubs.git@master#egg=PyQt5_stubs typed-ast==1.4.1 From 593fee8793737d2d2dd7a6f20e8ae7ed3f0bf6b9 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 8 Jun 2020 21:30:19 +0200 Subject: [PATCH 140/245] Update lazy-object-proxy from 1.4.3 to 1.5.0 --- misc/requirements/requirements-pylint.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index 791ced87c..cb4892f9c 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -9,7 +9,7 @@ github3.py==1.3.0 idna==2.9 isort==4.3.21 jwcrypto==0.7 -lazy-object-proxy==1.4.3 +lazy-object-proxy==1.5.0 mccabe==0.6.1 pycparser==2.20 pylint==2.4.4 # rq.filter: < 2.5 From d32063dcf5049da2c3a963b5826a28a94955735f Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 8 Jun 2020 21:30:20 +0200 Subject: [PATCH 141/245] Update sphinx from 3.0.4 to 3.1.0 --- misc/requirements/requirements-sphinx.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt index 6340faddc..5851b8b72 100644 --- a/misc/requirements/requirements-sphinx.txt +++ b/misc/requirements/requirements-sphinx.txt @@ -16,7 +16,7 @@ pytz==2020.1 requests==2.23.0 six==1.15.0 snowballstemmer==2.0.0 -Sphinx==3.0.4 +Sphinx==3.1.0 sphinxcontrib-applehelp==1.0.2 sphinxcontrib-devhelp==1.0.2 sphinxcontrib-htmlhelp==1.0.3 From b550fb348f2b5dd093a33adce701c994ee6bda27 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 8 Jun 2020 21:30:21 +0200 Subject: [PATCH 142/245] Update pytest from 5.4.2 to 5.4.3 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 0c96a2af1..8a6dab6da 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -26,7 +26,7 @@ py==1.8.1 py-cpuinfo==5.0.0 Pygments==2.6.1 pyparsing==2.4.7 -pytest==5.4.2 +pytest==5.4.3 pytest-bdd==3.3.0 pytest-benchmark==3.2.3 pytest-cov==2.9.0 From 59dbbc63961a0cfd4107b7c47623ec15812f8350 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 8 Jun 2020 21:30:22 +0200 Subject: [PATCH 143/245] Update pytest-bdd from 3.3.0 to 3.4.0 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 8a6dab6da..65f83a1fb 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -27,7 +27,7 @@ py-cpuinfo==5.0.0 Pygments==2.6.1 pyparsing==2.4.7 pytest==5.4.3 -pytest-bdd==3.3.0 +pytest-bdd==3.4.0 pytest-benchmark==3.2.3 pytest-cov==2.9.0 pytest-instafail==0.4.1.post0 From 4e97186a1151b33f883b9ebff1d72734ab2503a4 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 8 Jun 2020 21:30:23 +0200 Subject: [PATCH 144/245] Update pytest-mock from 3.1.0 to 3.1.1 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 65f83a1fb..22bfc3c2d 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -31,7 +31,7 @@ pytest-bdd==3.4.0 pytest-benchmark==3.2.3 pytest-cov==2.9.0 pytest-instafail==0.4.1.post0 -pytest-mock==3.1.0 +pytest-mock==3.1.1 pytest-qt==3.3.0 pytest-repeat==0.8.0 pytest-rerunfailures==9.0 From 6f5f80f77f92872f7b71e3a9a461173b23ae8491 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 8 Jun 2020 21:30:25 +0200 Subject: [PATCH 145/245] Update sortedcontainers from 2.1.0 to 2.2.2 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 22bfc3c2d..dd9073681 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -39,7 +39,7 @@ pytest-travis-fold==1.3.0 pytest-xvfb==1.2.0 PyVirtualDisplay==0.2.5 # rq.filter: < 1.0 six==1.15.0 -sortedcontainers==2.1.0 +sortedcontainers==2.2.2 soupsieve==2.0.1 vulture==1.5 wcwidth==0.2.2 From 82c77bae8b9d58cad6c2f23cf6bbd86d762e104e Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 8 Jun 2020 21:30:26 +0200 Subject: [PATCH 146/245] Update wcwidth from 0.2.2 to 0.2.4 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index dd9073681..ed4596a82 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -42,6 +42,6 @@ six==1.15.0 sortedcontainers==2.2.2 soupsieve==2.0.1 vulture==1.5 -wcwidth==0.2.2 +wcwidth==0.2.4 Werkzeug==1.0.1 jaraco.functools==2.0; python_version<"3.6" # rq.filter: <= 2.0 From 0f9720adac09002dd883d2ee9137d660e7d4769f Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 8 Jun 2020 21:30:27 +0200 Subject: [PATCH 147/245] Update tox from 3.15.1 to 3.15.2 --- misc/requirements/requirements-tox.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt index 3b4c93bb4..dd288088d 100644 --- a/misc/requirements/requirements-tox.txt +++ b/misc/requirements/requirements-tox.txt @@ -9,7 +9,7 @@ py==1.8.1 pyparsing==2.4.7 six==1.15.0 toml==0.10.1 -tox==3.15.1 +tox==3.15.2 tox-pip-version==0.0.7 tox-venv==0.4.0 virtualenv==20.0.21 From ee8b2cb27a40271381fc85ff00ac046c4090fd7a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 Jun 2020 13:18:45 +0200 Subject: [PATCH 148/245] Fix cast for new mypy version --- qutebrowser/utils/qtutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 109d2dfed..04db1f0cb 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -230,7 +230,7 @@ def savefile_open( if not open_ok: raise QtOSError(f) - dev = typing.cast(typing.IO[bytes], PyQIODevice(f)) + dev = typing.cast(typing.BinaryIO, PyQIODevice(f)) if binary: new_f = dev # type: typing.IO From 5b9237318e33009268780f28f0c84b0e0fb148d6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 Jun 2020 13:19:02 +0200 Subject: [PATCH 149/245] Hide mypy.ini --- mypy.ini => .mypy.ini | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename mypy.ini => .mypy.ini (100%) diff --git a/mypy.ini b/.mypy.ini similarity index 100% rename from mypy.ini rename to .mypy.ini From ba25246c310e033342e7374c125ce3df17d1c212 Mon Sep 17 00:00:00 2001 From: Constantine Theocharis Date: Tue, 9 Jun 2020 12:40:34 +0100 Subject: [PATCH 150/245] Merge statusbar.hide and statusbar.smarthide options into statusbar.show --- doc/help/settings.asciidoc | 24 +++++++++++++++--------- qutebrowser/config/configdata.yml | 22 +++++++++++----------- qutebrowser/mainwindow/statusbar/bar.py | 15 +++++++-------- 3 files changed, 33 insertions(+), 28 deletions(-) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index cc020ea88..82588c9a0 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -277,9 +277,9 @@ |<>|Name of the session to save by default. |<>|Load a restored tab as soon as it takes focus. |<>|Languages to use for spell checking. -|<>|Hide the statusbar unless a message is shown. |<>|Padding (in pixels) for the statusbar. |<>|Position of the status bar. +|<>|When to show the statusbar. |<>|List of widgets displayed in the statusbar. |<>|Open new tabs (middleclick/ctrl+click) in the background. |<>|Mouse button with which to close tabs. @@ -3540,14 +3540,6 @@ On QtWebEngine, this setting requires Qt 5.8 or newer. On QtWebKit, this setting is unavailable. -[[statusbar.hide]] -=== statusbar.hide -Hide the statusbar unless a message is shown. - -Type: <> - -Default: +pass:[false]+ - [[statusbar.padding]] === statusbar.padding Padding (in pixels) for the statusbar. @@ -3574,6 +3566,20 @@ Valid values: Default: +pass:[bottom]+ +[[statusbar.show]] +=== statusbar.show +When to show the statusbar. + +Type: <> + +Valid values: + + * +always+: Always show the statusbar. + * +never+: Always hide the statusbar unless a message is shown. + * +in_mode+: Only show the statusbar when in modes other than normal mode or when a message is shown. + +Default: +pass:[always]+ + [[statusbar.widgets]] === statusbar.widgets List of widgets displayed in the statusbar. diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index b8af6a9a5..4ca2231a3 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -1559,17 +1559,17 @@ spellcheck.languages: ## statusbar -statusbar.hide: - type: Bool - default: false - desc: Hide the statusbar unless a message is shown. - -statusbar.smarthide: - type: Bool - default: false - desc: >- - If statusbar.hide is true, only hide the statusbar in normal mode, and show - it in other modes. +statusbar.show: + default: always + type: + name: String + valid_values: + - always: Always show the statusbar. + - never: Always hide the statusbar unless a message is shown. + - in_mode: >- + Only show the statusbar when in modes other than normal mode or + when a message is shown. + desc: When to show the statusbar. statusbar.padding: type: Padding diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index cf9e6aa45..1485dd86a 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -203,7 +203,7 @@ class StatusBar(QWidget): @pyqtSlot(str) def _on_config_changed(self, option): - if option == 'statusbar.hide': + if option == 'statusbar.show': self.maybe_hide() elif option == 'statusbar.padding': self._set_hbox_padding() @@ -254,14 +254,13 @@ class StatusBar(QWidget): @pyqtSlot() def maybe_hide(self): """Hide the statusbar if it's configured to do so.""" - hide = config.val.statusbar.hide - smarthide = config.val.statusbar.smarthide + strategy = config.val.statusbar.show tab = self._current_tab() if tab is not None and tab.data.fullscreen: self.hide() - elif hide and not smarthide: + elif strategy == 'never': self.hide() - elif hide and smarthide: + elif strategy == 'in_mode': try: mode_manager = modeman.instance(self._win_id) if mode_manager.mode == usertypes.KeyMode.normal: @@ -269,7 +268,7 @@ class StatusBar(QWidget): except KeyError: # If modeman hasn't been initialized, hide the bar. self.hide() - else: + elif strategy == 'always': self.show() def _set_hbox_padding(self): @@ -347,7 +346,7 @@ class StatusBar(QWidget): def on_mode_entered(self, mode): """Mark certain modes in the commandline.""" mode_manager = modeman.instance(self._win_id) - if config.val.statusbar.smarthide and config.val.statusbar.hide: + if config.val.statusbar.show == 'in_mode': self.show() if mode_manager.parsers[mode].passthrough: self._set_mode_text(mode.name) @@ -363,7 +362,7 @@ class StatusBar(QWidget): def on_mode_left(self, old_mode, new_mode): """Clear marked mode.""" mode_manager = modeman.instance(self._win_id) - if config.val.statusbar.smarthide and config.val.statusbar.hide: + if config.val.statusbar.show == 'in_mode': self.hide() if mode_manager.parsers[old_mode].passthrough: if mode_manager.parsers[new_mode].passthrough: From 8d7b13f1efab8ab0777dd6c0aebba4b3e6728fc2 Mon Sep 17 00:00:00 2001 From: Constantine Theocharis Date: Tue, 9 Jun 2020 12:40:53 +0100 Subject: [PATCH 151/245] Add statusbar.show to YamlMigrations --- qutebrowser/config/configfiles.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index 7d567ebd3..498f2ae5d 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -332,6 +332,11 @@ class YamlMigrations(QObject): new_name='tabs.mode_on_change', true_value='persist', false_value='normal') + self._migrate_renamed_bool( + old_name='statusbar.hide', + new_name='statusbar.show', + true_value='never', + false_value='always') for setting in ['tabs.title.format', 'tabs.title.format_pinned', From d4a7d8ef6210002e6990a9108e49ba9b5b134e82 Mon Sep 17 00:00:00 2001 From: Constantine Theocharis Date: Tue, 9 Jun 2020 12:49:36 +0100 Subject: [PATCH 152/245] Add additional else clause for statusbar.show=in_mode --- qutebrowser/mainwindow/statusbar/bar.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 1485dd86a..8ccfc2070 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -265,6 +265,8 @@ class StatusBar(QWidget): mode_manager = modeman.instance(self._win_id) if mode_manager.mode == usertypes.KeyMode.normal: self.hide() + else: + self.show() except KeyError: # If modeman hasn't been initialized, hide the bar. self.hide() From d38a2094f1a006b5702d1d9702f1da8b3a610ce9 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 Jun 2020 13:57:20 +0200 Subject: [PATCH 153/245] tests: Add ignores for Qt 5.15 Chromium debug build --- tests/end2end/fixtures/quteprocess.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py index 626981f13..2e47c9e43 100644 --- a/tests/end2end/fixtures/quteprocess.py +++ b/tests/end2end/fixtures/quteprocess.py @@ -290,6 +290,18 @@ def is_ignored_chromium_message(line): # [2968:3108:0601/123442.125:ERROR:mf_helpers.cc(14)] Error in # dxva_video_decode_accelerator_win.cc on line 517 'Error in dxva_video_decode_accelerator_win.cc on line 517', + + # Qt 5.15 and debug build + # [134188:134199:0609/132454.797229:WARNING: + # simple_synchronous_entry.cc(1389)] + # Could not open platform files for entry. + # [134151:134187:0609/132456.754321:ERROR:process_posix.cc(333)] + # Unable to terminate process 134188: No such process (3) + # [134151:134187:0609/132456.754414:WARNING:internal_linux.cc(64)] + # Failed to read /proc/134188/stat + 'Could not open platform files for entry.', + 'Unable to terminate process *: No such process (3)', + 'Failed to read /proc/*/stat', ] return any(testutils.pattern_match(pattern=pattern, value=message) for pattern in ignored_messages) From a87bff269914b5924c9df21bf3763aeb7f20451a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 Jun 2020 13:59:21 +0200 Subject: [PATCH 154/245] Update changelog --- doc/changelog.asciidoc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 4ba21a4d3..33b69121e 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -18,6 +18,12 @@ breaking changes (such as renamed commands) can happen in minor releases. v1.13.0 (unreleased) -------------------- +Changed +~~~~~~~ + +- The `tor_identity` userscript now takes the password via a `-p` flag and has + a new `-c` flag to customize the Tor control port. + Fixed ~~~~~ From 3204364d9d4bceacc82ac1dcdd71dba8ea4dcb14 Mon Sep 17 00:00:00 2001 From: Constantine Theocharis Date: Tue, 9 Jun 2020 13:11:07 +0100 Subject: [PATCH 155/245] Update naming in qutebrowser/config/configdata.yml Co-authored-by: Florian Bruhin --- qutebrowser/config/configdata.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 4ca2231a3..256fdc906 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -1566,7 +1566,7 @@ statusbar.show: valid_values: - always: Always show the statusbar. - never: Always hide the statusbar unless a message is shown. - - in_mode: >- + - in-mode: >- Only show the statusbar when in modes other than normal mode or when a message is shown. desc: When to show the statusbar. From 2bc14ddc681f946535f905d07d8e4f80013590a5 Mon Sep 17 00:00:00 2001 From: Constantine Theocharis Date: Tue, 9 Jun 2020 13:29:03 +0100 Subject: [PATCH 156/245] Modify modeman.instance() to return specific error when unavailable. This avoids the need to have a generic 'except KeyError' in statusbar/bar.py when querying the current mode, in the case when the mode manager has not been initialized yet. --- qutebrowser/keyinput/modeman.py | 20 ++++++++++++++++++-- qutebrowser/mainwindow/statusbar/bar.py | 6 +++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index 880b1ec93..eb96020f3 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -68,6 +68,14 @@ class NotInModeError(Exception): """Exception raised when we want to leave a mode we're not in.""" +class UnavailableError(Exception): + + """Exception raised when trying to access modeman before initialization. + + Thrown by instance() if modeman has not been initialized yet. + """ + + def init(win_id: int, parent: QObject) -> 'ModeManager': """Initialize the mode manager and the keyparsers for the given win_id.""" modeman = ModeManager(win_id, parent) @@ -169,8 +177,16 @@ def init(win_id: int, parent: QObject) -> 'ModeManager': def instance(win_id: Union[int, str]) -> 'ModeManager': - """Get a modemanager object.""" - return objreg.get('mode-manager', scope='window', window=win_id) + """Get a modemanager object. + + Raises UnavailableError if there is no instance available yet. + """ + mode_manager = objreg.get('mode-manager', scope='window', window=win_id, + default=None) + if mode_manager is not None: + return mode_manager + else: + raise UnavailableError("ModeManager is not initialized yet.") def enter(win_id: int, diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 8ccfc2070..384b73d7a 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -263,13 +263,13 @@ class StatusBar(QWidget): elif strategy == 'in_mode': try: mode_manager = modeman.instance(self._win_id) + except modeman.UnavailableError: + self.hide() + else: if mode_manager.mode == usertypes.KeyMode.normal: self.hide() else: self.show() - except KeyError: - # If modeman hasn't been initialized, hide the bar. - self.hide() elif strategy == 'always': self.show() From 10211f51557b29341e6283c26e562c2eff6185e3 Mon Sep 17 00:00:00 2001 From: Constantine Theocharis Date: Tue, 9 Jun 2020 13:46:30 +0100 Subject: [PATCH 157/245] Add unreachable test in statusbar/bar.py --- qutebrowser/mainwindow/statusbar/bar.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 384b73d7a..2735ad073 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -272,6 +272,8 @@ class StatusBar(QWidget): self.show() elif strategy == 'always': self.show() + else: + raise utils.Unreachable def _set_hbox_padding(self): padding = config.val.statusbar.padding From 66b3cdb225e1b605dd391f8cb38510ea82a43044 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 Jun 2020 15:11:48 +0200 Subject: [PATCH 158/245] Simplify configdata.yml --- qutebrowser/config/configdata.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 42a73095e..a17df8fb5 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -3389,15 +3389,13 @@ bindings.commands: logging.level.ram: default: debug - type: - name: LogLevel + type: LogLevel desc: Level for in-memory logs. logging.level.console: default: info - type: - name: LogLevel + type: LogLevel desc: >- Level for console (stdout/stderr) logs. Ignored if the `--loglevel` or `--debug` CLI flags are used. From 26cdc87226f19ca26d44c402450bd4313afdd4b6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 Jun 2020 15:14:08 +0200 Subject: [PATCH 159/245] Update docs --- doc/changelog.asciidoc | 7 +++++++ doc/help/settings.asciidoc | 37 +++++++++++++++++++++++++++++++++++++ doc/qutebrowser.1.asciidoc | 2 +- 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 33b69121e..7bccf5d8e 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -24,6 +24,13 @@ Changed - The `tor_identity` userscript now takes the password via a `-p` flag and has a new `-c` flag to customize the Tor control port. +Added +~~~~~ + +- New settings: + * `logging.level.ram` and `logging.level.console` to configure the default + logging levels via the config. + Fixed ~~~~~ diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index cc020ea88..72e39bb9c 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -257,6 +257,8 @@ |<>|Keychains that shouldn't be shown in the keyhint dialog. |<>|Time (in milliseconds) from pressing a key to seeing the keyhint dialog. |<>|Rounding radius (in pixels) for the edges of the keyhint dialog. +|<>|Level for console (stdout/stderr) logs. Ignored if the `--loglevel` or `--debug` CLI flags are used. +|<>|Level for in-memory logs. |<>|Duration (in milliseconds) to show messages in the statusbar for. |<>|How to open links in an existing instance if a new one is launched. |<>|Which window to choose when opening links as new tabs. @@ -3248,6 +3250,40 @@ Type: <> Default: +pass:[6]+ +[[logging.level.console]] +=== logging.level.console +Level for console (stdout/stderr) logs. Ignored if the `--loglevel` or `--debug` CLI flags are used. + +Type: <> + +Valid values: + + * +vdebug+ + * +debug+ + * +info+ + * +warning+ + * +error+ + * +critical+ + +Default: +pass:[info]+ + +[[logging.level.ram]] +=== logging.level.ram +Level for in-memory logs. + +Type: <> + +Valid values: + + * +vdebug+ + * +debug+ + * +info+ + * +warning+ + * +error+ + * +critical+ + +Default: +pass:[debug]+ + [[messages.timeout]] === messages.timeout Duration (in milliseconds) to show messages in the statusbar for. @@ -4170,6 +4206,7 @@ Lists with duplicate flags are invalid. Each item is checked against the valid v When setting from a string, pass a json-like list, e.g. `["one", "two"]`. |ListOrValue|A list of values, or a single value. +|LogLevel|A logging level. |NewTabPosition|How new tabs are positioned. |Padding|Setting for paddings around elements. |Perc|A percentage. diff --git a/doc/qutebrowser.1.asciidoc b/doc/qutebrowser.1.asciidoc index 52ed64d3e..8dae3eaef 100644 --- a/doc/qutebrowser.1.asciidoc +++ b/doc/qutebrowser.1.asciidoc @@ -67,7 +67,7 @@ show it. === debug arguments *-l* '{critical,error,warning,info,debug,vdebug}', *--loglevel* '{critical,error,warning,info,debug,vdebug}':: - Set loglevel + Override the configured console loglevel *--logfilter* 'LOGFILTER':: Comma-separated list of things to be logged to the debug log on stdout. From ca14a91741f04fbbd1c40263dab256ed15a030d2 Mon Sep 17 00:00:00 2001 From: Constantine Theocharis Date: Tue, 9 Jun 2020 14:52:15 +0100 Subject: [PATCH 160/245] Change in_mode to in-mode everywhere --- doc/help/settings.asciidoc | 2 +- qutebrowser/mainwindow/statusbar/bar.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 82588c9a0..718377c4f 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -3576,7 +3576,7 @@ Valid values: * +always+: Always show the statusbar. * +never+: Always hide the statusbar unless a message is shown. - * +in_mode+: Only show the statusbar when in modes other than normal mode or when a message is shown. + * +in-mode+: Only show the statusbar when in modes other than normal mode or when a message is shown. Default: +pass:[always]+ diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 2735ad073..f83c77db9 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -260,7 +260,7 @@ class StatusBar(QWidget): self.hide() elif strategy == 'never': self.hide() - elif strategy == 'in_mode': + elif strategy == 'in-mode': try: mode_manager = modeman.instance(self._win_id) except modeman.UnavailableError: @@ -350,7 +350,7 @@ class StatusBar(QWidget): def on_mode_entered(self, mode): """Mark certain modes in the commandline.""" mode_manager = modeman.instance(self._win_id) - if config.val.statusbar.show == 'in_mode': + if config.val.statusbar.show == 'in-mode': self.show() if mode_manager.parsers[mode].passthrough: self._set_mode_text(mode.name) @@ -366,7 +366,7 @@ class StatusBar(QWidget): def on_mode_left(self, old_mode, new_mode): """Clear marked mode.""" mode_manager = modeman.instance(self._win_id) - if config.val.statusbar.show == 'in_mode': + if config.val.statusbar.show == 'in-mode': self.hide() if mode_manager.parsers[old_mode].passthrough: if mode_manager.parsers[new_mode].passthrough: From 5e74b62c67620c99150e37c7f61072ba9afb4ae7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 Jun 2020 17:19:06 +0200 Subject: [PATCH 161/245] Fix ~ with :config-write-py df4a011d48fa9f12fccf59ebca0954ccdf646d20 introduced a bug because the os.path.isabs check was done before os.path.expanduser - and isabs('~/...') will always be False. --- qutebrowser/config/configcommands.py | 2 +- tests/unit/config/test_configcommands.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py index 25b49055c..20702be10 100644 --- a/qutebrowser/config/configcommands.py +++ b/qutebrowser/config/configcommands.py @@ -460,9 +460,9 @@ class ConfigCommands: if filename is None: filename = standarddir.config_py() else: + filename = os.path.expanduser(filename) if not os.path.isabs(filename): filename = os.path.join(standarddir.config(), filename) - filename = os.path.expanduser(filename) if os.path.exists(filename) and not force: raise cmdutils.CommandError("{} already exists - use --force to " diff --git a/tests/unit/config/test_configcommands.py b/tests/unit/config/test_configcommands.py index b793a49ce..5718f6dc9 100644 --- a/tests/unit/config/test_configcommands.py +++ b/tests/unit/config/test_configcommands.py @@ -635,6 +635,19 @@ class TestWritePy: lines = confpy.read_text('utf-8').splitlines() assert '# Autogenerated config.py' in lines + @pytest.mark.posix + def test_expanduser(self, commands, monkeypatch, tmpdir): + """Make sure that using a path with ~/... works correctly.""" + home = tmpdir / 'home' + home.ensure(dir=True) + monkeypatch.setenv('HOME', str(home)) + + commands.config_write_py('~/config.py') + + confpy = home / 'config.py' + lines = confpy.read_text('utf-8').splitlines() + assert '# Autogenerated config.py' in lines + def test_existing_file(self, commands, tmpdir): confpy = tmpdir / 'config.py' confpy.ensure() From 3cd86f30fbf5c8ebeedb7ef3c3a8258ad49de8db Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 Jun 2020 17:32:02 +0200 Subject: [PATCH 162/245] Add note to generated config.py --- qutebrowser/config/configfiles.py | 11 +++++++++++ tests/unit/config/test_configfiles.py | 22 +++++++++++----------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index 7d567ebd3..3e7040b8c 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -603,6 +603,17 @@ class ConfigPyWriter: def _gen_header(self) -> typing.Iterator[str]: """Generate the initial header of the config.""" yield self._line("# Autogenerated config.py") + yield self._line("#") + + note = ("NOTE: config.py is intended for advanced users who are " + "comfortable with manually migrating the config file on " + "qutebrowser upgrades. If you prefer, you can also configure " + "qutebrowser using the :set/:bind/:config-* commands without " + "having to write a config.py file.") + for line in textwrap.wrap(note): + yield self._line("# {}".format(line)) + + yield self._line("#") yield self._line("# Documentation:") yield self._line("# qute://help/configuring.html") yield self._line("# qute://help/settings.html") diff --git a/tests/unit/config/test_configfiles.py b/tests/unit/config/test_configfiles.py index 4c0b6305c..2fce0bfb4 100644 --- a/tests/unit/config/test_configfiles.py +++ b/tests/unit/config/test_configfiles.py @@ -1052,6 +1052,13 @@ class TestConfigPyWriter: assert text == textwrap.dedent(""" # Autogenerated config.py + # + # NOTE: config.py is intended for advanced users who are comfortable + # with manually migrating the config file on qutebrowser upgrades. If + # you prefer, you can also configure qutebrowser using the + # :set/:bind/:config-* commands without having to write a config.py + # file. + # # Documentation: # qute://help/configuring.html # qute://help/settings.html @@ -1156,17 +1163,10 @@ class TestConfigPyWriter: def test_empty(self): writer = configfiles.ConfigPyWriter(options=[], bindings={}, commented=False) - text = '\n'.join(writer._gen_lines()) - expected = textwrap.dedent(""" - # Autogenerated config.py - # Documentation: - # qute://help/configuring.html - # qute://help/settings.html - - # Uncomment this to still load settings configured via autoconfig.yml - # config.load_autoconfig() - """).lstrip() - assert text == expected + lines = list(writer._gen_lines()) + assert lines[0] == '# Autogenerated config.py' + assert lines[-2] == '# config.load_autoconfig()' + assert not lines[-1] def test_pattern(self): opt = configdata.Option( From 5852b5dbc9bcb95615fb26667a8f52607585ebe9 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 Jun 2020 17:20:12 +0200 Subject: [PATCH 163/245] Update changelog --- doc/changelog.asciidoc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 7bccf5d8e..daed2fda7 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -23,6 +23,8 @@ Changed - The `tor_identity` userscript now takes the password via a `-p` flag and has a new `-c` flag to customize the Tor control port. +- `:config-write-py` now adds a note about `config.py` files being targeted at + advanced users. Added ~~~~~ @@ -36,6 +38,7 @@ Fixed - Crash when `tabs.focus_stack_size` is set to -1. - Crash when a `pdf.js` file for PDF.js exists, but `viewer.html` does not. +- `:config-write-py` now works with paths starting with `~/...` again. - New site-specific quirk for a missing `globalThis` in Qt <= 5.12 on Reddit and Spotify v1.12.0 (2020-06-01) From ea2271c2945063d30c4a5f48893f322e4fd3fef4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 Jun 2020 18:59:58 +0200 Subject: [PATCH 164/245] Add arguments for :report --- doc/changelog.asciidoc | 2 ++ doc/help/commands.asciidoc | 7 +++++++ qutebrowser/misc/crashdialog.py | 18 ++++++++++++------ qutebrowser/misc/crashsignal.py | 17 ++++++++++++++--- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index daed2fda7..f1f5e0dea 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -25,6 +25,8 @@ Changed a new `-c` flag to customize the Tor control port. - `:config-write-py` now adds a note about `config.py` files being targeted at advanced users. +- `:report` now takes two optional arguments for bug/contact information, so + that it can be used without the report window popping up. Added ~~~~~ diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index dc40c76a6..9b2cae6c6 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -1015,8 +1015,15 @@ Which count to pass the command. [[report]] === report +Syntax: +:report ['info'] ['contact']+ + Report a bug in qutebrowser. +==== positional arguments +* +'info'+: Information about the bug report. If given, no report dialog shows up. + +* +'contact'+: Contact information for the report. + [[restart]] === restart Restart qutebrowser while keeping existing tabs open. diff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py index 0f6d02712..4387e479a 100644 --- a/qutebrowser/misc/crashdialog.py +++ b/qutebrowser/misc/crashdialog.py @@ -125,9 +125,13 @@ class _CrashDialog(QDialog): self.setWindowTitle("Whoops!") self.resize(QSize(640, 600)) self._vbox = QVBoxLayout(self) + http_client = httpclient.HTTPClient() self._paste_client = pastebin.PastebinClient(http_client, self) self._pypi_client = autoupdate.PyPIVersionClient(self) + self._paste_client.success.connect(self.on_paste_success) + self._paste_client.error.connect(self.show_error) + self._init_text() self._init_contact_input() @@ -296,13 +300,17 @@ class _CrashDialog(QDialog): except Exception: log.misc.exception("Failed to save contact information!") - def report(self): - """Paste the crash info into the pastebin.""" + def report(self, *, info=None, contact=None): + """Paste the crash info into the pastebin. + + If info/contact are given as arguments, they override the values + entered in the dialog. + """ lines = [] lines.append("========== Report ==========") - lines.append(self._info.toPlainText()) + lines.append(info or self._info.toPlainText()) lines.append("========== Contact ==========") - lines.append(self._contact.toPlainText()) + lines.append(contact or self._contact.toPlainText()) lines.append("========== Debug log ==========") lines.append(self._debug_log.toPlainText()) self._paste_text = '\n\n'.join(lines) @@ -326,8 +334,6 @@ class _CrashDialog(QDialog): self._btn_report.setEnabled(False) self._btn_cancel.setEnabled(False) self._btn_report.setText("Reporting...") - self._paste_client.success.connect(self.on_paste_success) - self._paste_client.error.connect(self.show_error) self.report() @pyqtSlot() diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py index 5d8bd0ff5..3f80db769 100644 --- a/qutebrowser/misc/crashsignal.py +++ b/qutebrowser/misc/crashsignal.py @@ -162,14 +162,25 @@ class CrashHandler(QObject): earlyinit.init_faulthandler(self._crash_log_file) @cmdutils.register(instance='crash-handler') - def report(self): - """Report a bug in qutebrowser.""" + def report(self, info=None, contact=None): + """Report a bug in qutebrowser. + + Args: + info: Information about the bug report. If given, no report dialog + shows up. + contact: Contact information for the report. + """ pages = self._recover_pages() cmd_history = objreg.get('command-history')[-5:] all_objects = debug.get_all_objects() + self._crash_dialog = crashdialog.ReportDialog(pages, cmd_history, all_objects) - self._crash_dialog.show() + + if info is None: + self._crash_dialog.show() + else: + self._crash_dialog.report(info=info, contact=contact) @pyqtSlot() def shutdown(self): From 2535d6dc7aa06ca0032716f787771c40431a9ad4 Mon Sep 17 00:00:00 2001 From: Constantine Theocharis Date: Tue, 9 Jun 2020 18:29:06 +0100 Subject: [PATCH 165/245] Add ability to customise tab font based on selected status Also, use Qt Style Sheets to set the tab bar font instead of setFont(QFont). This makes the fonts.tabs.* settings consistent with all the other font settings (using Font instead of QFont). --- doc/help/settings.asciidoc | 19 ++++++++++++++----- qutebrowser/config/configdata.yml | 11 ++++++++--- qutebrowser/config/configfiles.py | 16 ++++++++++++++++ qutebrowser/mainwindow/tabwidget.py | 28 +++++++++++++++------------- 4 files changed, 53 insertions(+), 21 deletions(-) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index cc020ea88..648168a60 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -214,7 +214,8 @@ |<>|Font used for warning messages. |<>|Font used for prompts. |<>|Font used in the statusbar. -|<>|Font used in the tab bar. +|<>|Font used in the tab bar for selected tabs. +|<>|Font used in the tab bar for unselected tabs. |<>|Font family for cursive fonts. |<>|Font family for fantasy fonts. |<>|Font family for fixed fonts. @@ -2785,11 +2786,19 @@ Type: <> Default: +pass:[default_size default_family]+ -[[fonts.tabs]] -=== fonts.tabs -Font used in the tab bar. +[[fonts.tabs.selected]] +=== fonts.tabs.selected +Font used in the tab bar for selected tabs. -Type: <> +Type: <> + +Default: +pass:[default_size default_family]+ + +[[fonts.tabs.unselected]] +=== fonts.tabs.unselected +Font used in the tab bar for unselected tabs. + +Type: <> Default: +pass:[default_size default_family]+ diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 664f9facf..68b17fdde 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -2872,10 +2872,15 @@ fonts.statusbar: type: Font desc: Font used in the statusbar. -fonts.tabs: +fonts.tabs.selected: default: default_size default_family - type: QtFont - desc: Font used in the tab bar. + type: Font + desc: Font used in the tab bar for selected tabs. + +fonts.tabs.unselected: + default: default_size default_family + type: Font + desc: Font used in the tab bar for unselected tabs. fonts.web.family.standard: default: '' diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index 7d567ebd3..da203cc84 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -340,6 +340,10 @@ class YamlMigrations(QObject): r'(? None: + if old_name not in self._settings: + return + + for new_name in new_names: + self._settings[new_name] = {} + for scope, val in self._settings[old_name].items(): + self._settings[new_name][scope] = val + + del self._settings[old_name] + self.changed.emit() + def _migrate_string_value(self, name: str, source: str, target: str) -> None: diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index 558713fdf..006bd6cca 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -385,8 +385,13 @@ class TabBar(QTabBar): STYLESHEET = """ TabBar { + font: {{ conf.fonts.tabs.unselected }}; background-color: {{ conf.colors.tabs.bar.bg }}; } + + TabBar::tab:selected { + font: {{ conf.fonts.tabs.selected }}; + } """ new_tab_requested = pyqtSignal() @@ -395,8 +400,6 @@ class TabBar(QTabBar): super().__init__(parent) self._win_id = win_id self.setStyle(TabBarStyle()) - self._set_font() - config.instance.changed.connect(self._on_config_changed) self.vertical = False self._auto_hide_timer = QTimer() self._auto_hide_timer.setSingleShot(True) @@ -405,6 +408,9 @@ class TabBar(QTabBar): self.setAutoFillBackground(True) self.drag_in_progress = False stylesheet.set_register(self) + self.ensurePolished() + config.instance.changed.connect(self._on_config_changed) + self._set_icon_size() QTimer.singleShot(0, self.maybe_hide) def __repr__(self): @@ -416,8 +422,10 @@ class TabBar(QTabBar): @pyqtSlot(str) def _on_config_changed(self, option: str) -> None: - if option == 'fonts.tabs': - self._set_font() + if option.startswith('fonts.tabs.'): + self.update() + self.ensurePolished() + self._set_icon_size() elif option == 'tabs.favicons.scale': self._set_icon_size() elif option == 'tabs.show_switching_delay': @@ -433,7 +441,9 @@ class TabBar(QTabBar): "tabs.padding", "tabs.indicator.width", "tabs.min_width", - "tabs.pinned.shrink"]: + "tabs.pinned.shrink", + "fonts.tabs.selected", + "fonts.tabs.unselected"]: self._minimum_tab_size_hint_helper.cache_clear() self._minimum_tab_height.cache_clear() @@ -506,14 +516,6 @@ class TabBar(QTabBar): # code sets layoutDirty so it actually relayouts the tabs. self.setIconSize(self.iconSize()) - def _set_font(self): - """Set the tab bar font.""" - self.setFont(config.val.fonts.tabs) - self._set_icon_size() - # clear tab size cache - self._minimum_tab_size_hint_helper.cache_clear() - self._minimum_tab_height.cache_clear() - def _set_icon_size(self): """Set the tab bar favicon size.""" size = self.fontMetrics().height() - 2 From 9c9ab7015efd50ec9aafeb4612cc13b2e63dbe45 Mon Sep 17 00:00:00 2001 From: Constantine Theocharis Date: Tue, 9 Jun 2020 19:50:48 +0100 Subject: [PATCH 166/245] Add test for _migrate_multiple --- tests/unit/config/test_configfiles.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/unit/config/test_configfiles.py b/tests/unit/config/test_configfiles.py index 4c0b6305c..953ab60b5 100644 --- a/tests/unit/config/test_configfiles.py +++ b/tests/unit/config/test_configfiles.py @@ -588,6 +588,17 @@ class TestYamlMigrations: def test_font_replacements(self, migration_test, setting, old, new): migration_test(setting, old, new) + def test_migrate_multiple(self, yaml, autoconfig): + val = '10pt default_family' + autoconfig.write({'fonts.tabs': {'global': val}}) + + yaml.load() + yaml._save() + + data = autoconfig.read() + assert data['fonts.tabs.unselected']['global'] == val + assert data['fonts.tabs.selected']['global'] == val + class ConfPy: From 84b6d224ffd68c9f368a910e5b83c16de4596d69 Mon Sep 17 00:00:00 2001 From: Constantine Theocharis Date: Tue, 9 Jun 2020 19:51:40 +0100 Subject: [PATCH 167/245] Remove unnecessary call to update() Also, split a line that pylint complains is too long. --- qutebrowser/config/configfiles.py | 3 ++- qutebrowser/mainwindow/tabwidget.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index da203cc84..c07ad04e9 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -450,7 +450,8 @@ class YamlMigrations(QObject): self._settings[name][scope] = value self.changed.emit() - def _migrate_to_multiple(self, old_name: str, new_names: typing.Iterator[str]) -> None: + def _migrate_to_multiple(self, old_name: str, + new_names: typing.Iterator[str]) -> None: if old_name not in self._settings: return diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index 006bd6cca..60006fa14 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -423,7 +423,6 @@ class TabBar(QTabBar): @pyqtSlot(str) def _on_config_changed(self, option: str) -> None: if option.startswith('fonts.tabs.'): - self.update() self.ensurePolished() self._set_icon_size() elif option == 'tabs.favicons.scale': From 4f96bb0f9530a78fb3f49614524b4d4064c28ec1 Mon Sep 17 00:00:00 2001 From: Constantine Theocharis Date: Wed, 10 Jun 2020 13:36:47 +0100 Subject: [PATCH 168/245] Update qutebrowser/config/configdata.yml Co-authored-by: Marcel Schilling --- qutebrowser/config/configdata.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 68b17fdde..12b4cedc1 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -2875,12 +2875,12 @@ fonts.statusbar: fonts.tabs.selected: default: default_size default_family type: Font - desc: Font used in the tab bar for selected tabs. + desc: Font used for selected tabs. fonts.tabs.unselected: default: default_size default_family type: Font - desc: Font used in the tab bar for unselected tabs. + desc: Font used for unselected tabs. fonts.web.family.standard: default: '' From a867cab03554401e772abba64ebb93c7608f1575 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Jun 2020 14:56:56 +0200 Subject: [PATCH 169/245] Update docs --- doc/changelog.asciidoc | 4 ++++ doc/help/settings.asciidoc | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index f1f5e0dea..e0a2bac8f 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -27,6 +27,8 @@ Changed advanced users. - `:report` now takes two optional arguments for bug/contact information, so that it can be used without the report window popping up. +- The `fonts.tabs` setting has been split into + `fonts.tabs.{selected,unselected}` (see below). Added ~~~~~ @@ -34,6 +36,8 @@ Added - New settings: * `logging.level.ram` and `logging.level.console` to configure the default logging levels via the config. + * `fonts.tabs.selected` and `fonts.tabs.unselected` to set the font of the + selected tab independently from unselected tabs (e.g. to make it bold). Fixed ~~~~~ diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 6cbaff03c..8f40c1444 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -214,8 +214,8 @@ |<>|Font used for warning messages. |<>|Font used for prompts. |<>|Font used in the statusbar. -|<>|Font used in the tab bar for selected tabs. -|<>|Font used in the tab bar for unselected tabs. +|<>|Font used for selected tabs. +|<>|Font used for unselected tabs. |<>|Font family for cursive fonts. |<>|Font family for fantasy fonts. |<>|Font family for fixed fonts. @@ -2790,7 +2790,7 @@ Default: +pass:[default_size default_family]+ [[fonts.tabs.selected]] === fonts.tabs.selected -Font used in the tab bar for selected tabs. +Font used for selected tabs. Type: <> @@ -2798,7 +2798,7 @@ Default: +pass:[default_size default_family]+ [[fonts.tabs.unselected]] === fonts.tabs.unselected -Font used in the tab bar for unselected tabs. +Font used for unselected tabs. Type: <> From c5257184c752c9fc3686e45d88fac0f53e11eb4c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Jun 2020 14:57:17 +0200 Subject: [PATCH 170/245] Rename test_migrate_multiple --- tests/unit/config/test_configfiles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/config/test_configfiles.py b/tests/unit/config/test_configfiles.py index 0f087f37e..e877092fd 100644 --- a/tests/unit/config/test_configfiles.py +++ b/tests/unit/config/test_configfiles.py @@ -588,7 +588,7 @@ class TestYamlMigrations: def test_font_replacements(self, migration_test, setting, old, new): migration_test(setting, old, new) - def test_migrate_multiple(self, yaml, autoconfig): + def test_fonts_tabs(self, yaml, autoconfig): val = '10pt default_family' autoconfig.write({'fonts.tabs': {'global': val}}) From 50a04d5412386604b7771fcfdb4b298e10bca078 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Jun 2020 15:20:46 +0200 Subject: [PATCH 171/245] Update docs --- doc/changelog.asciidoc | 8 ++++++-- doc/help/settings.asciidoc | 4 ++-- qutebrowser/config/configdata.yml | 6 ++---- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index e0a2bac8f..3a6a70ea4 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -27,8 +27,12 @@ Changed advanced users. - `:report` now takes two optional arguments for bug/contact information, so that it can be used without the report window popping up. -- The `fonts.tabs` setting has been split into - `fonts.tabs.{selected,unselected}` (see below). +- Changes to settings: + * `fonts.tabs` has been split into `fonts.tabs.{selected,unselected}` (see + below). + * `statusbar.hide` has been renamed to `statusbar.show` with the possible + values being `always` (`hide = False`), `never` (`hide = True`) or + `in-mode` (new, only show statusbar outside of normal mode. Added ~~~~~ diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 654fe4af0..f15dd6f58 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -3620,8 +3620,8 @@ Type: <> Valid values: * +always+: Always show the statusbar. - * +never+: Always hide the statusbar unless a message is shown. - * +in-mode+: Only show the statusbar when in modes other than normal mode or when a message is shown. + * +never+: Always hide the statusbar. + * +in-mode+: Show the statusbar when in modes other than normal mode. Default: +pass:[always]+ diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 963533a1b..62e14e06b 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -1565,10 +1565,8 @@ statusbar.show: name: String valid_values: - always: Always show the statusbar. - - never: Always hide the statusbar unless a message is shown. - - in-mode: >- - Only show the statusbar when in modes other than normal mode or - when a message is shown. + - never: Always hide the statusbar. + - in-mode: Show the statusbar when in modes other than normal mode. desc: When to show the statusbar. statusbar.padding: From df49a8f3e433afd47da6bf9b0ce1e132b6881bd2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Jun 2020 15:56:26 +0200 Subject: [PATCH 172/245] tests: Add testutils.change_cwd() --- tests/helpers/utils.py | 12 ++++++++++++ tests/unit/misc/test_ipc.py | 16 +++++++--------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/tests/helpers/utils.py b/tests/helpers/utils.py index 25c3fb9a1..70a10436c 100644 --- a/tests/helpers/utils.py +++ b/tests/helpers/utils.py @@ -25,6 +25,7 @@ import gzip import pprint import os.path import contextlib +import pathlib import pytest @@ -186,6 +187,17 @@ def nop_contextmanager(): yield +@contextlib.contextmanager +def change_cwd(path): + """Use a path as current working directory.""" + old_cwd = pathlib.Path.cwd() + os.chdir(str(path)) + try: + yield + finally: + os.chdir(str(old_cwd)) + + @contextlib.contextmanager def ignore_bs4_warning(): """WORKAROUND for https://bugs.launchpad.net/beautifulsoup/+bug/1847592.""" diff --git a/tests/unit/misc/test_ipc.py b/tests/unit/misc/test_ipc.py index 1da88d3bb..a598043b0 100644 --- a/tests/unit/misc/test_ipc.py +++ b/tests/unit/misc/test_ipc.py @@ -36,7 +36,7 @@ from PyQt5.QtTest import QSignalSpy import qutebrowser from qutebrowser.misc import ipc from qutebrowser.utils import standarddir, utils -from helpers import stubs +from helpers import stubs, utils as testutils pytestmark = pytest.mark.usefixtures('qapp') @@ -513,14 +513,12 @@ class TestSendToRunningInstance: timeout=5000) as blocker: with qtbot.waitSignal(ipc_server.got_raw, timeout=5000) as raw_blocker: - old_cwd = pathlib.Path.cwd() - os.chdir(str(tmp_path)) - if not has_cwd: - m = mocker.patch('qutebrowser.misc.ipc.os') - m.getcwd.side_effect = OSError - sent = ipc.send_to_running_instance( - 'qute-test', ['foo'], None) - os.chdir(str(old_cwd)) + with testutils.change_cwd(tmp_path): + if not has_cwd: + m = mocker.patch('qutebrowser.misc.ipc.os') + m.getcwd.side_effect = OSError + sent = ipc.send_to_running_instance( + 'qute-test', ['foo'], None) assert sent From 3900c359ec920fa54f4f12535bdd0678cf514664 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Jun 2020 15:57:34 +0200 Subject: [PATCH 173/245] tests: Fix test_configinit after #5512 --- tests/unit/config/test_configinit.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/unit/config/test_configinit.py b/tests/unit/config/test_configinit.py index 2e149fa70..d25dd3596 100644 --- a/tests/unit/config/test_configinit.py +++ b/tests/unit/config/test_configinit.py @@ -344,12 +344,12 @@ class TestLateInit: # fonts.default_family and font settings customized # https://github.com/qutebrowser/qutebrowser/issues/3096 ([('fonts.default_family', 'Comic Sans MS'), - ('fonts.tabs', '12pt default_family'), + ('fonts.debug_console', '12pt default_family'), ('fonts.keyhint', '12pt default_family')], 12, 'Comic Sans MS'), # as above, but with default_size ([('fonts.default_family', 'Comic Sans MS'), ('fonts.default_size', '23pt'), - ('fonts.tabs', 'default_size default_family'), + ('fonts.debug_console', 'default_size default_family'), ('fonts.keyhint', 'default_size default_family')], 23, 'Comic Sans MS'), ]) @@ -382,7 +382,7 @@ class TestLateInit: expected = '{}pt "{}"'.format(size, family) assert config.instance.get('fonts.keyhint') == expected # QtFont - font = config.instance.get('fonts.tabs') + font = config.instance.get('fonts.debug_console') assert font.pointSize() == size assert font.family() == family @@ -405,10 +405,10 @@ class TestLateInit: assert 'fonts.keyhint' in changed_options # Font assert config.instance.get('fonts.keyhint') == '23pt "Comic Sans MS"' - assert 'fonts.tabs' in changed_options # QtFont - tabs_font = config.instance.get('fonts.tabs') - assert tabs_font.family() == 'Comic Sans MS' - assert tabs_font.pointSize() == 23 + assert 'fonts.debug_console' in changed_options # QtFont + debug_console_font = config.instance.get('fonts.debug_console') + assert debug_console_font.family() == 'Comic Sans MS' + assert debug_console_font.pointSize() == 23 # Font subclass, but doesn't end with "default_family" assert 'fonts.web.family.standard' not in changed_options From 1e413e814ce4fdb8318a63de5793e3af42333c49 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Jun 2020 15:59:56 +0200 Subject: [PATCH 174/245] mypy: Fix type for _migrate_to_multiple --- qutebrowser/config/configfiles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index 635702b01..01f58e5d1 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -456,7 +456,7 @@ class YamlMigrations(QObject): self.changed.emit() def _migrate_to_multiple(self, old_name: str, - new_names: typing.Iterator[str]) -> None: + new_names: typing.Iterable[str]) -> None: if old_name not in self._settings: return From e7cdc03685a35f8e96435c4f76fd233dc98e589d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Jun 2020 16:13:45 +0200 Subject: [PATCH 175/245] tests: Cleanups after pathlib migration in tests/unit/scripts/ --- tests/unit/scripts/test_check_coverage.py | 3 +-- tests/unit/scripts/test_run_vulture.py | 14 ++++++-------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/tests/unit/scripts/test_check_coverage.py b/tests/unit/scripts/test_check_coverage.py index a60c95525..cc16736f3 100644 --- a/tests/unit/scripts/test_check_coverage.py +++ b/tests/unit/scripts/test_check_coverage.py @@ -226,6 +226,5 @@ def _generate_files(): @pytest.mark.parametrize('filename', list(_generate_files())) def test_files_exist(filename): - basedir = (pathlib.Path(check_coverage.__file__).parent / - os.pardir / os.pardir) + basedir = pathlib.Path(check_coverage.__file__).parents[2] assert (basedir / filename).exists() diff --git a/tests/unit/scripts/test_run_vulture.py b/tests/unit/scripts/test_run_vulture.py index 0008f3147..217233aae 100644 --- a/tests/unit/scripts/test_run_vulture.py +++ b/tests/unit/scripts/test_run_vulture.py @@ -25,6 +25,8 @@ import textwrap import pytest +from tests.helpers import utils + try: from scripts.dev import run_vulture except ImportError: @@ -51,14 +53,10 @@ class VultureDir: def run(self): """Run vulture over all generated files and return the output.""" - #files = self._tmp_path.listdir() - files = list(self._tmp_path.glob('*')) - assert files - old_cwd = pathlib.Path.cwd() - os.chdir(str(self._tmp_path)) - return_value = run_vulture.run([e.name for e in files]) - os.chdir(str(old_cwd)) - return return_value + names = [p.name for p in self._tmp_path.glob('*')] + assert names + with utils.change_cwd(self._tmp_path): + return run_vulture.run(names) def makepyfile(self, **kwargs): """Create a python file, similar to TestDir.makepyfile.""" From 846adef2113680a383b9395cdaf5a92ad984e308 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Jun 2020 16:23:43 +0200 Subject: [PATCH 176/245] tests: Remove unused import --- tests/unit/scripts/test_check_coverage.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/scripts/test_check_coverage.py b/tests/unit/scripts/test_check_coverage.py index cc16736f3..d805eb184 100644 --- a/tests/unit/scripts/test_check_coverage.py +++ b/tests/unit/scripts/test_check_coverage.py @@ -18,7 +18,6 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -import os import sys import pathlib From abfd975ae98538200fa0d8a9d031383fcb6a3500 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Jun 2020 17:01:59 +0200 Subject: [PATCH 177/245] Use debug logging for log config messages See #5445 --- qutebrowser/utils/log.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 58311693f..d1f654d8f 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -542,18 +542,18 @@ def init_from_config(conf: 'configmodule.ConfigContainer') -> None: """ assert _args is not None if _args.debug: - init.info("--debug flag overrides log configs") + init.debug("--debug flag overrides log configs") return if ram_handler: ramlevel = conf.logging.level.ram - init.info("Configuring RAM loglevel to %s", ramlevel) + init.debug("Configuring RAM loglevel to %s", ramlevel) ram_handler.setLevel(LOG_LEVELS[ramlevel.upper()]) if console_handler: consolelevel = conf.logging.level.console if _args.loglevel: - init.info("--loglevel flag overrides logging.level.console") + init.debug("--loglevel flag overrides logging.level.console") else: - init.info("Configuring console loglevel to %s", consolelevel) + init.debug("Configuring console loglevel to %s", consolelevel) console_handler.setLevel(LOG_LEVELS[consolelevel.upper()]) From 011dee7c7ae92f86f2e882eae2e2f961e447bde5 Mon Sep 17 00:00:00 2001 From: Constantine Theocharis Date: Wed, 10 Jun 2020 16:08:05 +0100 Subject: [PATCH 178/245] Remove QtFont as a config type --- doc/help/settings.asciidoc | 5 +- qutebrowser/config/configdata.yml | 2 +- qutebrowser/config/configtypes.py | 92 +-------------------------- qutebrowser/misc/consolewidget.py | 30 ++++----- tests/unit/config/test_configfiles.py | 2 - tests/unit/config/test_configinit.py | 10 --- tests/unit/config/test_configtypes.py | 53 +++------------ 7 files changed, 26 insertions(+), 168 deletions(-) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index f15dd6f58..7642391e7 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -2700,7 +2700,7 @@ Default: empty === fonts.debug_console Font used for the debugging console. -Type: <> +Type: <> Default: +pass:[default_size default_family]+ @@ -4234,9 +4234,6 @@ A value can be in one of the following formats: * `#RGB`/`#RRGGBB`/`#RRRGGGBBB`/ |QtColor|A color value. A value can be in one of the following formats: * `#RGB`/`#RRGGBB`/`#RRRGGGBBB`/`#RRRRGGGGBBBB` * An SVG color name as specified in http://www.w3.org/TR/SVG/types.html#ColorKeywords[the W3C specification]. * transparent (no color) * `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or percentages) * `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359) -|QtFont|A font family, with optional style/weight/size. - -* Style: `normal`/`italic`/`oblique` * Weight: `normal`, `bold`, `100`..`900` * Size: _number_ `px`/`pt` |Regex|A regular expression. When setting from `config.py`, both a string or a `re.compile(...)` object are valid. diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 62e14e06b..f709a8e28 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -2834,7 +2834,7 @@ fonts.contextmenu: fonts.debug_console: default: default_size default_family - type: QtFont + type: Font desc: Font used for the debugging console. fonts.downloads: diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index c8d100a7b..0f489acd5 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -1157,7 +1157,7 @@ class QssColor(BaseType): class FontBase(BaseType): - """Base class for Font/QtFont/FontFamily.""" + """Base class for Font/FontFamily.""" # Gets set when the config is initialized. default_family = None # type: str @@ -1292,96 +1292,6 @@ class FontFamily(FontBase): return value -class QtFont(FontBase): - - """A Font which gets converted to a QFont.""" - - __doc__ = Font.__doc__ # for src2asciidoc.py - - def _parse_families(self, family_str: str) -> configutils.FontFamilies: - if family_str == 'default_family' and self.default_family is not None: - family_str = self.default_family - - return configutils.FontFamilies.from_str(family_str) - - def _set_style(self, font: QFont, match: typing.Match) -> None: - style = match.group('style') - style_map = { - 'normal': QFont.StyleNormal, - 'italic': QFont.StyleItalic, - 'oblique': QFont.StyleOblique, - } - if style: - font.setStyle(style_map[style]) - else: - font.setStyle(QFont.StyleNormal) - - def _set_weight(self, font: QFont, match: typing.Match) -> None: - weight = match.group('weight') - namedweight = match.group('namedweight') - weight_map = { - 'normal': QFont.Normal, - 'bold': QFont.Bold, - } - if namedweight: - font.setWeight(weight_map[namedweight]) - elif weight: - # based on qcssparser.cpp:setFontWeightFromValue - font.setWeight(min(int(weight) // 8, 99)) - else: - font.setWeight(QFont.Normal) - - def _set_size(self, font: QFont, match: typing.Match) -> None: - size = match.group('size') - if size: - if size == 'default_size': - size = self.default_size - - if size is None: - # initial validation before default_size is set up. - pass - elif size.lower().endswith('pt'): - font.setPointSizeF(float(size[:-2])) - elif size.lower().endswith('px'): - font.setPixelSize(int(size[:-2])) - else: - # This should never happen as the regex only lets pt/px - # through. - raise ValueError("Unexpected size unit in {!r}!".format( - size)) # pragma: no cover - - def _set_families(self, font: QFont, match: typing.Match) -> None: - family_str = match.group('family') - families = self._parse_families(family_str) - if hasattr(font, 'setFamilies'): - # Added in Qt 5.13 - font.setFamily(families.family) # type: ignore[arg-type] - font.setFamilies(list(families)) - else: # pragma: no cover - font.setFamily(families.to_str(quote=False)) - - def to_py(self, value: _StrUnset) -> typing.Union[_UnsetNone, QFont]: - self._basic_py_validation(value, str) - if isinstance(value, usertypes.Unset): - return value - elif not value: - return None - - match = self.font_regex.fullmatch(value) - if not match: # pragma: no cover - # This should never happen, as the regex always matches everything - # as family. - raise configexc.ValidationError(value, "must be a valid font") - - font = QFont() - self._set_style(font, match) - self._set_weight(font, match) - self._set_size(font, match) - self._set_families(font, match) - - return font - - class Regex(BaseType): """A regular expression. diff --git a/qutebrowser/misc/consolewidget.py b/qutebrowser/misc/consolewidget.py index 7e46da3c8..4132244c9 100644 --- a/qutebrowser/misc/consolewidget.py +++ b/qutebrowser/misc/consolewidget.py @@ -27,7 +27,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt from PyQt5.QtWidgets import QTextEdit, QWidget, QVBoxLayout, QApplication from PyQt5.QtGui import QTextCursor -from qutebrowser.config import config +from qutebrowser.config import config, stylesheet from qutebrowser.misc import cmdhistory, miscwidgets from qutebrowser.utils import utils, objreg @@ -48,6 +48,12 @@ class ConsoleLineEdit(miscwidgets.CommandLineEdit): execute = pyqtSignal(str) + STYLESHEET = """ + ConsoleLineEdit { + font: {{ conf.fonts.debug_console }}; + } + """ + def __init__(self, _namespace, parent): """Constructor. @@ -55,8 +61,7 @@ class ConsoleLineEdit(miscwidgets.CommandLineEdit): _namespace: The local namespace of the interpreter. """ super().__init__(parent=parent) - self._update_font() - config.instance.changed.connect(self._update_font) + stylesheet.set_register(self) self._history = cmdhistory.History(parent=self) self.returnPressed.connect(self.on_return_pressed) @@ -106,32 +111,27 @@ class ConsoleLineEdit(miscwidgets.CommandLineEdit): else: super().keyPressEvent(e) - @config.change_filter('fonts.debug_console') - def _update_font(self): - """Set the correct font.""" - self.setFont(config.val.fonts.debug_console) - class ConsoleTextEdit(QTextEdit): """Custom QTextEdit for console output.""" + STYLESHEET = """ + ConsoleTextEdit { + font: {{ conf.fonts.debug_console }}; + } + """ + def __init__(self, parent=None): super().__init__(parent) self.setAcceptRichText(False) self.setReadOnly(True) - config.instance.changed.connect(self._update_font) - self._update_font() + stylesheet.set_register(self) self.setFocusPolicy(Qt.ClickFocus) def __repr__(self): return utils.get_repr(self) - @config.change_filter('fonts.debug_console') - def _update_font(self): - """Update font when config changed.""" - self.setFont(config.val.fonts.debug_console) - def append_text(self, text): """Append new text and scroll output to bottom. diff --git a/tests/unit/config/test_configfiles.py b/tests/unit/config/test_configfiles.py index e877092fd..3cd0c3339 100644 --- a/tests/unit/config/test_configfiles.py +++ b/tests/unit/config/test_configfiles.py @@ -578,8 +578,6 @@ class TestYamlMigrations: @pytest.mark.parametrize('setting, old, new', [ # Font ('fonts.hints', '10pt monospace', '10pt default_family'), - # QtFont - ('fonts.debug_console', '10pt monospace', '10pt default_family'), # String ('content.headers.accept_language', 'x monospace', 'x monospace'), # Not at end of string diff --git a/tests/unit/config/test_configinit.py b/tests/unit/config/test_configinit.py index d25dd3596..6e95cadbe 100644 --- a/tests/unit/config/test_configinit.py +++ b/tests/unit/config/test_configinit.py @@ -344,12 +344,10 @@ class TestLateInit: # fonts.default_family and font settings customized # https://github.com/qutebrowser/qutebrowser/issues/3096 ([('fonts.default_family', 'Comic Sans MS'), - ('fonts.debug_console', '12pt default_family'), ('fonts.keyhint', '12pt default_family')], 12, 'Comic Sans MS'), # as above, but with default_size ([('fonts.default_family', 'Comic Sans MS'), ('fonts.default_size', '23pt'), - ('fonts.debug_console', 'default_size default_family'), ('fonts.keyhint', 'default_size default_family')], 23, 'Comic Sans MS'), ]) @@ -381,10 +379,6 @@ class TestLateInit: # Font expected = '{}pt "{}"'.format(size, family) assert config.instance.get('fonts.keyhint') == expected - # QtFont - font = config.instance.get('fonts.debug_console') - assert font.pointSize() == size - assert font.family() == family @pytest.fixture def run_configinit(self, init_patch, fake_save_manager, args): @@ -405,10 +399,6 @@ class TestLateInit: assert 'fonts.keyhint' in changed_options # Font assert config.instance.get('fonts.keyhint') == '23pt "Comic Sans MS"' - assert 'fonts.debug_console' in changed_options # QtFont - debug_console_font = config.instance.get('fonts.debug_console') - assert debug_console_font.family() == 'Comic Sans MS' - assert debug_console_font.pointSize() == 23 # Font subclass, but doesn't end with "default_family" assert 'fonts.web.family.standard' not in changed_options diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index 841892ef2..5b5d159a6 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -255,8 +255,8 @@ class TestAll: # For some types, we don't actually get the internal (YAML-like) value # back from from_str(), so we can't convert it back. - if klass in [configtypes.FuzzyUrl, configtypes.QtFont, - configtypes.ShellCommand, configtypes.Url]: + if klass in [configtypes.FuzzyUrl, configtypes.ShellCommand, + configtypes.Url]: return converted = typ.to_str(val) @@ -1381,7 +1381,7 @@ class FontDesc: class TestFont: - """Test Font/QtFont.""" + """Test Font.""" TESTS = { # (style, weight, pointsize, pixelsize, family @@ -1430,7 +1430,7 @@ class TestFont: font_xfail = pytest.mark.xfail(reason='FIXME: #103') - @pytest.fixture(params=[configtypes.Font, configtypes.QtFont]) + @pytest.fixture(params=[configtypes.Font]) def klass(self, request): return request.param @@ -1438,45 +1438,12 @@ class TestFont: def font_class(self): return configtypes.Font - @pytest.fixture - def qtfont_class(self): - return configtypes.QtFont - @pytest.mark.parametrize('val, desc', sorted(TESTS.items())) def test_to_py_valid(self, klass, val, desc): - if klass is configtypes.Font: - expected = val - elif klass is configtypes.QtFont: - expected = Font.fromdesc(desc) + assert klass is configtypes.Font + expected = val assert klass().to_py(val) == expected - def test_qtfont(self, qtfont_class): - """Test QtFont's to_py.""" - value = Font(qtfont_class().to_py('10pt "Foobar Neue", Fubar')) - - if hasattr(value, 'families'): - # Added in Qt 5.13 - assert value.family() == 'Foobar Neue' - assert value.families() == ['Foobar Neue', 'Fubar'] - else: - assert value.family() == 'Foobar Neue, Fubar' - - assert value.weight() == QFont.Normal - assert value.style() == QFont.StyleNormal - - assert value.pointSize() == 10 - - def test_qtfont_float(self, qtfont_class): - """Test QtFont's to_py with a float as point size. - - We can't test the point size for equality as Qt seems to do some - rounding as appropriate. - """ - value = Font(qtfont_class().to_py('10.5pt Test')) - assert value.family() == 'Test' - assert value.pointSize() >= 10 - assert value.pointSize() <= 11 - @pytest.mark.parametrize('val', [ pytest.param('green "Foobar Neue"', marks=font_xfail), pytest.param('italic green "Foobar Neue"', marks=font_xfail), @@ -1495,12 +1462,8 @@ class TestFont: def test_defaults_replacement(self, klass, monkeypatch): configtypes.FontBase.set_defaults(['Terminus'], '23pt') - if klass is configtypes.Font: - expected = '23pt Terminus' - elif klass is configtypes.QtFont: - desc = FontDesc(QFont.StyleNormal, QFont.Normal, 23, None, - 'Terminus') - expected = Font.fromdesc(desc) + assert klass is configtypes.Font + expected = '23pt Terminus' assert klass().to_py('23pt default_family') == expected From 946e52903e482fb26d54b50ca3e74f70e5017cdf Mon Sep 17 00:00:00 2001 From: Constantine Theocharis Date: Wed, 10 Jun 2020 16:29:48 +0100 Subject: [PATCH 179/245] Remove unneeded imports --- qutebrowser/config/configtypes.py | 2 +- qutebrowser/misc/consolewidget.py | 2 +- tests/unit/scripts/test_run_vulture.py | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 0f489acd5..e798498fc 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -56,7 +56,7 @@ import typing import attr import yaml from PyQt5.QtCore import QUrl, Qt -from PyQt5.QtGui import QColor, QFont, QFontDatabase +from PyQt5.QtGui import QColor, QFontDatabase from PyQt5.QtWidgets import QTabWidget, QTabBar, QApplication from PyQt5.QtNetwork import QNetworkProxy diff --git a/qutebrowser/misc/consolewidget.py b/qutebrowser/misc/consolewidget.py index 4132244c9..f4369274a 100644 --- a/qutebrowser/misc/consolewidget.py +++ b/qutebrowser/misc/consolewidget.py @@ -27,7 +27,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt from PyQt5.QtWidgets import QTextEdit, QWidget, QVBoxLayout, QApplication from PyQt5.QtGui import QTextCursor -from qutebrowser.config import config, stylesheet +from qutebrowser.config import stylesheet from qutebrowser.misc import cmdhistory, miscwidgets from qutebrowser.utils import utils, objreg diff --git a/tests/unit/scripts/test_run_vulture.py b/tests/unit/scripts/test_run_vulture.py index 217233aae..edf3451cb 100644 --- a/tests/unit/scripts/test_run_vulture.py +++ b/tests/unit/scripts/test_run_vulture.py @@ -18,8 +18,6 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -import os -import pathlib import sys import textwrap From 29887dfc607c94dcaad268f5b64de99e01a76afe Mon Sep 17 00:00:00 2001 From: Constantine Theocharis Date: Wed, 10 Jun 2020 16:29:59 +0100 Subject: [PATCH 180/245] Simplify some tests --- tests/unit/config/test_configtypes.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index 5b5d159a6..5e82333c9 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -1430,9 +1430,9 @@ class TestFont: font_xfail = pytest.mark.xfail(reason='FIXME: #103') - @pytest.fixture(params=[configtypes.Font]) + @pytest.fixture def klass(self, request): - return request.param + return configtypes.Font @pytest.fixture def font_class(self): @@ -1440,7 +1440,6 @@ class TestFont: @pytest.mark.parametrize('val, desc', sorted(TESTS.items())) def test_to_py_valid(self, klass, val, desc): - assert klass is configtypes.Font expected = val assert klass().to_py(val) == expected @@ -1462,7 +1461,6 @@ class TestFont: def test_defaults_replacement(self, klass, monkeypatch): configtypes.FontBase.set_defaults(['Terminus'], '23pt') - assert klass is configtypes.Font expected = '23pt Terminus' assert klass().to_py('23pt default_family') == expected From 32c0bd1478e8f6b752861b869ef1fe405f3a356f Mon Sep 17 00:00:00 2001 From: Constantine Theocharis Date: Wed, 10 Jun 2020 17:33:09 +0100 Subject: [PATCH 181/245] Simplify test_configtypes.py --- tests/unit/config/test_configtypes.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index 5e82333c9..37a5580d0 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -1381,8 +1381,6 @@ class FontDesc: class TestFont: - """Test Font.""" - TESTS = { # (style, weight, pointsize, pixelsize, family '"Foobar Neue"': @@ -1440,8 +1438,7 @@ class TestFont: @pytest.mark.parametrize('val, desc', sorted(TESTS.items())) def test_to_py_valid(self, klass, val, desc): - expected = val - assert klass().to_py(val) == expected + assert klass().to_py(val) == val @pytest.mark.parametrize('val', [ pytest.param('green "Foobar Neue"', marks=font_xfail), @@ -1461,8 +1458,7 @@ class TestFont: def test_defaults_replacement(self, klass, monkeypatch): configtypes.FontBase.set_defaults(['Terminus'], '23pt') - expected = '23pt Terminus' - assert klass().to_py('23pt default_family') == expected + assert klass().to_py('23pt default_family') == '23pt Terminus' class TestFontFamily: From 2af00b6bf2d0cbd867867dcadde4a815823d1acc Mon Sep 17 00:00:00 2001 From: Constantine Theocharis Date: Wed, 10 Jun 2020 17:39:57 +0100 Subject: [PATCH 182/245] Move stylesheet to ConsoleWidget to avoid duplication --- qutebrowser/misc/consolewidget.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/qutebrowser/misc/consolewidget.py b/qutebrowser/misc/consolewidget.py index f4369274a..aed42237a 100644 --- a/qutebrowser/misc/consolewidget.py +++ b/qutebrowser/misc/consolewidget.py @@ -48,12 +48,6 @@ class ConsoleLineEdit(miscwidgets.CommandLineEdit): execute = pyqtSignal(str) - STYLESHEET = """ - ConsoleLineEdit { - font: {{ conf.fonts.debug_console }}; - } - """ - def __init__(self, _namespace, parent): """Constructor. @@ -61,7 +55,6 @@ class ConsoleLineEdit(miscwidgets.CommandLineEdit): _namespace: The local namespace of the interpreter. """ super().__init__(parent=parent) - stylesheet.set_register(self) self._history = cmdhistory.History(parent=self) self.returnPressed.connect(self.on_return_pressed) @@ -116,17 +109,10 @@ class ConsoleTextEdit(QTextEdit): """Custom QTextEdit for console output.""" - STYLESHEET = """ - ConsoleTextEdit { - font: {{ conf.fonts.debug_console }}; - } - """ - def __init__(self, parent=None): super().__init__(parent) self.setAcceptRichText(False) self.setReadOnly(True) - stylesheet.set_register(self) self.setFocusPolicy(Qt.ClickFocus) def __repr__(self): @@ -157,6 +143,12 @@ class ConsoleWidget(QWidget): _interpreter: The InteractiveInterpreter to execute code with. """ + STYLESHEET = """ + ConsoleWidget > ConsoleTextEdit, ConsoleWidget > ConsoleLineEdit { + font: {{ conf.fonts.debug_console }}; + } + """ + def __init__(self, parent=None): super().__init__(parent) if not hasattr(sys, 'ps1'): @@ -182,6 +174,7 @@ class ConsoleWidget(QWidget): self._vbox.setSpacing(0) self._vbox.addWidget(self._output) self._vbox.addWidget(self._lineedit) + stylesheet.set_register(self) self.setLayout(self._vbox) self._lineedit.setFocus() self._interpreter = code.InteractiveInterpreter(namespace) From c40cb30855231dd0eb09e0a7c94b3f772ded87c7 Mon Sep 17 00:00:00 2001 From: Constantine Theocharis Date: Wed, 10 Jun 2020 17:46:05 +0100 Subject: [PATCH 183/245] Remove unnecessary argument in TestFont.klass --- tests/unit/config/test_configtypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index 37a5580d0..8b0c4b191 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -1429,7 +1429,7 @@ class TestFont: font_xfail = pytest.mark.xfail(reason='FIXME: #103') @pytest.fixture - def klass(self, request): + def klass(self): return configtypes.Font @pytest.fixture From 161b866b75544a6d615b97f7cea3614c2a9d3924 Mon Sep 17 00:00:00 2001 From: Pierguido Lambri Date: Wed, 10 Jun 2020 18:31:55 +0100 Subject: [PATCH 184/245] Added instructions on how to install the Fedora debuginfo packages Signed-off-by: Pierguido Lambri --- doc/stacktrace.asciidoc | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/doc/stacktrace.asciidoc b/doc/stacktrace.asciidoc index f38c54940..a50a563b6 100644 --- a/doc/stacktrace.asciidoc +++ b/doc/stacktrace.asciidoc @@ -34,6 +34,27 @@ is available in the repositories: # apt-get install python3-pyqt5-dbg python3-pyqt5.qtwebkit-dbg python3-dbg libqt5webkit5-dbg ---- +Fedora +^^^^^^^^^^^^^^^^^ + +For Fedora you first need to install the dnf/yum-utils: + +---- +# sudo dnf install dnf-utils +---- + +Or: + +---- +# sudo yum install yum-utils +---- + +Then install the needed debuginfo packages: + +---- +# sudo dnf install python3 qt5-qtwebkit python3-qt5-webkit python3-qt5-base python-qt5 python3-qt5 +---- + Archlinux ^^^^^^^^^ From ed7d0c037cf85e900d8b56b646b31a29cc88fa38 Mon Sep 17 00:00:00 2001 From: Pierguido Lambri Date: Wed, 10 Jun 2020 18:33:16 +0100 Subject: [PATCH 185/245] Fixed a wrong formatting Signed-off-by: Pierguido Lambri --- doc/stacktrace.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/stacktrace.asciidoc b/doc/stacktrace.asciidoc index a50a563b6..d812cc26b 100644 --- a/doc/stacktrace.asciidoc +++ b/doc/stacktrace.asciidoc @@ -35,7 +35,7 @@ is available in the repositories: ---- Fedora -^^^^^^^^^^^^^^^^^ +^^^^^^ For Fedora you first need to install the dnf/yum-utils: From 09fab1e38356dc6d207bb4ad50d67fb210c31298 Mon Sep 17 00:00:00 2001 From: Pierguido Lambri Date: Wed, 10 Jun 2020 18:44:40 +0100 Subject: [PATCH 186/245] And fixed with the right command Signed-off-by: Pierguido Lambri --- doc/stacktrace.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/stacktrace.asciidoc b/doc/stacktrace.asciidoc index d812cc26b..97660c931 100644 --- a/doc/stacktrace.asciidoc +++ b/doc/stacktrace.asciidoc @@ -52,7 +52,7 @@ Or: Then install the needed debuginfo packages: ---- -# sudo dnf install python3 qt5-qtwebkit python3-qt5-webkit python3-qt5-base python-qt5 python3-qt5 +# sudo debuginfo-install python3 qt5-qtwebkit python3-qt5-webkit python3-qt5-base python-qt5 python3-qt5 ---- Archlinux From 44e16ec2c66ecae36e7589ba4d2f731d21a92a8d Mon Sep 17 00:00:00 2001 From: Pierguido Lambri Date: Wed, 10 Jun 2020 19:22:27 +0100 Subject: [PATCH 187/245] Removed the sudo command Signed-off-by: Pierguido Lambri --- doc/stacktrace.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/stacktrace.asciidoc b/doc/stacktrace.asciidoc index 97660c931..ef37c80b4 100644 --- a/doc/stacktrace.asciidoc +++ b/doc/stacktrace.asciidoc @@ -40,13 +40,13 @@ Fedora For Fedora you first need to install the dnf/yum-utils: ---- -# sudo dnf install dnf-utils +# dnf install dnf-utils ---- Or: ---- -# sudo yum install yum-utils +# yum install yum-utils ---- Then install the needed debuginfo packages: From 6f545d30481a28b99904068b6aa2773b12bdb9d0 Mon Sep 17 00:00:00 2001 From: Pierguido Lambri Date: Wed, 10 Jun 2020 19:23:28 +0100 Subject: [PATCH 188/245] Fixed the debuginfo command, removed the sudo command and installed the right debug symbol packages for qtwebengine Signed-off-by: Pierguido Lambri --- doc/stacktrace.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/stacktrace.asciidoc b/doc/stacktrace.asciidoc index ef37c80b4..3bcca49bf 100644 --- a/doc/stacktrace.asciidoc +++ b/doc/stacktrace.asciidoc @@ -52,7 +52,7 @@ Or: Then install the needed debuginfo packages: ---- -# sudo debuginfo-install python3 qt5-qtwebkit python3-qt5-webkit python3-qt5-base python-qt5 python3-qt5 +# debuginfo-install python3 qt5-qtwebengine python3-qt5-webengine python3-qt5-base python-qt5 python3-qt5 python3-qt5-webkit-debuginfo ---- Archlinux From ec7dd49e16d37004fb8ce0fd433984b9e574097d Mon Sep 17 00:00:00 2001 From: Pierguido Lambri Date: Wed, 10 Jun 2020 19:30:34 +0100 Subject: [PATCH 189/245] Removed the useless debuginfo suffix Signed-off-by: Pierguido Lambri --- doc/stacktrace.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/stacktrace.asciidoc b/doc/stacktrace.asciidoc index 3bcca49bf..4dc327e0e 100644 --- a/doc/stacktrace.asciidoc +++ b/doc/stacktrace.asciidoc @@ -52,7 +52,7 @@ Or: Then install the needed debuginfo packages: ---- -# debuginfo-install python3 qt5-qtwebengine python3-qt5-webengine python3-qt5-base python-qt5 python3-qt5 python3-qt5-webkit-debuginfo +# debuginfo-install python3 qt5-qtwebengine python3-qt5-webengine python3-qt5-base python-qt5 python3-qt5 python3-qt5-webkit ---- Archlinux From 8d80910373ecf2395729a5964f8e606897879e9a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Jun 2020 20:41:36 +0200 Subject: [PATCH 190/245] Update changelog --- doc/changelog.asciidoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 3a6a70ea4..b7ebcb83f 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -33,6 +33,10 @@ Changed * `statusbar.hide` has been renamed to `statusbar.show` with the possible values being `always` (`hide = False`), `never` (`hide = True`) or `in-mode` (new, only show statusbar outside of normal mode. + * The `QtFont` config type formerly used for `fonts.tabs` and + `fonts.debug_console` is now removed and entirely replaced by `Font`. The + former distinction was mainly an implementation detail, and the accepted + values shouldn't have changed. Added ~~~~~ From 0af3a8a4675f876121db22b07a21f23e09d9048a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Jun 2020 21:30:13 +0200 Subject: [PATCH 191/245] Update docs --- doc/changelog.asciidoc | 2 ++ doc/help/settings.asciidoc | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index b7ebcb83f..f52f9b02a 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -27,6 +27,8 @@ Changed advanced users. - `:report` now takes two optional arguments for bug/contact information, so that it can be used without the report window popping up. +- New `t[Cc][Hh]` default bindings which work similarly to the `t[Ss][Hh]` + bindings for JavaScript but toggle cookie permissions. - Changes to settings: * `fonts.tabs` has been split into `fonts.tabs.{selected,unselected}` (see below). diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 7642391e7..466b0ec1b 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -651,6 +651,9 @@ Default: * +pass:[sk]+: +pass:[set-cmd-text -s :bind]+ * +pass:[sl]+: +pass:[set-cmd-text -s :set -t]+ * +pass:[ss]+: +pass:[set-cmd-text -s :set]+ +* +pass:[tCH]+: +pass:[config-cycle -p -u *://*.{url:host}/* content.cookies.accept all no-3rdparty never ;; reload]+ +* +pass:[tCh]+: +pass:[config-cycle -p -u *://{url:host}/* content.cookies.accept all no-3rdparty never ;; reload]+ +* +pass:[tCu]+: +pass:[config-cycle -p -u {url} content.cookies.accept all no-3rdparty never ;; reload]+ * +pass:[tIH]+: +pass:[config-cycle -p -u *://*.{url:host}/* content.images ;; reload]+ * +pass:[tIh]+: +pass:[config-cycle -p -u *://{url:host}/* content.images ;; reload]+ * +pass:[tIu]+: +pass:[config-cycle -p -u {url} content.images ;; reload]+ @@ -660,6 +663,9 @@ Default: * +pass:[tSH]+: +pass:[config-cycle -p -u *://*.{url:host}/* content.javascript.enabled ;; reload]+ * +pass:[tSh]+: +pass:[config-cycle -p -u *://{url:host}/* content.javascript.enabled ;; reload]+ * +pass:[tSu]+: +pass:[config-cycle -p -u {url} content.javascript.enabled ;; reload]+ +* +pass:[tcH]+: +pass:[config-cycle -p -t -u *://*.{url:host}/* content.cookies.accept all no-3rdparty never ;; reload]+ +* +pass:[tch]+: +pass:[config-cycle -p -t -u *://{url:host}/* content.cookies.accept all no-3rdparty never ;; reload]+ +* +pass:[tcu]+: +pass:[config-cycle -p -t -u {url} content.cookies.accept all no-3rdparty never ;; reload]+ * +pass:[th]+: +pass:[back -t]+ * +pass:[tiH]+: +pass:[config-cycle -p -t -u *://*.{url:host}/* content.images ;; reload]+ * +pass:[tih]+: +pass:[config-cycle -p -t -u *://{url:host}/* content.images ;; reload]+ From fab5f5137353295906e63a28051f6770439962e7 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Wed, 10 Jun 2020 22:37:54 -0400 Subject: [PATCH 192/245] Enable configuring loglevel at runtime. Now `:set logging.level.{ram,console}` will work after qutebrowser has started. This allows us to derecate debug-set-loglevel. Logging is initialized too early for config.change_filter to work, so I had to hack it. --- qutebrowser/app.py | 9 +++++++++ qutebrowser/misc/utilcmds.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 2c164fb96..abb6dfa76 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -373,6 +373,13 @@ def open_desktopservices_url(url): tabbed_browser.tabopen(url) +# This is effectively a @config.change_filter +# Howerver, logging is initialized too early to use that annotation +def _on_config_changed(name: str) -> None: + if name.startswith('logging.'): + log.init_from_config(config.val) + + def _init_modules(*, args): """Initialize all 'modules' which need to be initialized. @@ -381,6 +388,8 @@ def _init_modules(*, args): """ log.init.debug("Initializing logging from config...") log.init_from_config(config.val) + config.instance.changed.connect(_on_config_changed) + log.init.debug("Initializing save manager...") save_manager = savemanager.SaveManager(q_app) objreg.register('save-manager', save_manager) diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index fa8b97628..379bd400b 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -224,7 +224,7 @@ def log_capacity(capacity: int) -> None: log.ram_handler.change_log_capacity(capacity) -@cmdutils.register(debug=True) +@cmdutils.register(debug=True, deprecated="Use `:set log.level.console`") @cmdutils.argument('level', choices=sorted( (level.lower() for level in log.LOG_LEVELS), key=lambda e: log.LOG_LEVELS[e.upper()])) From 23b5ee977714a56f64f2d9059dea050de504863d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2020 11:37:51 +0200 Subject: [PATCH 193/245] Remove :debug-log-level It's a hidden :debug-* command anyways, so no need to keep it around. --- doc/help/commands.asciidoc | 10 ---------- qutebrowser/misc/utilcmds.py | 18 ------------------ tests/end2end/features/utilcmds.feature | 4 ---- tests/unit/misc/test_utilcmds.py | 11 ----------- 4 files changed, 43 deletions(-) diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 9b2cae6c6..1e870cbaa 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -1929,7 +1929,6 @@ These commands are mainly intended for debugging. They are hidden if qutebrowser |<>|Show a keytester widget. |<>|Change the number of log lines to be stored in RAM. |<>|Change the log filter for console logging. -|<>|Change the log level for console logging. |<>|Evaluate a python string and display the results as a web page. |<>|Put data into the fake clipboard and enable logging, used for tests. |<>|Trace executed code via hunter. @@ -2004,15 +2003,6 @@ Change the log filter for console logging. * +'filters'+: A comma separated list of logger names. Can also be "none" to clear any existing filters. -[[debug-log-level]] -=== debug-log-level -Syntax: +:debug-log-level 'level'+ - -Change the log level for console logging. - -==== positional arguments -* +'level'+: The log level to set. - [[debug-pyeval]] === debug-pyeval Syntax: +:debug-pyeval [*--file*] [*--quiet*] 's'+ diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index 379bd400b..2a4b543f8 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -224,24 +224,6 @@ def log_capacity(capacity: int) -> None: log.ram_handler.change_log_capacity(capacity) -@cmdutils.register(debug=True, deprecated="Use `:set log.level.console`") -@cmdutils.argument('level', choices=sorted( - (level.lower() for level in log.LOG_LEVELS), - key=lambda e: log.LOG_LEVELS[e.upper()])) -def debug_log_level(level: str) -> None: - """Change the log level for console logging. - - Args: - level: The log level to set. - """ - if log.console_handler is None: - raise cmdutils.CommandError("No log.console_handler. Not attached " - "to a console?") - - log.change_console_formatter(log.LOG_LEVELS[level.upper()]) - log.console_handler.setLevel(log.LOG_LEVELS[level.upper()]) - - @cmdutils.register(debug=True) def debug_log_filter(filters: str) -> None: """Change the log filter for console logging. diff --git a/tests/end2end/features/utilcmds.feature b/tests/end2end/features/utilcmds.feature index 9b4eb5760..bfe0035dd 100644 --- a/tests/end2end/features/utilcmds.feature +++ b/tests/end2end/features/utilcmds.feature @@ -166,10 +166,6 @@ Feature: Miscellaneous utility commands exposed to the user. # Other :debug-log-{level,filter} features are tested in # unit/utils/test_log.py as using them would break end2end tests. - Scenario: Using debug-log-level with invalid level - When I run :debug-log-level hello - Then the error "level: Invalid value hello - expected one of: vdebug, debug, info, warning, error, critical" should be shown - Scenario: Using debug-log-filter with invalid filter When I run :debug-log-filter blah Then the error "filters: Invalid value blah - expected one of: statusbar, *" should be shown diff --git a/tests/unit/misc/test_utilcmds.py b/tests/unit/misc/test_utilcmds.py index 3066dcc7a..87e80afc1 100644 --- a/tests/unit/misc/test_utilcmds.py +++ b/tests/unit/misc/test_utilcmds.py @@ -42,17 +42,6 @@ def test_repeat_command_initial(mocker, mode_manager): utilcmds.repeat_command(win_id=0) -def test_debug_log_level(mocker): - """Test interactive log level changing.""" - formatter_mock = mocker.patch( - 'qutebrowser.misc.utilcmds.log.change_console_formatter') - handler_mock = mocker.patch( - 'qutebrowser.misc.utilcmds.log.console_handler') - utilcmds.debug_log_level(level='debug') - formatter_mock.assert_called_with(logging.DEBUG) - handler_mock.setLevel.assert_called_with(logging.DEBUG) - - class FakeWindow: """Mock class for window_only.""" From a9d10aee8403fdfcfc68d38a40312c7811d0dfe7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2020 11:39:05 +0200 Subject: [PATCH 194/245] tests: Separate init_from_config logs in utils.test_log --- tests/unit/utils/test_log.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/unit/utils/test_log.py b/tests/unit/utils/test_log.py index 6b770ed44..3e3243e48 100644 --- a/tests/unit/utils/test_log.py +++ b/tests/unit/utils/test_log.py @@ -255,14 +255,16 @@ class TestInitLog: with pytest.raises(PendingDeprecationWarning): warnings.warn("test warning", PendingDeprecationWarning) + +class TestInitFromConfig: + @pytest.mark.parametrize('cli, conf, expected', [ (None, 'info', logging.INFO), (None, 'warning', logging.WARNING), ('info', 'warning', logging.INFO), ('warning', 'info', logging.WARNING), ]) - def test_init_from_config_console(self, cli, conf, expected, args, - config_stub): + def test_console(self, cli, conf, expected, args, config_stub): args.debug = False args.loglevel = cli log.init_log(args) @@ -277,7 +279,7 @@ class TestInitLog: ('info', logging.INFO), ('critical', logging.CRITICAL), ]) - def test_init_from_config_ram(self, conf, expected, args, config_stub): + def test_ram(self, conf, expected, args, config_stub): args.debug = False log.init_log(args) @@ -285,7 +287,7 @@ class TestInitLog: log.init_from_config(config_stub.val) assert log.ram_handler.level == expected - def test_init_from_config_consistent_default(self, config_stub): + def test_consistent_default(self, config_stub): """Ensure config defaults are consistent with the builtin defaults.""" args = qutebrowser.get_argparser().parse_args([]) log.init_log(args) From 49b0c73fdd4a5410a49ec97d46ff2686ab65549c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2020 11:43:10 +0200 Subject: [PATCH 195/245] Update changelog --- doc/changelog.asciidoc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index f52f9b02a..8a77a3fd5 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -18,6 +18,12 @@ breaking changes (such as renamed commands) can happen in minor releases. v1.13.0 (unreleased) -------------------- +Removed +~~~~~~~ + +- The `:debug-log-level` command was removed as it's replaced by the new + `logging.level.console` setting. + Changed ~~~~~~~ From fe9bde1b1d737a0aa9cbc9ad4346e9a491e31a42 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2020 11:56:22 +0200 Subject: [PATCH 196/245] Switch some webview/misc logging messages to network logger --- qutebrowser/browser/shared.py | 4 ++-- qutebrowser/browser/webengine/interceptor.py | 8 ++++---- qutebrowser/browser/webengine/webenginequtescheme.py | 12 ++++++------ qutebrowser/browser/webengine/webenginetab.py | 6 +++--- qutebrowser/browser/webkit/network/networkmanager.py | 6 +++--- .../browser/webkit/network/webkitqutescheme.py | 2 +- qutebrowser/components/adblock.py | 2 +- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py index 4fbede419..a689e287a 100644 --- a/qutebrowser/browser/shared.py +++ b/qutebrowser/browser/shared.py @@ -160,7 +160,7 @@ def ignore_certificate_errors(url, errors, abort_on): True if the error should be ignored, False otherwise. """ ssl_strict = config.instance.get('content.ssl_strict', url=url) - log.webview.debug("Certificate errors {!r}, strict {}".format( + log.network.debug("Certificate errors {!r}, strict {}".format( errors, ssl_strict)) for error in errors: @@ -186,7 +186,7 @@ def ignore_certificate_errors(url, errors, abort_on): ignore = False return ignore elif ssl_strict is False: - log.webview.debug("ssl_strict is False, only warning about errors") + log.network.debug("ssl_strict is False, only warning about errors") for err in errors: # FIXME we might want to use warn here (non-fatal error) # https://github.com/qutebrowser/qutebrowser/issues/114 diff --git a/qutebrowser/browser/webengine/interceptor.py b/qutebrowser/browser/webengine/interceptor.py index 7d455d4c3..d4dcb522f 100644 --- a/qutebrowser/browser/webengine/interceptor.py +++ b/qutebrowser/browser/webengine/interceptor.py @@ -154,7 +154,7 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor): info.resourceType()) navigation_type_str = debug.qenum_key(QWebEngineUrlRequestInfo, info.navigationType()) - log.webview.debug("{} {}, first-party {}, resource {}, " + log.network.debug("{} {}, first-party {}, resource {}, " "navigation {}".format( bytes(info.requestMethod()).decode('ascii'), info.requestUrl().toDisplayString(), @@ -164,7 +164,7 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor): url = info.requestUrl() first_party = info.firstPartyUrl() if not url.isValid(): - log.webview.debug("Ignoring invalid intercepted URL: {}".format( + log.network.debug("Ignoring invalid intercepted URL: {}".format( url.errorString())) return @@ -173,7 +173,7 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor): try: resource_type = self._resource_types[info.resourceType()] except KeyError: - log.webview.warning( + log.network.warning( "Resource type {} not found in RequestInterceptor dict." .format(debug.qenum_key(QWebEngineUrlRequestInfo, info.resourceType()))) @@ -184,7 +184,7 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor): if (first_party != QUrl('qute://settings/') or info.resourceType() != QWebEngineUrlRequestInfo.ResourceTypeXhr): - log.webview.warning("Blocking malicious request from {} to {}" + log.network.warning("Blocking malicious request from {} to {}" .format(first_party.toDisplayString(), url.toDisplayString())) info.block(True) diff --git a/qutebrowser/browser/webengine/webenginequtescheme.py b/qutebrowser/browser/webengine/webenginequtescheme.py index 2c69b521f..879f8aeca 100644 --- a/qutebrowser/browser/webengine/webenginequtescheme.py +++ b/qutebrowser/browser/webengine/webenginequtescheme.py @@ -86,9 +86,9 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler): return True if initiator.isValid() and initiator.scheme() != 'qute': - log.misc.warning("Blocking malicious request from {} to {}".format( - initiator.toDisplayString(), - request_url.toDisplayString())) + log.network.warning("Blocking malicious request from {} to {}" + .format(initiator.toDisplayString(), + request_url.toDisplayString())) job.fail(QWebEngineUrlRequestJob.RequestDenied) return False @@ -119,7 +119,7 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler): assert url.scheme() == 'qute' - log.misc.debug("Got request for {}".format(url.toDisplayString())) + log.network.debug("Got request for {}".format(url.toDisplayString())) try: mimetype, data = qutescheme.data_for_url(url) except qutescheme.Error as e: @@ -136,14 +136,14 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler): QWebEngineUrlRequestJob.RequestFailed, } exctype = type(e) - log.misc.error("{} while handling qute://* URL".format( + log.network.error("{} while handling qute://* URL".format( exctype.__name__)) job.fail(errors[exctype]) except qutescheme.Redirect as e: qtutils.ensure_valid(e.url) job.redirect(e.url) else: - log.misc.debug("Returning {} data".format(mimetype)) + log.network.debug("Returning {} data".format(mimetype)) # We can't just use the QBuffer constructor taking a QByteArray, # because that somehow segfaults... diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index d1e7e88e6..85190e1c7 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -1619,16 +1619,16 @@ class WebEngineTab(browsertab.AbstractTab): url = error.url() self._insecure_hosts.add(url.host()) - log.webview.debug("Certificate error: {}".format(error)) + log.network.debug("Certificate error: {}".format(error)) if error.is_overridable(): error.ignore = shared.ignore_certificate_errors( url, [error], abort_on=[self.abort_questions]) else: - log.webview.error("Non-overridable certificate error: " + log.network.error("Non-overridable certificate error: " "{}".format(error)) - log.webview.debug("ignore {}, URL {}, requested {}".format( + log.network.debug("ignore {}, URL {}, requested {}".format( error.ignore, url, self.url(requested=True))) # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-56207 diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index 039ed0dba..1662f84fc 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -236,7 +236,7 @@ class NetworkManager(QNetworkAccessManager): errors: A list of errors. """ errors = [certificateerror.CertificateErrorWrapper(e) for e in errors] - log.webview.debug("Certificate errors: {!r}".format( + log.network.debug("Certificate errors: {!r}".format( ' / '.join(str(err) for err in errors))) try: host_tpl = urlutils.host_tuple( @@ -252,7 +252,7 @@ class NetworkManager(QNetworkAccessManager): is_rejected = set(errors).issubset( self._rejected_ssl_errors[host_tpl]) - log.webview.debug("Already accepted: {} / " + log.network.debug("Already accepted: {} / " "rejected {}".format(is_accepted, is_rejected)) if is_rejected: @@ -425,7 +425,7 @@ class NetworkManager(QNetworkAccessManager): if 'log-requests' in objects.debug_flags: operation = debug.qenum_key(QNetworkAccessManager, op) operation = operation.replace('Operation', '').upper() - log.webview.debug("{} {}, first-party {}".format( + log.network.debug("{} {}, first-party {}".format( operation, req.url().toDisplayString(), current_url.toDisplayString())) diff --git a/qutebrowser/browser/webkit/network/webkitqutescheme.py b/qutebrowser/browser/webkit/network/webkitqutescheme.py index 782bcc94a..0dce98765 100644 --- a/qutebrowser/browser/webkit/network/webkitqutescheme.py +++ b/qutebrowser/browser/webkit/network/webkitqutescheme.py @@ -48,7 +48,7 @@ def handler(request, operation, current_url): if ((url.scheme(), url.host(), url.path()) == ('qute', 'settings', '/set')): if current_url != QUrl('qute://settings/'): - log.webview.warning("Blocking malicious request from {} to {}" + log.network.warning("Blocking malicious request from {} to {}" .format(current_url.toDisplayString(), url.toDisplayString())) return networkreply.ErrorNetworkReply( diff --git a/qutebrowser/components/adblock.py b/qutebrowser/components/adblock.py index a683e9190..f6bc8bb3e 100644 --- a/qutebrowser/components/adblock.py +++ b/qutebrowser/components/adblock.py @@ -33,7 +33,7 @@ from qutebrowser.api import (cmdutils, hook, config, message, downloads, interceptor, apitypes, qtutils) -logger = logging.getLogger('misc') +logger = logging.getLogger('network') _host_blocker = typing.cast('HostBlocker', None) From 2b629e0d3d0c4b5ecbe49630718cf9579294020e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2020 12:01:49 +0200 Subject: [PATCH 197/245] qutescheme: Unify code between qute_log and qute_plainlog --- qutebrowser/browser/qutescheme.py | 35 ++++++++++++------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index ec6e19082..83536d3e9 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -307,43 +307,34 @@ def qute_version(_url): return 'text/html', src -@add_handler('plainlog') -def qute_plainlog(url: QUrl) -> _HandlerRet: - """Handler for qute://plainlog. +def _qute_log(url: QUrl, *, html: bool) -> _HandlerRet: + """Shared code between qute://log and qute://plainlog. An optional query parameter specifies the minimum log level to print. For example, qute://log?level=warning prints warnings and errors. Level can be one of: vdebug, debug, info, warning, error, critical. """ if log.ram_handler is None: - text = "Log output was disabled." + content = None if html else "Log output was disabled." else: level = QUrlQuery(url).queryItemValue('level') if not level: level = 'vdebug' - text = log.ram_handler.dump_log(html=False, level=level) - src = jinja.render('pre.html', title='log', content=text) + content = log.ram_handler.dump_log(html=html, level=level) + + template = 'log.html' if html else 'pre.html' + src = jinja.render(template, title='log', content=content) return 'text/html', src +@add_handler('plainlog') +def qute_plainlog(url: QUrl) -> _HandlerRet: + return _qute_log(url, html=False) + + @add_handler('log') def qute_log(url: QUrl) -> _HandlerRet: - """Handler for qute://log. - - An optional query parameter specifies the minimum log level to print. - For example, qute://log?level=warning prints warnings and errors. - Level can be one of: vdebug, debug, info, warning, error, critical. - """ - if log.ram_handler is None: - html_log = None - else: - level = QUrlQuery(url).queryItemValue('level') - if not level: - level = 'vdebug' - html_log = log.ram_handler.dump_log(html=True, level=level) - - src = jinja.render('log.html', title='log', content=html_log) - return 'text/html', src + return _qute_log(url, html=True) @add_handler('gpl') From 679eaab28f6ee470bfdd7ca3699efdb619b23ebe Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2020 12:11:08 +0200 Subject: [PATCH 198/245] Merge qute://plainlog into qute://log?plain --- doc/changelog.asciidoc | 3 +++ qutebrowser/browser/commands.py | 11 ++++++--- qutebrowser/browser/qutescheme.py | 27 +++++++++-------------- tests/end2end/features/qutescheme.feature | 4 ---- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 8a77a3fd5..f5f039c2e 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -23,6 +23,9 @@ Removed - The `:debug-log-level` command was removed as it's replaced by the new `logging.level.console` setting. +- The `qute://plainlog` special page got replaced by `qute://log?plain` - the + names of those pages is considered an implementation detail, and + `:messages --plain` should be used instead. Changed ~~~~~~~ diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index f115501d3..639f641f5 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1398,10 +1398,15 @@ class CommandDispatcher: """ if level.upper() not in log.LOG_LEVELS: raise cmdutils.CommandError("Invalid log level {}!".format(level)) + + query = QUrlQuery() + query.addQueryItem('level', level) if plain: - url = QUrl('qute://plainlog?level={}'.format(level)) - else: - url = QUrl('qute://log?level={}'.format(level)) + query.addQueryItem('plain', None) + + url = QUrl('qute://log') + url.setQuery(query) + self._open(url, tab, bg, window) def _open_editor_cb(self, elem): diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index 83536d3e9..fc1f2a123 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -307,36 +307,31 @@ def qute_version(_url): return 'text/html', src -def _qute_log(url: QUrl, *, html: bool) -> _HandlerRet: - """Shared code between qute://log and qute://plainlog. +@add_handler('log') +def qute_log(url: QUrl) -> _HandlerRet: + """Handler for qute://log. An optional query parameter specifies the minimum log level to print. For example, qute://log?level=warning prints warnings and errors. Level can be one of: vdebug, debug, info, warning, error, critical. """ + query = QUrlQuery(url) + plain = (query.hasQueryItem('plain') and + query.queryItemValue('plain').lower() != 'false') + if log.ram_handler is None: - content = None if html else "Log output was disabled." + content = "Log output was disabled." if plain else None else: - level = QUrlQuery(url).queryItemValue('level') + level = query.queryItemValue('level') if not level: level = 'vdebug' - content = log.ram_handler.dump_log(html=html, level=level) + content = log.ram_handler.dump_log(html=not plain, level=level) - template = 'log.html' if html else 'pre.html' + template = 'pre.html' if plain else 'log.html' src = jinja.render(template, title='log', content=content) return 'text/html', src -@add_handler('plainlog') -def qute_plainlog(url: QUrl) -> _HandlerRet: - return _qute_log(url, html=False) - - -@add_handler('log') -def qute_log(url: QUrl) -> _HandlerRet: - return _qute_log(url, html=True) - - @add_handler('gpl') def qute_gpl(_url: QUrl) -> _HandlerRet: """Handler for qute://gpl. Return HTML content as string.""" diff --git a/tests/end2end/features/qutescheme.feature b/tests/end2end/features/qutescheme.feature index 35c110dc5..d41975d78 100644 --- a/tests/end2end/features/qutescheme.feature +++ b/tests/end2end/features/qutescheme.feature @@ -269,10 +269,6 @@ Feature: Special qute:// pages And I wait for "Changing title for idx * to 'log'" in the log Then no crash should happen - Scenario: Using qute://plainlog directly - When I open qute://plainlog - Then no crash should happen - # :version Scenario: Open qute://version From 33dd70dae67ce051c46fc8a1631e2a5fd94f50b8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2020 12:34:50 +0200 Subject: [PATCH 199/245] Revert "tests: Separate init_from_config logs in utils.test_log" Whoops... It uses fixtures from that test class! This reverts commit a9d10aee8403fdfcfc68d38a40312c7811d0dfe7. --- tests/unit/utils/test_log.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/unit/utils/test_log.py b/tests/unit/utils/test_log.py index 3e3243e48..6b770ed44 100644 --- a/tests/unit/utils/test_log.py +++ b/tests/unit/utils/test_log.py @@ -255,16 +255,14 @@ class TestInitLog: with pytest.raises(PendingDeprecationWarning): warnings.warn("test warning", PendingDeprecationWarning) - -class TestInitFromConfig: - @pytest.mark.parametrize('cli, conf, expected', [ (None, 'info', logging.INFO), (None, 'warning', logging.WARNING), ('info', 'warning', logging.INFO), ('warning', 'info', logging.WARNING), ]) - def test_console(self, cli, conf, expected, args, config_stub): + def test_init_from_config_console(self, cli, conf, expected, args, + config_stub): args.debug = False args.loglevel = cli log.init_log(args) @@ -279,7 +277,7 @@ class TestInitFromConfig: ('info', logging.INFO), ('critical', logging.CRITICAL), ]) - def test_ram(self, conf, expected, args, config_stub): + def test_init_from_config_ram(self, conf, expected, args, config_stub): args.debug = False log.init_log(args) @@ -287,7 +285,7 @@ class TestInitFromConfig: log.init_from_config(config_stub.val) assert log.ram_handler.level == expected - def test_consistent_default(self, config_stub): + def test_init_from_config_consistent_default(self, config_stub): """Ensure config defaults are consistent with the builtin defaults.""" args = qutebrowser.get_argparser().parse_args([]) log.init_log(args) From 3edaa173020cac1079437d67f96ccb5309ed5640 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2020 12:37:57 +0200 Subject: [PATCH 200/245] Fix lint --- tests/unit/misc/test_utilcmds.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/unit/misc/test_utilcmds.py b/tests/unit/misc/test_utilcmds.py index 87e80afc1..e8f651dc8 100644 --- a/tests/unit/misc/test_utilcmds.py +++ b/tests/unit/misc/test_utilcmds.py @@ -19,8 +19,6 @@ """Tests for qutebrowser.misc.utilcmds.""" -import logging - import pytest from PyQt5.QtCore import QUrl From 34822bb530a57aaa82420828ff6e630dfa1b5cd8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2020 12:44:31 +0200 Subject: [PATCH 201/245] Change logging format when the loglevel is changed via config --- qutebrowser/utils/log.py | 4 +++- tests/unit/utils/test_log.py | 19 ++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index d1f654d8f..117d5f5d1 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -554,7 +554,9 @@ def init_from_config(conf: 'configmodule.ConfigContainer') -> None: init.debug("--loglevel flag overrides logging.level.console") else: init.debug("Configuring console loglevel to %s", consolelevel) - console_handler.setLevel(LOG_LEVELS[consolelevel.upper()]) + level = LOG_LEVELS[consolelevel.upper()] + console_handler.setLevel(level) + change_console_formatter(level) class QtWarningFilter(logging.Filter): diff --git a/tests/unit/utils/test_log.py b/tests/unit/utils/test_log.py index 6b770ed44..bf2af5147 100644 --- a/tests/unit/utils/test_log.py +++ b/tests/unit/utils/test_log.py @@ -216,6 +216,11 @@ class TestInitLog: """Fixture providing an argparse namespace for init_log.""" return self._get_default_args() + @pytest.fixture + def empty_args(self): + """Logging commandline arguments without any customization.""" + return qutebrowser.get_argparser().parse_args([]) + def test_stderr_none(self, args): """Test init_log with sys.stderr = None.""" old_stderr = sys.stderr @@ -285,10 +290,9 @@ class TestInitLog: log.init_from_config(config_stub.val) assert log.ram_handler.level == expected - def test_init_from_config_consistent_default(self, config_stub): + def test_init_from_config_consistent_default(self, config_stub, empty_args): """Ensure config defaults are consistent with the builtin defaults.""" - args = qutebrowser.get_argparser().parse_args([]) - log.init_log(args) + log.init_log(empty_args) assert log.ram_handler.level == logging.DEBUG assert log.console_handler.level == logging.INFO @@ -298,6 +302,15 @@ class TestInitLog: assert log.ram_handler.level == logging.DEBUG assert log.console_handler.level == logging.INFO + def test_init_from_config_format(self, config_stub, empty_args): + """If we change to the debug level, make sure the format changes.""" + log.init_log(empty_args) + assert log.console_handler.formatter._fmt == log.SIMPLE_FMT + + config_stub.val.logging.level.console = 'debug' + log.init_from_config(config_stub.val) + assert log.console_handler.formatter._fmt == log.EXTENDED_FMT + class TestHideQtWarning: From d9ecc35eefb354b2a1cdbca9944a120cd87474d1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2020 14:26:19 +0200 Subject: [PATCH 202/245] Fix lint --- qutebrowser/browser/commands.py | 2 +- qutebrowser/components/adblock.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 639f641f5..76f802891 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1402,7 +1402,7 @@ class CommandDispatcher: query = QUrlQuery() query.addQueryItem('level', level) if plain: - query.addQueryItem('plain', None) + query.addQueryItem('plain', typing.cast(str, None)) url = QUrl('qute://log') url.setQuery(query) diff --git a/qutebrowser/components/adblock.py b/qutebrowser/components/adblock.py index f6bc8bb3e..b34711fdd 100644 --- a/qutebrowser/components/adblock.py +++ b/qutebrowser/components/adblock.py @@ -129,7 +129,7 @@ class HostBlocker: if self._is_blocked(request_url=info.request_url, first_party_url=info.first_party_url): logger.debug("Request to {} blocked by host blocker." - .format(info.request_url.host())) + .format(info.request_url.host())) info.block() def _read_hosts_line(self, raw_line: bytes) -> typing.Set[str]: From ed26d43c16dbcc7b9534cb57d5a8b82f80706f7a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2020 13:04:06 +0200 Subject: [PATCH 203/245] Fix typo (cherry picked from commit 7dceedb26bd5d0df97a97cd727e19fd9937bd850) --- qutebrowser/utils/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 117d5f5d1..6f117f1a6 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -639,7 +639,7 @@ class RAMHandler(logging.Handler): def dump_log(self, html: bool = False, level: str = 'vdebug') -> str: """Dump the complete formatted log data as string. - FIXME: We should do all the HTML formatter via jinja2. + FIXME: We should do all the HTML formatting via jinja2. (probably obsolete when moving to a widget for logging, https://github.com/qutebrowser/qutebrowser/issues/34 """ From 025a70254f621b2c7f2c08f0f028e39b37c24f18 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2020 15:15:19 +0200 Subject: [PATCH 204/245] commands: Add some additional tests for *args handling (cherry picked from commit 3983a6d3ee30f86b1afd809bc786322cbff8fd55) --- tests/unit/api/test_cmdutils.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/unit/api/test_cmdutils.py b/tests/unit/api/test_cmdutils.py index 1d2cd3f46..9ca6fc2ae 100644 --- a/tests/unit/api/test_cmdutils.py +++ b/tests/unit/api/test_cmdutils.py @@ -140,9 +140,31 @@ class TestRegister: @cmdutils.register() def fun(*args): """Blah.""" + assert args == ['one', 'two'] + + objects.commands['fun'].parser.parse_args(['one', 'two']) + + def test_star_args_empty(self): + """Check handling of *args without any value.""" + @cmdutils.register() + def fun(*args): + """Blah.""" + assert not args + with pytest.raises(argparser.ArgumentParserError): objects.commands['fun'].parser.parse_args([]) + def test_star_args_type(self): + """Check handling of *args with a type. + + This isn't implemented, so be sure we catch it. + """ + with pytest.raises(AssertionError): + @cmdutils.register() + def fun(*args: int): + """Blah.""" + pass + def test_star_args_optional(self): """Check handling of *args withstar_args_optional.""" @cmdutils.register(star_args_optional=True) From ac4fc1a1e3aa87cc0a26fb7060141c9a059c9599 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2020 15:15:41 +0200 Subject: [PATCH 205/245] command: Use consistent name for arg_info (cherry picked from commit 279d28f982f3afe32c94fc938677bcad816b647e) --- qutebrowser/commands/command.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 4eefe481c..2672fcd68 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -334,8 +334,8 @@ class Command: Args: param: The inspect.Parameter to look at. """ - arginfo = self.get_arg_info(param) - if arginfo.value: + arg_info = self.get_arg_info(param) + if arg_info.value: # Filled values are passed 1:1 return None elif param.kind in [inspect.Parameter.VAR_POSITIONAL, From 58dc10ec66e0dd8c75c7aee18641752914065b17 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2020 13:02:20 +0200 Subject: [PATCH 206/245] Refactor log.LogFilter This essentially does two things: 1) Use a set for LogFilter This means we don't support filters like "eggs.bacon" anymore (but we *do* support loggers like that) - however, those were already disallowed by the --logfilter argument validation anyways! In return, we get probably slightly better performance (checking set membership rather than iterating all filters) and more straightforward code. 2) Move parsing from various locations around the code to the LogFilter class. --- qutebrowser/misc/utilcmds.py | 14 ++-- qutebrowser/qutebrowser.py | 11 ++-- qutebrowser/utils/log.py | 72 ++++++++++++++------- tests/unit/test_qutebrowser.py | 63 ++++++++++++++++++ tests/unit/utils/test_log.py | 114 +++++++++++++++++++++------------ 5 files changed, 193 insertions(+), 81 deletions(-) create mode 100644 tests/unit/test_qutebrowser.py diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index 2a4b543f8..05fea9501 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -236,16 +236,12 @@ def debug_log_filter(filters: str) -> None: raise cmdutils.CommandError("No log.console_filter. Not attached " "to a console?") - if filters.strip().lower() == 'none': - log.console_filter.names = None - return + try: + new_filter = log.LogFilter.parse(filters) + except log.InvalidLogFilterError as e: + raise cmdutils.CommandError(e) - if not set(filters.split(',')).issubset(log.LOGGER_NAMES): - raise cmdutils.CommandError("filters: Invalid value {} - expected one " - "of: {}".format( - filters, ', '.join(log.LOGGER_NAMES))) - - log.console_filter.names = filters.split(',') + log.console_filter.update_from(new_filter) @cmdutils.register() diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index 3369c1ebe..8765f5217 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -150,12 +150,11 @@ def logfilter_error(logfilter): logfilter: A comma separated list of logger names. """ from qutebrowser.utils import log - if set(logfilter.lstrip('!').split(',')).issubset(log.LOGGER_NAMES): - return logfilter - else: - raise argparse.ArgumentTypeError( - "filters: Invalid value {} - expected a list of: {}".format( - logfilter, ', '.join(log.LOGGER_NAMES))) + try: + log.LogFilter.parse(logfilter) + except log.InvalidLogFilterError as e: + raise argparse.ArgumentTypeError(e) + return logfilter def debug_flag_error(flag): diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 6f117f1a6..e603ea2b2 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -193,16 +193,7 @@ def init_log(args: argparse.Namespace) -> None: root = logging.getLogger() global console_filter if console is not None: - if not args.logfilter: - negate = False - names = None - elif args.logfilter.startswith('!'): - negate = True - names = args.logfilter[1:].split(',') - else: - negate = False - names = args.logfilter.split(',') - console_filter = LogFilter(names, negate) + console_filter = LogFilter.parse(args.logfilter) console.addFilter(console_filter) root.addHandler(console) if ram is not None: @@ -577,6 +568,17 @@ class QtWarningFilter(logging.Filter): return do_log +class InvalidLogFilterError(Exception): + + """Raised when an invalid filter string is passed to LogFilter.parse().""" + + def __init__(self, names: typing.Set[str]): + invalid = names - set(LOGGER_NAMES) + super().__init__("Invalid log category {} - valid categories: {}" + .format(', '.join(sorted(invalid)), + ', '.join(LOGGER_NAMES))) + + class LogFilter(logging.Filter): """Filter to filter log records based on the commandline argument. @@ -585,30 +587,52 @@ class LogFilter(logging.Filter): comma-separated list instead. Attributes: - names: A list of record names to filter. - negated: Whether names is a list of records to log or to suppress. + names: A set of logging names to allow. + negated: Whether names is a set of names to log or to suppress. """ - def __init__(self, names: typing.Optional[typing.Iterable[str]], - negate: bool = False) -> None: + def __init__(self, names: typing.Set[str], negated: bool = False) -> None: super().__init__() self.names = names - self.negated = negate + self.negated = negated + + @classmethod + def parse(cls, filter_str: typing.Optional[str]) -> 'LogFilter': + """Parse a log filter from a string.""" + if filter_str is None or filter_str == 'none': + names = set() + negated = False + else: + filter_str = filter_str.lower() + + if filter_str.startswith('!'): + negated = True + filter_str = filter_str[1:] + else: + negated = False + + names = {e.strip() for e in filter_str.split(',')} + + if not names.issubset(LOGGER_NAMES): + raise InvalidLogFilterError(names) + + return cls(names=names, negated=negated) + + def update_from(self, other: 'LogFilter') -> None: + """Update this filter's properties from another filter.""" + self.names = other.names + self.negated = other.negated def filter(self, record: logging.LogRecord) -> bool: """Determine if the specified record is to be logged.""" - if self.names is None: + if not self.names: + # No filter return True - if record.levelno > logging.DEBUG: + elif record.levelno > logging.DEBUG: # More important than DEBUG, so we won't filter at all return True - for name in self.names: - if record.name == name: - return not self.negated - elif not record.name.startswith(name): - continue - elif record.name[len(name)] == '.': - return not self.negated + elif record.name.split('.')[0] in self.names: + return not self.negated return self.negated diff --git a/tests/unit/test_qutebrowser.py b/tests/unit/test_qutebrowser.py new file mode 100644 index 000000000..6dbc24351 --- /dev/null +++ b/tests/unit/test_qutebrowser.py @@ -0,0 +1,63 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2020 Florian Bruhin (The Compiler) +# +# 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 . + +"""Tests for qutebrowser.qutebrowser. + +(Mainly commandline flag parsing) +""" + +import argparse + +import pytest + +from qutebrowser import qutebrowser + + +@pytest.fixture +def parser(): + return qutebrowser.get_argparser() + + +class TestDebugFlag: + + def test_valid(self, parser): + args = parser.parse_args(['--debug-flag', 'chromium', '--debug-flag', 'stack']) + assert args.debug_flags == ['chromium', 'stack'] + + def test_invalid(self, parser, capsys): + with pytest.raises(SystemExit): + parser.parse_args(['--debug-flag', 'invalid']) + + _out, err = capsys.readouterr() + assert 'Invalid debug flag - valid flags:' in err + + +class TestLogFilter: + + def test_valid(self, parser): + args = parser.parse_args(['--logfilter', 'misc']) + assert args.logfilter == 'misc' + + def test_invalid(self, parser, capsys): + with pytest.raises(SystemExit): + parser.parse_args(['--logfilter', 'invalid']) + + _out, err = capsys.readouterr() + print(err) + assert 'Invalid log category invalid - valid categories' in err diff --git a/tests/unit/utils/test_log.py b/tests/unit/utils/test_log.py index bf2af5147..14fa07aeb 100644 --- a/tests/unit/utils/test_log.py +++ b/tests/unit/utils/test_log.py @@ -33,6 +33,7 @@ from PyQt5 import QtCore from qutebrowser import qutebrowser from qutebrowser.utils import log from qutebrowser.misc import utilcmds +from qutebrowser.api import cmdutils @pytest.fixture(autouse=True) @@ -118,28 +119,23 @@ class TestLogFilter: @pytest.mark.parametrize('filters, negated, category, logged', [ # Filter letting all messages through - (None, False, 'eggs.bacon.spam', True), - (None, False, 'eggs', True), - (None, True, 'ham', True), + (set(), False, 'eggs.bacon.spam', True), + (set(), False, 'eggs', True), + (set(), True, 'ham', True), # Matching records - (['eggs', 'bacon'], False, 'eggs', True), - (['eggs', 'bacon'], False, 'bacon', True), - (['eggs.bacon'], False, 'eggs.bacon', True), + ({'eggs', 'bacon'}, False, 'eggs', True), + ({'eggs', 'bacon'}, False, 'bacon', True), + ({'eggs'}, False, 'eggs.fried', True), # Non-matching records - (['eggs', 'bacon'], False, 'spam', False), - (['eggs'], False, 'eggsauce', False), - (['eggs.bacon'], False, 'eggs.baconstrips', False), - # Child loggers - (['eggs.bacon', 'spam.ham'], False, 'eggs.bacon.spam', True), - (['eggs.bacon', 'spam.ham'], False, 'spam.ham.salami', True), + ({'eggs', 'bacon'}, False, 'spam', False), + ({'eggs'}, False, 'eggsauce', False), + ({'fried'}, False, 'eggs.fried', False), # Suppressed records - (['eggs', 'bacon'], True, 'eggs', False), - (['eggs', 'bacon'], True, 'bacon', False), - (['eggs.bacon'], True, 'eggs.bacon', False), + ({'eggs', 'bacon'}, True, 'eggs', False), + ({'eggs', 'bacon'}, True, 'bacon', False), # Non-suppressed records - (['eggs', 'bacon'], True, 'spam', True), - (['eggs'], True, 'eggsauce', True), - (['eggs.bacon'], True, 'eggs.baconstrips', True), + ({'eggs', 'bacon'}, True, 'spam', True), + ({'eggs'}, True, 'eggsauce', True), ]) def test_logfilter(self, logger, filters, negated, category, logged): """Ensure the multi-record filtering filterer filters multiple records. @@ -150,19 +146,29 @@ class TestLogFilter: record = self._make_record(logger, category) assert logfilter.filter(record) == logged + def test_logfilter_benchmark(self, logger, benchmark): + record = self._make_record(logger, 'unfiltered') + filters = set(log.LOGGER_NAMES) # Extreme case + logfilter = log.LogFilter(filters, negated=False) + benchmark(lambda: logfilter.filter(record)) + @pytest.mark.parametrize('category', ['eggs', 'bacon']) def test_debug(self, logger, category): """Test if messages more important than debug are never filtered.""" - logfilter = log.LogFilter(['eggs']) + logfilter = log.LogFilter({'eggs'}) record = self._make_record(logger, category, level=logging.INFO) assert logfilter.filter(record) - @pytest.mark.parametrize('category, logged_before, logged_after', [ - ('init', True, False), ('url', False, True), ('js', False, True)]) + @pytest.mark.parametrize('category, filter_str, logged_before, logged_after', [ + ('init', 'url,js', True, False), + ('url', 'url,js', False, True), + ('js', 'url,js', False, True), + ('js', 'none', False, True), + ]) def test_debug_log_filter_cmd(self, monkeypatch, logger, category, - logged_before, logged_after): + filter_str, logged_before, logged_after): """Test the :debug-log-filter command handler.""" - logfilter = log.LogFilter(["init"]) + logfilter = log.LogFilter({"init"}) monkeypatch.setattr(log, 'console_filter', logfilter) record = self._make_record(logger, category) @@ -171,6 +177,37 @@ class TestLogFilter: utilcmds.debug_log_filter('url,js') assert logfilter.filter(record) == logged_after + def test_debug_log_filter_cmd_invalid(self, monkeypatch): + logfilter = log.LogFilter(set()) + monkeypatch.setattr(log, 'console_filter', logfilter) + with pytest.raises(cmdutils.CommandError, + match='Invalid log category blabla'): + utilcmds.debug_log_filter('blabla') + + @pytest.mark.parametrize('filter_str, expected_names, negated', [ + ('!js,misc', {'js', 'misc'}, True), + ('js,misc', {'js', 'misc'}, False), + ('js, misc', {'js', 'misc'}, False), + ('JS, Misc', {'js', 'misc'}, False), + (None, set(), False), + ('none', set(), False), + ]) + def test_parsing(self, filter_str, expected_names, negated): + logfilter = log.LogFilter.parse(filter_str) + assert logfilter.names == expected_names + assert logfilter.negated == negated + + @pytest.mark.parametrize('filter_str, invalid', [ + ('js,!misc', '!misc'), + ('blabla,js,blablub', 'blabla, blablub'), + ]) + def test_parsing_invalid(self, filter_str, invalid): + with pytest.raises( + log.InvalidLogFilterError, + match='Invalid log category {} - ' + 'valid categories: statusbar, .*'.format(invalid)): + log.LogFilter.parse(filter_str) + @pytest.mark.parametrize('data, expected', [ # Less data @@ -199,7 +236,7 @@ class TestInitLog: def _get_default_args(self): return argparse.Namespace(debug=True, loglevel='debug', color=True, - loglines=10, logfilter="", force_color=False, + loglines=10, logfilter=None, force_color=False, json_logging=False, debug_flags=set()) @pytest.fixture(autouse=True) @@ -217,9 +254,13 @@ class TestInitLog: return self._get_default_args() @pytest.fixture - def empty_args(self): + def parser(self): + return qutebrowser.get_argparser() + + @pytest.fixture + def empty_args(self, parser): """Logging commandline arguments without any customization.""" - return qutebrowser.get_argparser().parse_args([]) + return parser.parse_args([]) def test_stderr_none(self, args): """Test init_log with sys.stderr = None.""" @@ -228,22 +269,6 @@ class TestInitLog: log.init_log(args) sys.stderr = old_stderr - @pytest.mark.parametrize('logfilter, expected_names, negated', [ - ('!one,two', ['one', 'two'], True), - ('one,two', ['one', 'two'], False), - ('one,!two', ['one', '!two'], False), - (None, None, False), - ]) - def test_negation_parser(self, args, mocker, - logfilter, expected_names, negated): - """Test parsing the --logfilter argument.""" - filter_mock = mocker.patch('qutebrowser.utils.log.LogFilter', - autospec=True) - args.logfilter = logfilter - log.init_log(args) - assert filter_mock.called - assert filter_mock.call_args[0] == (expected_names, negated) - def test_python_warnings(self, args, caplog): log.init_log(args) @@ -311,6 +336,11 @@ class TestInitLog: log.init_from_config(config_stub.val) assert log.console_handler.formatter._fmt == log.EXTENDED_FMT + def test_logfilter(self, parser): + args = parser.parse_args(['--logfilter', 'misc']) + log.init_log(args) + assert log.console_filter.names == {'misc'} + class TestHideQtWarning: From 48fed500ef546092374d92e42fe60203896f4773 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2020 13:05:31 +0200 Subject: [PATCH 207/245] Add categories to qute://log --- doc/help/commands.asciidoc | 5 +++- qutebrowser/browser/commands.py | 15 ++++++++++-- qutebrowser/browser/qutescheme.py | 21 +++++++++++++++-- qutebrowser/utils/log.py | 28 ++++++++++++++++++----- tests/end2end/features/qutescheme.feature | 21 +++++++++++++++++ tests/end2end/features/utilcmds.feature | 2 +- tests/unit/utils/test_log.py | 12 +++++----- 7 files changed, 86 insertions(+), 18 deletions(-) diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 1e870cbaa..8d70b19c8 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -809,7 +809,7 @@ Show a warning message in the statusbar. [[messages]] === messages -Syntax: +:messages [*--plain*] [*--tab*] [*--bg*] [*--window*] ['level']+ +Syntax: +:messages [*--plain*] [*--tab*] [*--bg*] [*--window*] [*--logfilter* 'logfilter'] ['level']+ Show a log of past messages. @@ -822,6 +822,9 @@ Show a log of past messages. * +*-t*+, +*--tab*+: Open in a new tab. * +*-b*+, +*--bg*+: Open in a background tab. * +*-w*+, +*--window*+: Open in a new window. +* +*-f*+, +*--logfilter*+: A comma-separated filter string of logging categories. If the filter string starts with an exclamation mark, it + is negated. + [[navigate]] === navigate diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 76f802891..78ed6c383 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1384,14 +1384,18 @@ class CommandDispatcher: self._open(url, tab, bg, window) @cmdutils.register(instance='command-dispatcher', scope='window') - def messages(self, level='info', plain=False, tab=False, bg=False, - window=False): + @cmdutils.argument('logfilter', flag='f') + def messages(self, level='info', *, plain=False, tab=False, bg=False, + window=False, logfilter=None): """Show a log of past messages. Args: level: Include messages with `level` or higher severity. Valid values: vdebug, debug, info, warning, error, critical. plain: Whether to show plaintext (as opposed to html). + logfilter: A comma-separated filter string of logging categories. + If the filter string starts with an exclamation mark, it + is negated. tab: Open in a new tab. bg: Open in a background tab. window: Open in a new window. @@ -1404,6 +1408,13 @@ class CommandDispatcher: if plain: query.addQueryItem('plain', typing.cast(str, None)) + if logfilter: + try: + log.LogFilter.parse(logfilter) + except log.InvalidLogFilterError as e: + raise cmdutils.CommandError(e) + query.addQueryItem('logfilter', logfilter) + url = QUrl('qute://log') url.setQuery(query) diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index fc1f2a123..5e8eb36c5 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -311,9 +311,16 @@ def qute_version(_url): def qute_log(url: QUrl) -> _HandlerRet: """Handler for qute://log. - An optional query parameter specifies the minimum log level to print. + There are three query parameters: + + - level: The minimum log level to print. For example, qute://log?level=warning prints warnings and errors. Level can be one of: vdebug, debug, info, warning, error, critical. + + - plain: If given (and not 'false'), plaintext is shown. + + - logfilter: A filter string like the --logfilter commandline argument + accepts. """ query = QUrlQuery(url) plain = (query.hasQueryItem('plain') and @@ -325,7 +332,17 @@ def qute_log(url: QUrl) -> _HandlerRet: level = query.queryItemValue('level') if not level: level = 'vdebug' - content = log.ram_handler.dump_log(html=not plain, level=level) + + filter_str = query.queryItemValue('logfilter') + + try: + logfilter = (log.LogFilter.parse(filter_str, only_debug=False) + if filter_str else None) + except log.InvalidLogFilterError as e: + raise UrlInvalidError(e) + + content = log.ram_handler.dump_log(html=not plain, + level=level, logfilter=logfilter) template = 'pre.html' if plain else 'log.html' src = jinja.render(template, title='log', content=content) diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index e603ea2b2..197f594f9 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -589,15 +589,20 @@ class LogFilter(logging.Filter): Attributes: names: A set of logging names to allow. negated: Whether names is a set of names to log or to suppress. + only_debug: Only filter debug logs, always show anything more important + than debug. """ - def __init__(self, names: typing.Set[str], negated: bool = False) -> None: + def __init__(self, names: typing.Set[str], *, negated: bool = False, + only_debug: bool = True) -> None: super().__init__() self.names = names self.negated = negated + self.only_debug = only_debug @classmethod - def parse(cls, filter_str: typing.Optional[str]) -> 'LogFilter': + def parse(cls, filter_str: typing.Optional[str], *, + only_debug: bool = True) -> 'LogFilter': """Parse a log filter from a string.""" if filter_str is None or filter_str == 'none': names = set() @@ -616,19 +621,20 @@ class LogFilter(logging.Filter): if not names.issubset(LOGGER_NAMES): raise InvalidLogFilterError(names) - return cls(names=names, negated=negated) + return cls(names=names, negated=negated, only_debug=only_debug) def update_from(self, other: 'LogFilter') -> None: """Update this filter's properties from another filter.""" self.names = other.names self.negated = other.negated + self.only_debug = other.only_debug def filter(self, record: logging.LogRecord) -> bool: """Determine if the specified record is to be logged.""" if not self.names: # No filter return True - elif record.levelno > logging.DEBUG: + elif record.levelno > logging.DEBUG and self.only_debug: # More important than DEBUG, so we won't filter at all return True elif record.name.split('.')[0] in self.names: @@ -660,15 +666,24 @@ class RAMHandler(logging.Handler): def emit(self, record: logging.LogRecord) -> None: self._data.append(record) - def dump_log(self, html: bool = False, level: str = 'vdebug') -> str: + def dump_log(self, html: bool = False, level: str = 'vdebug', + logfilter: LogFilter = None) -> str: """Dump the complete formatted log data as string. FIXME: We should do all the HTML formatting via jinja2. (probably obsolete when moving to a widget for logging, https://github.com/qutebrowser/qutebrowser/issues/34 + + Args: + html: Produce HTML rather than plaintext output. + level: The minimal loglevel to show. + logfilter: A LogFilter instance used to filter log lines. """ minlevel = LOG_LEVELS.get(level.upper(), VDEBUG_LEVEL) + if logfilter is None: + logfilter = LogFilter(set()) + if html: assert self.html_formatter is not None fmt = self.html_formatter.format @@ -679,7 +694,8 @@ class RAMHandler(logging.Handler): try: lines = [fmt(record) for record in self._data - if record.levelno >= minlevel] + if record.levelno >= minlevel and + logfilter.filter(record)] finally: self.release() return '\n'.join(lines) diff --git a/tests/end2end/features/qutescheme.feature b/tests/end2end/features/qutescheme.feature index d41975d78..2325912c5 100644 --- a/tests/end2end/features/qutescheme.feature +++ b/tests/end2end/features/qutescheme.feature @@ -258,17 +258,38 @@ Feature: Special qute:// pages And the page should contain the plaintext "the-warning-message" And the page should contain the plaintext "the-info-message" + Scenario: Showing messages of category 'message' + When I run :message-info the-info-message + And I run :messages -f message + Then qute://log/?level=info&logfilter=message should be loaded + And the page should contain the plaintext "the-info-message" + + Scenario: Showing messages of category 'misc' + When I run :message-info the-info-message + And I run :messages -f misc + Then qute://log/?level=info&logfilter=misc should be loaded + And the page should not contain the plaintext "the-info-message" + @qtwebengine_flaky Scenario: Showing messages of an invalid level When I run :messages cataclysmic Then the error "Invalid log level cataclysmic!" should be shown + Scenario: Showing messages with an invalid category + When I run :messages -f invalid + Then the error "Invalid log category invalid - *" should be shown + Scenario: Using qute://log directly When I open qute://log without waiting # With Qt 5.9, we don't get a loaded message? And I wait for "Changing title for idx * to 'log'" in the log Then no crash should happen + # FIXME More possible tests: + # :message --plain + # Using qute://log directly with invalid category + # same with invalid level + # :version Scenario: Open qute://version diff --git a/tests/end2end/features/utilcmds.feature b/tests/end2end/features/utilcmds.feature index bfe0035dd..94db7c403 100644 --- a/tests/end2end/features/utilcmds.feature +++ b/tests/end2end/features/utilcmds.feature @@ -168,7 +168,7 @@ Feature: Miscellaneous utility commands exposed to the user. Scenario: Using debug-log-filter with invalid filter When I run :debug-log-filter blah - Then the error "filters: Invalid value blah - expected one of: statusbar, *" should be shown + Then the error "Invalid log category blah - valid categories: statusbar, *" should be shown Scenario: Using debug-log-filter When I run :debug-log-filter commands,ipc,webview diff --git a/tests/unit/utils/test_log.py b/tests/unit/utils/test_log.py index 14fa07aeb..8c2e636a1 100644 --- a/tests/unit/utils/test_log.py +++ b/tests/unit/utils/test_log.py @@ -142,7 +142,7 @@ class TestLogFilter: (Blame @toofar for this comment) """ - logfilter = log.LogFilter(filters, negated) + logfilter = log.LogFilter(filters, negated=negated) record = self._make_record(logger, category) assert logfilter.filter(record) == logged @@ -152,12 +152,12 @@ class TestLogFilter: logfilter = log.LogFilter(filters, negated=False) benchmark(lambda: logfilter.filter(record)) - @pytest.mark.parametrize('category', ['eggs', 'bacon']) - def test_debug(self, logger, category): + @pytest.mark.parametrize('only_debug', [True, False]) + def test_debug(self, logger, only_debug): """Test if messages more important than debug are never filtered.""" - logfilter = log.LogFilter({'eggs'}) - record = self._make_record(logger, category, level=logging.INFO) - assert logfilter.filter(record) + logfilter = log.LogFilter({'eggs'}, only_debug=only_debug) + record = self._make_record(logger, 'bacon', level=logging.INFO) + assert logfilter.filter(record) == only_debug @pytest.mark.parametrize('category, filter_str, logged_before, logged_after', [ ('init', 'url,js', True, False), From 290f5ec527c93be4789ff772e23493ea1d56eb82 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2020 16:54:39 +0200 Subject: [PATCH 208/245] Update changelog --- doc/changelog.asciidoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index f5f039c2e..eaf4f5146 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -38,6 +38,9 @@ Changed that it can be used without the report window popping up. - New `t[Cc][Hh]` default bindings which work similarly to the `t[Ss][Hh]` bindings for JavaScript but toggle cookie permissions. +- The `:message` command now takes a `--logfilter` / `-f` argument, which is a + list of logging categories to show. +- The `:debug-log-filter` command now understands the full logfilter syntax. - Changes to settings: * `fonts.tabs` has been split into `fonts.tabs.{selected,unselected}` (see below). @@ -48,6 +51,7 @@ Changed `fonts.debug_console` is now removed and entirely replaced by `Font`. The former distinction was mainly an implementation detail, and the accepted values shouldn't have changed. +- Small performance improvements. Added ~~~~~ From 049d51d1d79753509e85087939a88011286c5090 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2020 17:03:53 +0200 Subject: [PATCH 209/245] Fix lint --- qutebrowser/browser/qutescheme.py | 2 +- tests/unit/api/test_cmdutils.py | 1 - tests/unit/test_qutebrowser.py | 5 ++--- tests/unit/utils/test_log.py | 19 +++++++++++-------- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index 5e8eb36c5..b661f533d 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -337,7 +337,7 @@ def qute_log(url: QUrl) -> _HandlerRet: try: logfilter = (log.LogFilter.parse(filter_str, only_debug=False) - if filter_str else None) + if filter_str else None) except log.InvalidLogFilterError as e: raise UrlInvalidError(e) diff --git a/tests/unit/api/test_cmdutils.py b/tests/unit/api/test_cmdutils.py index 9ca6fc2ae..58643640c 100644 --- a/tests/unit/api/test_cmdutils.py +++ b/tests/unit/api/test_cmdutils.py @@ -163,7 +163,6 @@ class TestRegister: @cmdutils.register() def fun(*args: int): """Blah.""" - pass def test_star_args_optional(self): """Check handling of *args withstar_args_optional.""" diff --git a/tests/unit/test_qutebrowser.py b/tests/unit/test_qutebrowser.py index 6dbc24351..5a792a6d2 100644 --- a/tests/unit/test_qutebrowser.py +++ b/tests/unit/test_qutebrowser.py @@ -22,8 +22,6 @@ (Mainly commandline flag parsing) """ -import argparse - import pytest from qutebrowser import qutebrowser @@ -37,7 +35,8 @@ def parser(): class TestDebugFlag: def test_valid(self, parser): - args = parser.parse_args(['--debug-flag', 'chromium', '--debug-flag', 'stack']) + args = parser.parse_args(['--debug-flag', 'chromium', + '--debug-flag', 'stack']) assert args.debug_flags == ['chromium', 'stack'] def test_invalid(self, parser, capsys): diff --git a/tests/unit/utils/test_log.py b/tests/unit/utils/test_log.py index 8c2e636a1..f73b88b2c 100644 --- a/tests/unit/utils/test_log.py +++ b/tests/unit/utils/test_log.py @@ -159,12 +159,14 @@ class TestLogFilter: record = self._make_record(logger, 'bacon', level=logging.INFO) assert logfilter.filter(record) == only_debug - @pytest.mark.parametrize('category, filter_str, logged_before, logged_after', [ - ('init', 'url,js', True, False), - ('url', 'url,js', False, True), - ('js', 'url,js', False, True), - ('js', 'none', False, True), - ]) + @pytest.mark.parametrize( + 'category, filter_str, logged_before, logged_after', [ + ('init', 'url,js', True, False), + ('url', 'url,js', False, True), + ('js', 'url,js', False, True), + ('js', 'none', False, True), + ] + ) def test_debug_log_filter_cmd(self, monkeypatch, logger, category, filter_str, logged_before, logged_after): """Test the :debug-log-filter command handler.""" @@ -236,8 +238,9 @@ class TestInitLog: def _get_default_args(self): return argparse.Namespace(debug=True, loglevel='debug', color=True, - loglines=10, logfilter=None, force_color=False, - json_logging=False, debug_flags=set()) + loglines=10, logfilter=None, + force_color=False, json_logging=False, + debug_flags=set()) @pytest.fixture(autouse=True) def setup(self, mocker): From f9bcda014727f97b3b253a922a155d6499202166 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2020 22:20:17 +0200 Subject: [PATCH 210/245] Fix unhandled SelectionUnsupportedError in :completion-item-yank --- doc/changelog.asciidoc | 2 ++ qutebrowser/completion/completionwidget.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index eaf4f5146..03d97f87b 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -67,6 +67,8 @@ Fixed - Crash when `tabs.focus_stack_size` is set to -1. - Crash when a `pdf.js` file for PDF.js exists, but `viewer.html` does not. +- Crash when `:completion-item-yank --sel` is used on a platform without + primary selection support (e.g. Windows/macOS). - `:config-write-py` now works with paths starting with `~/...` again. - New site-specific quirk for a missing `globalThis` in Qt <= 5.12 on Reddit and Spotify diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index b9be0bd9d..26fbcdf4f 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -424,4 +424,8 @@ class CompletionView(QTreeView): if not index.isValid(): raise cmdutils.CommandError("No item selected!") text = self.model().data(index) + + if not utils.supports_selection(): + sel = False + utils.set_clipboard(text, selection=sel) From 24f7b1f36977033058ceebd73aa2af8bed2c1320 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 12 Jun 2020 12:31:05 +0200 Subject: [PATCH 211/245] requirements: Remove lxml from dev requirements d805e2d71e8e54428aae9a814a898e4ba7004118 removed lxml in check_coverage.py, 5e64aae70be25e8871846af15ff92cfce7a816d9 removed ua_fetch.py Thus, no lxml usages are remaining. --- misc/requirements/requirements-dev.txt | 1 - misc/requirements/requirements-dev.txt-raw | 1 - 2 files changed, 2 deletions(-) diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt index 2e7939deb..cf4d246f4 100644 --- a/misc/requirements/requirements-dev.txt +++ b/misc/requirements/requirements-dev.txt @@ -11,7 +11,6 @@ github3.py==1.3.0 hunter==3.1.3 idna==2.9 jwcrypto==0.7 -lxml==4.5.1 manhole==1.6.0 packaging==20.4 pycparser==2.20 diff --git a/misc/requirements/requirements-dev.txt-raw b/misc/requirements/requirements-dev.txt-raw index f75a837af..71e19f502 100644 --- a/misc/requirements/requirements-dev.txt-raw +++ b/misc/requirements/requirements-dev.txt-raw @@ -4,5 +4,4 @@ pympler github3.py bump2version requests -lxml pyqt-builder From f07c7043673970426fe903c4c1927ce0cd46f27f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 12 Jun 2020 12:40:12 +0200 Subject: [PATCH 212/245] Improve some init logging --- qutebrowser/app.py | 4 +++- qutebrowser/browser/webkit/network/networkmanager.py | 9 +++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index abb6dfa76..c90de481e 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -485,7 +485,9 @@ class Application(QApplication): self._last_focus_object = None qt_args = configinit.qt_args(args) - log.init.debug("Qt arguments: {}, based on {}".format(qt_args, args)) + log.init.debug("Commandline args: {}".format(sys.argv[1:])) + log.init.debug("Parsed: {}".format(args)) + log.init.debug("Qt arguments: {}".format(qt_args[1:])) super().__init__(qt_args) objects.args = args diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index 1662f84fc..954c92a89 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -105,7 +105,7 @@ def _is_secure_cipher(cipher): def init(): """Disable insecure SSL ciphers on old Qt versions.""" default_ciphers = QSslSocket.defaultCiphers() - log.init.debug("Default Qt ciphers: {}".format( + log.init.vdebug("Default Qt ciphers: {}".format( ', '.join(c.name() for c in default_ciphers))) good_ciphers = [] @@ -116,9 +116,10 @@ def init(): else: bad_ciphers.append(cipher) - log.init.debug("Disabling bad ciphers: {}".format( - ', '.join(c.name() for c in bad_ciphers))) - QSslSocket.setDefaultCiphers(good_ciphers) + if bad_ciphers: + log.init.debug("Disabling bad ciphers: {}".format( + ', '.join(c.name() for c in bad_ciphers))) + QSslSocket.setDefaultCiphers(good_ciphers) _SavedErrorsType = typing.MutableMapping[urlutils.HostTupleType, From 2880e8215b1bfda747f8cca40958cd6990d8211b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 12 Jun 2020 13:53:46 +0200 Subject: [PATCH 213/245] Adjust version number in session warning --- qutebrowser/html/warning-sessions.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/html/warning-sessions.html b/qutebrowser/html/warning-sessions.html index dd0c4127b..0c6622df6 100644 --- a/qutebrowser/html/warning-sessions.html +++ b/qutebrowser/html/warning-sessions.html @@ -9,7 +9,7 @@ qute://warning/sessions to show it again at a later time.

Since Qt doesn't provide an API to load the history of a tab, qutebrowser relies on a reverse-engineered binary serialization format to load tab history from session files. With Qt 5.15, unfortunately that format changed (due to the underlying Chromium upgrade), in a way which makes it impossible for qutebrowser to load tab history from existing session data.

-

At the time of writing (April 2020), a new session format which stores part of the needed binary data in saved sessions is in development and will be released with qutebrowser v1.12.0.

+

At the time of writing (April 2020), a new session format which stores part of the needed binary data in saved sessions is in development and is expected to be released with qutebrowser v1.13.0.

As a stop-gap measure:

From 0c6c566996372df13c8d846493d3008d0ec40af2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 12 Jun 2020 23:43:45 +0200 Subject: [PATCH 214/245] configdata: Remove comment about darkModeClassifierType My analysis was correct and the setting now got removed from Chromium: https://chromium-review.googlesource.com/c/chromium/src/+/2169489 Also see some other refactorings in the linked bug report: https://bugs.chromium.org/p/chromium/issues/detail?id=1091095 See #5394 --- qutebrowser/config/configdata.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index cbf18660e..12c0d8e0b 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -2619,11 +2619,6 @@ colors.webpage.prefers_color_scheme_dark: ## dark mode -# darkModeClassifierType is not exposed, as the icon classifier isn't actually -# implemented in Chromium: -# -# https://source.chromium.org/chromium/chromium/src/+/master:third_party/blink/renderer/platform/graphics/dark_mode_icon_classifier.cc - colors.webpage.darkmode.enabled: default: false type: Bool From 306eff2f94a665b4e41ea4dd5604a604283f8dba Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 13 Jun 2020 00:19:01 +0200 Subject: [PATCH 215/245] Improve lost focusproxy handling Disable the workaround if not on Qt 5.11, don't log if nothing was found, and clean up the code a bit. --- qutebrowser/browser/webengine/webview.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py index 2b197323b..be507c8b6 100644 --- a/qutebrowser/browser/webengine/webview.py +++ b/qutebrowser/browser/webengine/webview.py @@ -66,20 +66,26 @@ class WebEngineView(QWebEngineView): However, it sometimes isn't, so we use this as a WORKAROUND for https://bugreports.qt.io/browse/QTBUG-68727 - This got introduced in Qt 5.11.0 and fixed in 5.12.0. + The above bug got introduced in Qt 5.11.0 and fixed in 5.12.0. """ - if 'lost-focusproxy' not in objects.debug_flags: - proxy = self.focusProxy() - if proxy is not None: - return proxy + proxy = self.focusProxy() + + if 'lost-focusproxy' in objects.debug_flags: + proxy = None + + if (proxy is not None or + not qtutils.version_check('5.11', compiled=False) or + qtutils.version_check('5.12', compiled=False)): + return proxy # We don't want e.g. a QMenu. rwhv_class = 'QtWebEngineCore::RenderWidgetHostViewQtDelegateWidget' children = [c for c in self.findChildren(QWidget) if c.isVisible() and c.inherits(rwhv_class)] - log.webview.debug("Found possibly lost focusProxy: {}" - .format(children)) + if children: + log.webview.debug("Found possibly lost focusProxy: {}" + .format(children)) return children[-1] if children else None From 6d8553af3a58687811fce1b1f48cd4d0ec73be42 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 13 Jun 2020 00:40:08 +0200 Subject: [PATCH 216/245] Fix lint --- qutebrowser/browser/webengine/webview.py | 6 ++++-- qutebrowser/browser/webkit/network/networkmanager.py | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py index be507c8b6..9f2984f8d 100644 --- a/qutebrowser/browser/webengine/webview.py +++ b/qutebrowser/browser/webengine/webview.py @@ -19,6 +19,8 @@ """The main browser widget for QtWebEngine.""" +import typing + from PyQt5.QtCore import pyqtSignal, QUrl, PYQT_VERSION from PyQt5.QtGui import QPalette from PyQt5.QtWidgets import QWidget @@ -68,7 +70,7 @@ class WebEngineView(QWebEngineView): The above bug got introduced in Qt 5.11.0 and fixed in 5.12.0. """ - proxy = self.focusProxy() + proxy = self.focusProxy() # type: typing.Optional[QWidget] if 'lost-focusproxy' in objects.debug_flags: proxy = None @@ -85,7 +87,7 @@ class WebEngineView(QWebEngineView): if children: log.webview.debug("Found possibly lost focusProxy: {}" - .format(children)) + .format(children)) return children[-1] if children else None diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index 954c92a89..0f5063cfb 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -105,8 +105,9 @@ def _is_secure_cipher(cipher): def init(): """Disable insecure SSL ciphers on old Qt versions.""" default_ciphers = QSslSocket.defaultCiphers() - log.init.vdebug("Default Qt ciphers: {}".format( - ', '.join(c.name() for c in default_ciphers))) + log.init.vdebug( # type: ignore[attr-defined] + "Default Qt ciphers: {}".format( + ', '.join(c.name() for c in default_ciphers))) good_ciphers = [] bad_ciphers = [] From 8add93f49b01b13bca32f036825cff2ed8fa6ad6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 15 Jun 2020 15:57:38 +0200 Subject: [PATCH 217/245] Add note about colors.webpage.darkmode.policy.images on Qt 5.15.0 See #5505 --- doc/help/settings.asciidoc | 1 + qutebrowser/config/configdata.yml | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 466b0ec1b..725611a69 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -1643,6 +1643,7 @@ On QtWebKit, this setting is unavailable. [[colors.webpage.darkmode.policy.images]] === colors.webpage.darkmode.policy.images Which images to apply dark mode to. +WARNING: On Qt 5.15.0, this setting can cause frequent renderer process crashes due to a https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/304211[bug in Qt]. This setting requires a restart. Type: <> diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 12c0d8e0b..40cf9fa0b 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -2690,7 +2690,12 @@ colors.webpage.darkmode.policy.images: - always: Apply dark mode filter to all images. - never: Never apply dark mode filter to any images. - smart: Apply dark mode based on image content. - desc: Which images to apply dark mode to. + desc: >- + Which images to apply dark mode to. + + WARNING: On Qt 5.15.0, this setting can cause frequent renderer process + crashes due to a + https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/304211[bug in Qt]. restart: true backend: QtWebEngine: Qt 5.14 From ed8de2786be659dedaee95d3bef786bd57116435 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 15 Jun 2020 19:03:28 +0200 Subject: [PATCH 218/245] Fix exception with invalid feature permission URLs --- doc/changelog.asciidoc | 2 ++ qutebrowser/browser/webengine/webenginetab.py | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 03d97f87b..1c76fff27 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -69,6 +69,8 @@ Fixed - Crash when a `pdf.js` file for PDF.js exists, but `viewer.html` does not. - Crash when `:completion-item-yank --sel` is used on a platform without primary selection support (e.g. Windows/macOS). +- Crash when there's a feature permission request from Qt with an invalid URL + (which seems to happen with Qt 5.15 sometimes). - `:config-write-py` now works with paths starting with `~/...` again. - New site-specific quirk for a missing `globalThis` in Qt <= 5.12 on Reddit and Spotify diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 85190e1c7..683b2c1fd 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -969,9 +969,17 @@ class _WebEnginePermissions(QObject): page.setFeaturePermission, url, feature, QWebEnginePage.PermissionDeniedByUser) + permission_str = debug.qenum_key(QWebEnginePage, feature) + + if not url.isValid(): + log.webview.warning("Ignoring feature permission {} for invalid " + "URL {}".format(permission_str, url)) + deny_permission() + return + if feature not in self._options: log.webview.error("Unhandled feature permission {}".format( - debug.qenum_key(QWebEnginePage, feature))) + permission_str)) deny_permission() return From e39b2335d6a376633f28cb80d590f44cd4775bcb Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 15 Jun 2020 20:24:13 +0200 Subject: [PATCH 219/245] Create codeql-analysis.yml --- .github/workflows/codeql-analysis.yml | 51 +++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 000000000..da885d80b --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,51 @@ +name: "Code scanning - action" + +on: + push: + pull_request: + schedule: + - cron: '0 3 * * 1' + +jobs: + CodeQL-Build: + + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + # Override language selection by uncommenting this and choosing your languages + # with: + # languages: go, javascript, csharp, python, cpp, java + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 From 7cbc447c55152aeeb06606f03f32e7f9ad161f83 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 15 Jun 2020 20:27:14 +0200 Subject: [PATCH 220/245] Code scanning: Exclude C++ --- .github/workflows/codeql-analysis.yml | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index da885d80b..5de8a8726 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,4 +1,4 @@ -name: "Code scanning - action" +name: "Code scanning" on: push: @@ -24,28 +24,10 @@ jobs: - run: git checkout HEAD^2 if: ${{ github.event_name == 'pull_request' }} - # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v1 - # Override language selection by uncommenting this and choosing your languages - # with: - # languages: go, javascript, csharp, python, cpp, java - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release + with: + languages: javascript, python - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 From 267fb64fcb2f3324cfedeafc198b31bbfcee1e68 Mon Sep 17 00:00:00 2001 From: Nicholas Lantz Date: Mon, 15 Jun 2020 16:07:34 -0600 Subject: [PATCH 221/245] Renamed a couple of mouse settings. Changed: - input.mouse_backforward to input.mouse.back_forward_buttons - input.rocker_gestures to input.mouse.rocker_gestures --- doc/help/settings.asciidoc | 26 +++++++++++++------------- qutebrowser/browser/eventfilter.py | 6 +++--- qutebrowser/config/configdata.yml | 17 ++++++++++------- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 9122571f3..4d68cf3a7 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -252,9 +252,9 @@ |<>|Leave insert mode when starting a new page load. |<>|Switch to insert mode when clicking flash and other plugins. |<>|Include hyperlinks in the keyboard focus chain when tabbing. -|<>|Enable back and forward buttons on the mouse. +|<>|Enable back and forward buttons on the mouse. +|<>|Enable Opera-like mouse rocker gestures. |<>|Timeout (in milliseconds) for partially typed key bindings. -|<>|Enable Opera-like mouse rocker gestures. |<>|Enable spatial navigation. |<>|Keychains that shouldn't be shown in the keyhint dialog. |<>|Time (in milliseconds) from pressing a key to seeing the keyhint dialog. @@ -3213,14 +3213,23 @@ Type: <> Default: +pass:[true]+ -[[input.mouse_backforward]] -=== input.mouse_backforward +[[input.mouse.back_forward_buttons]] +=== input.mouse.back_forward_buttons Enable back and forward buttons on the mouse. Type: <> Default: +pass:[true]+ +[[input.mouse.rocker_gestures]] +=== input.mouse.rocker_gestures +Enable Opera-like mouse rocker gestures. +This disables the context menu. + +Type: <> + +Default: +pass:[false]+ + [[input.partial_timeout]] === input.partial_timeout Timeout (in milliseconds) for partially typed key bindings. @@ -3230,15 +3239,6 @@ Type: <> Default: +pass:[5000]+ -[[input.rocker_gestures]] -=== input.rocker_gestures -Enable Opera-like mouse rocker gestures. -This disables the context menu. - -Type: <> - -Default: +pass:[false]+ - [[input.spatial_navigation]] === input.spatial_navigation Enable spatial navigation. diff --git a/qutebrowser/browser/eventfilter.py b/qutebrowser/browser/eventfilter.py index d78b608a5..9e93fd13f 100644 --- a/qutebrowser/browser/eventfilter.py +++ b/qutebrowser/browser/eventfilter.py @@ -116,7 +116,7 @@ class TabEventFilter(QObject): Return: True if the event should be filtered, False otherwise. """ - is_rocker_gesture = (config.val.input.rocker_gestures and + is_rocker_gesture = (config.val.input.mouse.rocker_gestures and e.buttons() == Qt.LeftButton | Qt.RightButton) if e.button() in [Qt.XButton1, Qt.XButton2] or is_rocker_gesture: @@ -204,7 +204,7 @@ class TabEventFilter(QObject): Return: True if the event should be filtered, False otherwise. """ - return config.val.input.rocker_gestures + return config.val.input.mouse.rocker_gestures def _handle_key_release(self, e): """Ignore repeated key release events going to the website. @@ -276,7 +276,7 @@ class TabEventFilter(QObject): Return: True if the event should be filtered, False otherwise. """ - if (not config.val.input.mouse_backforward and + if (not config.val.input.mouse.back_forward_buttons and e.button() in [Qt.XButton1, Qt.XButton2]): # Back and forward on mice are disabled return diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 15a687283..67d2eef5f 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -1379,11 +1379,19 @@ input.links_included_in_focus_chain: supports_pattern: true desc: Include hyperlinks in the keyboard focus chain when tabbing. -input.mouse_backforward: +input.mouse.back_forward_buttons: default: true type: Bool desc: Enable back and forward buttons on the mouse. +input.mouse.rocker_gestures: + default: false + type: Bool + desc: >- + Enable Opera-like mouse rocker gestures. + + This disables the context menu. + input.partial_timeout: default: 5000 type: @@ -1397,12 +1405,7 @@ input.partial_timeout: cleared after this time. input.rocker_gestures: - default: false - type: Bool - desc: >- - Enable Opera-like mouse rocker gestures. - - This disables the context menu. + renamed: input.mouse.rocker_gestures input.spatial_navigation: default: false From 590b73015bdf15ec181a7c53e82f7641cd545c33 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 16 Jun 2020 16:28:07 +0200 Subject: [PATCH 222/245] Rename urlmarks.py to test_urlmarks.py --- tests/unit/browser/{urlmarks.py => test_urlmarks.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/unit/browser/{urlmarks.py => test_urlmarks.py} (100%) diff --git a/tests/unit/browser/urlmarks.py b/tests/unit/browser/test_urlmarks.py similarity index 100% rename from tests/unit/browser/urlmarks.py rename to tests/unit/browser/test_urlmarks.py From 2e12ba21afcba7989e574a4819929bf2bdbe0181 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 16 Jun 2020 17:19:32 +0200 Subject: [PATCH 223/245] Improve QtWebKit/QtWebEngine import checks This handles two cases found in crash reports: - "AssertionError: qWebKitVersion is not None" when QtWebKit itself is importable but qWebKitVersion is not. Fixes #5484. - "ValueError: PyCapsule_GetPointer called with incorrect name" when importing QtWebKit - no idea what's going on here exactly. Note that there might be other cases in the code where those imports are attempted and only ImportError is handled (because ValueError really shouldn't happen there...) - but we can't really track them down until we see more crash reports with the same issue. --- doc/changelog.asciidoc | 2 ++ qutebrowser/misc/backendproblem.py | 5 +++-- qutebrowser/utils/version.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 1c76fff27..3f8a48b27 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -71,6 +71,8 @@ Fixed primary selection support (e.g. Windows/macOS). - Crash when there's a feature permission request from Qt with an invalid URL (which seems to happen with Qt 5.15 sometimes). +- Crash in rare cases where QtWebKit/QtWebEngine imports fail in unexpected + ways. - `:config-write-py` now works with paths starting with `~/...` again. - New site-specific quirk for a missing `globalThis` in Qt <= 5.12 on Reddit and Spotify diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py index 6e2bd1866..089e3191f 100644 --- a/qutebrowser/misc/backendproblem.py +++ b/qutebrowser/misc/backendproblem.py @@ -362,8 +362,9 @@ class _BackendProblemChecker: try: from PyQt5 import QtWebKit + from PyQt5.QtWebKit import qWebKitVersion from PyQt5 import QtWebKitWidgets - except ImportError as e: + except (ImportError, ValueError) as e: results.webkit_available = False results.webkit_error = str(e) else: @@ -375,7 +376,7 @@ class _BackendProblemChecker: try: from PyQt5 import QtWebEngineWidgets - except ImportError as e: + except (ImportError, ValueError) as e: results.webengine_available = False results.webengine_error = str(e) else: diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index c0ff7e1ac..75c6f6ede 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -234,7 +234,7 @@ def _module_versions() -> typing.Sequence[str]: for modname, attributes in modules.items(): try: module = importlib.import_module(modname) - except ImportError: + except (ImportError, ValueError): text = '{}: no'.format(modname) else: for name in attributes: From 06e23f44f8f1558b21723c9279870ad862465bce Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 16 Jun 2020 17:30:42 +0200 Subject: [PATCH 224/245] tests: Stabilize test_qute_settings_persistence for real Before, the "Config option changed: *" triggered on: Config option changed: content.headers.user_agent = ... https://github.com/qutebrowser/qutebrowser/issues/5390#issuecomment-628773737 --- tests/end2end/test_invocations.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py index a31494634..b4a343a37 100644 --- a/tests/end2end/test_invocations.py +++ b/tests/end2end/test_invocations.py @@ -382,7 +382,8 @@ def test_qute_settings_persistence(short_tmpdir, request, quteproc_new): 'cset("search.ignore_case", "always")') quteproc_new.wait_for(message='No output or error') quteproc_new.wait_for(category='config', - message='Config option changed: *') + message='Config option changed: ' + 'search.ignore_case = always') assert quteproc_new.get_setting('search.ignore_case') == 'always' From ef0482cabbf07952b164ebd6ad252d42d45102dc Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 16 Jun 2020 17:36:07 +0200 Subject: [PATCH 225/245] Update tests/manual/mouse.html --- tests/manual/mouse.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/manual/mouse.html b/tests/manual/mouse.html index eb75df44d..d1f0f7dee 100644 --- a/tests/manual/mouse.html +++ b/tests/manual/mouse.html @@ -10,7 +10,8 @@
  • When clicking the link with shift, tabs.background should be reversed accordingly.
  • Ctrl + Mousewheel should zoom in/out
  • Back/forward keys on mouse should navigate back/forward
  • -
  • With input.rocker_gestures set, no context menu should be shown, but pressing left+right/right+left buttons should navigate back/forward
  • -
  • When setting input.rocker_gestures dynamically, the context menu should be hidden/shown accordingly.
  • +
  • If input.mouse.back_forward_buttons is set to false, those buttons should not have any effect
  • +
  • With input.mouse.rocker_gestures set, no context menu should be shown, but pressing left+right/right+left buttons should navigate back/forward
  • +
  • When setting input.mouse.rocker_gestures dynamically, the context menu should be hidden/shown accordingly.
  • From 0e02c910b29d501485459635aa643a390c5ba728 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 16 Jun 2020 17:44:20 +0200 Subject: [PATCH 226/245] Update changelog --- doc/changelog.asciidoc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 3f8a48b27..420062f43 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -51,6 +51,7 @@ Changed `fonts.debug_console` is now removed and entirely replaced by `Font`. The former distinction was mainly an implementation detail, and the accepted values shouldn't have changed. + * `input.rocker_gestures` has been renamed to `input.mouse.rocker_gestures`. - Small performance improvements. Added @@ -61,6 +62,8 @@ Added logging levels via the config. * `fonts.tabs.selected` and `fonts.tabs.unselected` to set the font of the selected tab independently from unselected tabs (e.g. to make it bold). + * `input.mouse.back_forward_buttons` which can be set to `false` to disable + back/forward mouse buttons. Fixed ~~~~~ From ed227b052b7fe7b4a87789a6a5fa0df00c32295d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 16 Jun 2020 18:49:25 +0200 Subject: [PATCH 227/245] Fix test_urlmarks.py --- tests/unit/browser/test_urlmarks.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/unit/browser/test_urlmarks.py b/tests/unit/browser/test_urlmarks.py index b795d5b10..ac86dd338 100644 --- a/tests/unit/browser/test_urlmarks.py +++ b/tests/unit/browser/test_urlmarks.py @@ -17,7 +17,9 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -"""Tests for the global page history.""" +"""Tests for bookmarks/quickmarks.""" + +import unittest.mock import pytest from PyQt5.QtCore import QUrl @@ -44,8 +46,8 @@ def test_init(bm_file, fake_save_manager): bm = urlmarks.BookmarkManager() fake_save_manager.add_saveable.assert_called_once_with( 'bookmark-manager', - bm.save, - bm.changed, + unittest.mock.ANY, + unittest.mock.ANY, filename=str(bm_file), ) From a38620a0f92738cef7a4ce95f1cfe3336a3bf3f4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 16 Jun 2020 18:55:25 +0200 Subject: [PATCH 228/245] userscripts: Clear self._filepath on OSError As an additional precaution that a "tainted" path isn't being used anywhere. See https://github.com/qutebrowser/qutebrowser/security/code-scanning/2 --- qutebrowser/commands/userscripts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py index b9ef41f01..57ea474d4 100644 --- a/qutebrowser/commands/userscripts.py +++ b/qutebrowser/commands/userscripts.py @@ -262,6 +262,7 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner): os.mkfifo(self._filepath) # pylint: enable=no-member,useless-suppression except OSError as e: + self._filepath = None # Make sure it's not used message.error("Error while creating FIFO: {}".format(e)) return From af3f088a8f777fd67dae2e8367d54fe8400ee86f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 16 Jun 2020 19:10:21 +0200 Subject: [PATCH 229/245] Use 0600 mode for uerscript FIFO os.mkfifo uses mode 0666 masked with the umask, i.e. 0644 on most systems by default. We'd like things to be more restrictive, to make sure other users can't get any data from another users' userscript FIFO. This shouldn't make any difference in practice because the FIFO is in standarddir.runtime() which must have 0700 permissions (and qutebrowser creates all standarddir directories with 0700 permissions if they don't exist). However, it's still a good idea to restrict the permissions as a second line of defense. --- qutebrowser/commands/userscripts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py index 57ea474d4..485161600 100644 --- a/qutebrowser/commands/userscripts.py +++ b/qutebrowser/commands/userscripts.py @@ -259,7 +259,7 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner): self._filepath = tempfile.mktemp(prefix='qutebrowser-userscript-', dir=standarddir.runtime()) # pylint: disable=no-member,useless-suppression - os.mkfifo(self._filepath) + os.mkfifo(self._filepath, mode=0o600) # pylint: enable=no-member,useless-suppression except OSError as e: self._filepath = None # Make sure it's not used From b300a5464ffaff32bd22d87ebb9f9a5a03d0df63 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Jun 2020 09:34:12 +0200 Subject: [PATCH 230/245] Point to GitHub discussions feature --- .github/ISSUE_TEMPLATE/3_Support_question.md | 16 ---------------- .github/ISSUE_TEMPLATE/config.yml | 5 +++++ README.asciidoc | 5 +++++ 3 files changed, 10 insertions(+), 16 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/3_Support_question.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/3_Support_question.md b/.github/ISSUE_TEMPLATE/3_Support_question.md deleted file mode 100644 index 9d67d716b..000000000 --- a/.github/ISSUE_TEMPLATE/3_Support_question.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: ❓ Support Question -about: It's okay to ask questions via GitHub, but IRC/Reddit/Mailinglist might be better. - ---- - - - -**Version info (see `:version`)**: - -**If applicable: Does the issue happen if you start with `--temp-basedir`?**: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..f736a6fd6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: ❓ Support Question + url: https://github.com/qutebrowser/qutebrowser/discussions + about: Use GitHub's new discussions feature for questions diff --git a/README.asciidoc b/README.asciidoc index 3a7778def..1c943da40 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -81,6 +81,11 @@ get sent to the general qutebrowser@ list). If you're a reddit user, there's a https://www.reddit.com/r/qutebrowser/[/r/qutebrowser] subreddit there. +Finally, qutebrowser is participating in the Beta for GitHub's new Discussions +feature, so you can also use the +https://github.com/qutebrowser/qutebrowser/discussions[discussions tab] on +GitHub to get in touch. + Contributions / Bugs -------------------- From 03a5291d0596989bba8e056c9be06489330bb9d0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Jun 2020 11:32:18 +0200 Subject: [PATCH 231/245] basekeyparser: Remove old config format comment --- qutebrowser/keyinput/basekeyparser.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 3c4adee49..9eb20c166 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -322,9 +322,6 @@ class BaseKeyParser(QObject): def _read_config(self, modename: str = None) -> None: """Read the configuration. - Config format: key = command, e.g.: - = quit - Args: modename: Name of the mode to use. """ From ccc9d1779c3d753a9c2bc1d488ec32d85e7fc135 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Jun 2020 13:16:14 +0200 Subject: [PATCH 232/245] modeparsers: Refactor to avoid subclassing Before the changes in this commit, we've had to have a subclassed parser for every mode, even if there was no special key handling going on in that mode. With a couple of changes, we can avoid many of those subclasses and only have subclasses for bigger changes (like hint or register modes). - The awkward handling of self._modename in _read_config() is now removed. _read_config() doesn't take an argument, always uses the mode in self._mode and gets called from __init__. - BaseKeyParser takes the mode as an argument to __init__. - The class attributes (do_log/passthrough/supports_count) now also get passed via the constructor. --- qutebrowser/keyinput/basekeyparser.py | 63 +++-- qutebrowser/keyinput/modeman.py | 56 +++-- qutebrowser/keyinput/modeparsers.py | 119 +++------- qutebrowser/utils/usertypes.py | 4 + tests/unit/keyinput/test_basekeyparser.py | 272 ++++++++++------------ 5 files changed, 218 insertions(+), 296 deletions(-) diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 9eb20c166..cc7fdc5be 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -140,23 +140,16 @@ class BaseKeyParser(QObject): Not intended to be instantiated directly. Subclasses have to override execute() to do whatever they want to. - Class Attributes: - Match: types of a match between a binding and the keystring. - partial: No keychain matched yet, but it's still possible in the - future. - definitive: Keychain matches exactly. - none: No more matches possible. - - do_log: Whether to log keypresses or not. - passthrough: Whether unbound keys should be passed through with this - handler. - supports_count: Whether count is supported. - Attributes: + mode_name: The name of the mode in the config. bindings: Bound key bindings + _mode: The usertypes.KeyMode associated with this keyparser. _win_id: The window ID this keyparser is associated with. _sequence: The currently entered key sequence - _modename: The name of the input mode associated with this keyparser. + _do_log: Whether to log keypresses or not. + passthrough: Whether unbound keys should be passed through with this + handler. + _supports_count: Whether count is supported. Signals: keystring_updated: Emitted when the keystring is updated. @@ -169,21 +162,31 @@ class BaseKeyParser(QObject): keystring_updated = pyqtSignal(str) request_leave = pyqtSignal(usertypes.KeyMode, str, bool) - do_log = True - passthrough = False - supports_count = True - def __init__(self, win_id: int, parent: QObject = None) -> None: + def __init__(self, *, mode: usertypes.KeyMode, + win_id: int, + parent: QObject = None, + do_log: bool = True, + passthrough: bool = False, + supports_count: bool = True) -> None: super().__init__(parent) self._win_id = win_id - self._modename = None self._sequence = keyutils.KeySequence() self._count = '' + self._mode = mode + self._do_log = do_log + self.passthrough = passthrough + self._supports_count = supports_count self.bindings = BindingTrie() + self._read_config() config.instance.changed.connect(self._on_config_changed) def __repr__(self) -> str: - return utils.get_repr(self) + return utils.get_repr(self, mode=self._mode, + win_id=self._win_id, + do_log=self._do_log, + passthrough=self.passthrough, + supports_count=self._supports_count) def _debug_log(self, message: str) -> None: """Log a message to the debug log if logging is active. @@ -191,7 +194,7 @@ class BaseKeyParser(QObject): Args: message: The message to log. """ - if self.do_log: + if self._do_log: log.keyboard.debug(message) def _match_key(self, sequence: keyutils.KeySequence) -> MatchResult: @@ -234,7 +237,7 @@ class BaseKeyParser(QObject): dry_run: bool) -> bool: """Try to match a key as count.""" txt = str(sequence[-1]) # To account for sequences changed above. - if (txt in string.digits and self.supports_count and + if (txt in string.digits and self._supports_count and not (not self._count and txt == '0')): self._debug_log("Trying match as count") assert len(txt) == 1, txt @@ -319,22 +322,12 @@ class BaseKeyParser(QObject): def _on_config_changed(self) -> None: self._read_config() - def _read_config(self, modename: str = None) -> None: - """Read the configuration. - - Args: - modename: Name of the mode to use. - """ - if modename is None: - if self._modename is None: - raise ValueError("read_config called with no mode given, but " - "None defined so far!") - modename = self._modename - else: - self._modename = modename + def _read_config(self) -> None: + """Read the configuration.""" self.bindings = BindingTrie() + config_bindings = config.key_instance.get_bindings_for(self._mode.name) - for key, cmd in config.key_instance.get_bindings_for(modename).items(): + for key, cmd in config_bindings.items(): assert cmd self.bindings[key] = cmd diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index eb96020f3..2ec956422 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -102,70 +102,86 @@ def init(win_id: int, parent: QObject) -> 'ModeManager': parent=modeman), usertypes.KeyMode.insert: - modeparsers.PassthroughKeyParser( - win_id=win_id, + modeparsers.CommandKeyParser( mode=usertypes.KeyMode.insert, + win_id=win_id, commandrunner=commandrunner, - parent=modeman), + parent=modeman, + passthrough=True, + do_log=False, + supports_count=False), usertypes.KeyMode.passthrough: - modeparsers.PassthroughKeyParser( - win_id=win_id, + modeparsers.CommandKeyParser( mode=usertypes.KeyMode.passthrough, + win_id=win_id, commandrunner=commandrunner, - parent=modeman), + parent=modeman, + passthrough=True, + do_log=False, + supports_count=False), usertypes.KeyMode.command: - modeparsers.PassthroughKeyParser( - win_id=win_id, + modeparsers.CommandKeyParser( mode=usertypes.KeyMode.command, + win_id=win_id, commandrunner=commandrunner, - parent=modeman), + parent=modeman, + passthrough=True, + do_log=False, + supports_count=False), usertypes.KeyMode.prompt: - modeparsers.PassthroughKeyParser( - win_id=win_id, + modeparsers.CommandKeyParser( mode=usertypes.KeyMode.prompt, + win_id=win_id, commandrunner=commandrunner, - parent=modeman), + parent=modeman, + passthrough=True, + do_log=False, + supports_count=False), usertypes.KeyMode.yesno: - modeparsers.PromptKeyParser( + modeparsers.CommandKeyParser( + mode=usertypes.KeyMode.yesno, win_id=win_id, commandrunner=commandrunner, - parent=modeman), + parent=modeman, + supports_count=False), usertypes.KeyMode.caret: - modeparsers.CaretKeyParser( + modeparsers.CommandKeyParser( + mode=usertypes.KeyMode.caret, win_id=win_id, commandrunner=commandrunner, - parent=modeman), + parent=modeman, + passthrough=True), usertypes.KeyMode.set_mark: modeparsers.RegisterKeyParser( - win_id=win_id, mode=usertypes.KeyMode.set_mark, + win_id=win_id, commandrunner=commandrunner, parent=modeman), usertypes.KeyMode.jump_mark: modeparsers.RegisterKeyParser( - win_id=win_id, mode=usertypes.KeyMode.jump_mark, + win_id=win_id, commandrunner=commandrunner, parent=modeman), usertypes.KeyMode.record_macro: modeparsers.RegisterKeyParser( - win_id=win_id, mode=usertypes.KeyMode.record_macro, + win_id=win_id, commandrunner=commandrunner, parent=modeman), usertypes.KeyMode.run_macro: modeparsers.RegisterKeyParser( - win_id=win_id, mode=usertypes.KeyMode.run_macro, + win_id=win_id, commandrunner=commandrunner, parent=modeman), } # type: ParserDictType diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index acac59ad5..e848250c0 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -51,10 +51,16 @@ class CommandKeyParser(basekeyparser.BaseKeyParser): _commandrunner: CommandRunner instance. """ - def __init__(self, win_id: int, + def __init__(self, *, mode: usertypes.KeyMode, + win_id: int, commandrunner: 'runners.CommandRunner', - parent: QObject = None) -> None: - super().__init__(win_id, parent) + parent: QObject = None, + do_log: bool = True, + passthrough: bool = False, + supports_count: bool = True) -> None: + super().__init__(mode=mode, win_id=win_id, parent=parent, + do_log=do_log, passthrough=passthrough, + supports_count=supports_count) self._commandrunner = commandrunner def execute(self, cmdstr: str, count: int = None) -> None: @@ -72,11 +78,11 @@ class NormalKeyParser(CommandKeyParser): _partial_timer: Timer to clear partial keypresses. """ - def __init__(self, win_id: int, + def __init__(self, *, win_id: int, commandrunner: 'runners.CommandRunner', parent: QObject = None) -> None: - super().__init__(win_id, commandrunner, parent) - self._read_config('normal') + super().__init__(mode=usertypes.KeyMode.normal, win_id=win_id, + commandrunner=commandrunner, parent=parent) self._partial_timer = usertypes.Timer(self, 'partial-match') self._partial_timer.setSingleShot(True) self._partial_timer.timeout.connect(self._clear_partial_match) @@ -130,55 +136,6 @@ class NormalKeyParser(CommandKeyParser): self._inhibited = False -class PassthroughKeyParser(CommandKeyParser): - - """KeyChainParser which passes through normal keys. - - Used for insert/passthrough modes. - - Attributes: - _mode: The mode this keyparser is for. - """ - - do_log = False - passthrough = True - supports_count = False - - def __init__(self, win_id: int, - mode: usertypes.KeyMode, - commandrunner: 'runners.CommandRunner', - parent: QObject = None) -> None: - """Constructor. - - Args: - mode: The mode this keyparser is for. - parent: Qt parent. - warn: Whether to warn if an ignored key was bound. - """ - super().__init__(win_id, commandrunner, parent) - self._read_config(mode.name) - self._mode = mode - - def __repr__(self) -> str: - return utils.get_repr(self, mode=self._mode) - - -class PromptKeyParser(CommandKeyParser): - - """KeyParser for yes/no prompts.""" - - supports_count = False - - def __init__(self, win_id: int, - commandrunner: 'runners.CommandRunner', - parent: QObject = None) -> None: - super().__init__(win_id, commandrunner, parent) - self._read_config('yesno') - - def __repr__(self) -> str: - return utils.get_repr(self) - - class HintKeyParser(CommandKeyParser): """KeyChainParser for hints. @@ -189,17 +146,16 @@ class HintKeyParser(CommandKeyParser): _last_press: The nature of the last keypress, a LastPress member. """ - supports_count = False - - def __init__(self, win_id: int, + def __init__(self, *, win_id: int, commandrunner: 'runners.CommandRunner', hintmanager: hints.HintManager, parent: QObject = None) -> None: - super().__init__(win_id, commandrunner, parent) + super().__init__(mode=usertypes.KeyMode.hint, win_id=win_id, + commandrunner=commandrunner, parent=parent, + supports_count=False) self._hintmanager = hintmanager self._filtertext = '' self._last_press = LastPress.none - self._read_config('hint') self.keystring_updated.connect(self._hintmanager.handle_partial_key) def _handle_filter_key(self, e: QKeyEvent) -> QKeySequence.SequenceMatch: @@ -277,37 +233,23 @@ class HintKeyParser(CommandKeyParser): self._filtertext = '' -class CaretKeyParser(CommandKeyParser): - - """KeyParser for caret mode.""" - - passthrough = True - - def __init__(self, win_id: int, - commandrunner: 'runners.CommandRunner', - parent: QObject = None) -> None: - super().__init__(win_id, commandrunner, parent) - self._read_config('caret') - - class RegisterKeyParser(CommandKeyParser): """KeyParser for modes that record a register key. Attributes: - _mode: One of KeyMode.set_mark, KeyMode.jump_mark, KeyMode.record_macro - and KeyMode.run_macro. + _register_mode: One of KeyMode.set_mark, KeyMode.jump_mark, + KeyMode.record_macro and KeyMode.run_macro. """ - supports_count = False - - def __init__(self, win_id: int, + def __init__(self, *, win_id: int, mode: usertypes.KeyMode, commandrunner: 'runners.CommandRunner', parent: QObject = None) -> None: - super().__init__(win_id, commandrunner, parent) - self._mode = mode - self._read_config('register') + super().__init__(mode=usertypes.KeyMode.register, win_id=win_id, + commandrunner=commandrunner, parent=parent, + supports_count=False) + self._register_mode = mode def handle(self, e: QKeyEvent, *, dry_run: bool = False) -> QKeySequence.SequenceMatch: @@ -326,19 +268,20 @@ class RegisterKeyParser(CommandKeyParser): window=self._win_id) try: - if self._mode == usertypes.KeyMode.set_mark: + if self._register_mode == usertypes.KeyMode.set_mark: tabbed_browser.set_mark(key) - elif self._mode == usertypes.KeyMode.jump_mark: + elif self._register_mode == usertypes.KeyMode.jump_mark: tabbed_browser.jump_mark(key) - elif self._mode == usertypes.KeyMode.record_macro: + elif self._register_mode == usertypes.KeyMode.record_macro: macros.macro_recorder.record_macro(key) - elif self._mode == usertypes.KeyMode.run_macro: + elif self._register_mode == usertypes.KeyMode.run_macro: macros.macro_recorder.run_macro(self._win_id, key) else: - raise ValueError( - "{} is not a valid register mode".format(self._mode)) + raise ValueError("{} is not a valid register mode".format( + self._register_mode)) except cmdexc.Error as err: message.error(str(err), stack=traceback.format_exc()) - self.request_leave.emit(self._mode, "valid register key", True) + self.request_leave.emit( + self._register_mode, "valid register key", True) return QKeySequence.ExactMatch diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index 247946497..0b6f9c219 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -256,6 +256,10 @@ class KeyMode(enum.Enum): jump_mark = 10 record_macro = 11 run_macro = 12 + # 'register' is a bit of an oddball here: It's not really a "real" mode, + # but it's used in the config for common bindings for + # set_mark/jump_mark/record_macro/run_macro. + register = 13 class Exit(enum.IntEnum): diff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py index a5a1eb553..2a53841fe 100644 --- a/tests/unit/keyinput/test_basekeyparser.py +++ b/tests/unit/keyinput/test_basekeyparser.py @@ -25,7 +25,7 @@ from PyQt5.QtCore import Qt import pytest from qutebrowser.keyinput import basekeyparser, keyutils -from qutebrowser.utils import utils +from qutebrowser.utils import utils, usertypes # Alias because we need this a lot in here. @@ -33,24 +33,28 @@ def keyseq(s): return keyutils.KeySequence.parse(s) -@pytest.fixture -def keyparser(key_config_stub): - """Fixture providing a BaseKeyParser supporting count/chains.""" - kp = basekeyparser.BaseKeyParser(win_id=0) +def _create_keyparser(mode): + kp = basekeyparser.BaseKeyParser(mode=mode, win_id=0) kp.execute = mock.Mock() - yield kp + return kp @pytest.fixture -def handle_text(fake_keyevent, keyparser): - """Helper function to handle multiple fake keypresses. +def keyparser(key_config_stub, keyinput_bindings): + return _create_keyparser(usertypes.KeyMode.normal) - Automatically uses the keyparser of the current test via the keyparser - fixture. - """ - def func(*args): + +@pytest.fixture +def prompt_keyparser(key_config_stub, keyinput_bindings): + return _create_keyparser(usertypes.KeyMode.prompt) + + +@pytest.fixture +def handle_text(fake_keyevent): + """Helper function to handle multiple fake keypresses.""" + def func(kp, *args): for enumval in args: - keyparser.handle(fake_keyevent(enumval)) + kp.handle(fake_keyevent(enumval)) return func @@ -63,7 +67,7 @@ class TestDebugLog: assert caplog.messages == ['foo'] def test_no_log(self, keyparser, caplog): - keyparser.do_log = False + keyparser._do_log = False keyparser._debug_log('foo') assert not caplog.records @@ -79,9 +83,8 @@ class TestDebugLog: ]) def test_split_count(config_stub, key_config_stub, input_key, supports_count, count, command): - kp = basekeyparser.BaseKeyParser(win_id=0) - kp.supports_count = supports_count - kp._read_config('normal') + kp = basekeyparser.BaseKeyParser(mode=usertypes.KeyMode.normal, win_id=0, + supports_count=supports_count) for info in keyseq(input_key): kp.handle(info.to_event()) @@ -90,80 +93,44 @@ def test_split_count(config_stub, key_config_stub, assert kp._sequence == keyseq(command) -@pytest.mark.usefixtures('keyinput_bindings') -class TestReadConfig: +def test_empty_binding(keyparser, config_stub): + """Make sure setting an empty binding doesn't crash.""" + config_stub.val.bindings.commands = {'normal': {'co': ''}} + # The config is re-read automatically - def test_read_config_invalid(self, keyparser): - """Test reading config without setting modename before.""" - with pytest.raises(ValueError): - keyparser._read_config() - def test_read_config_modename(self, keyparser): - """Test reading config with _modename set.""" - keyparser._modename = 'normal' - keyparser._read_config() - assert keyseq('a') in keyparser.bindings +@pytest.mark.parametrize('changed_mode, expected', [ + ('normal', True), ('command', False), +]) +def test_read_config(keyparser, key_config_stub, changed_mode, expected): + keyparser._read_config() + # Sanity checks + assert keyseq('a') in keyparser.bindings + assert keyseq('new') not in keyparser.bindings - def test_read_config_valid(self, keyparser): - """Test reading config.""" - keyparser._read_config('prompt') - assert keyseq('ccc') in keyparser.bindings - assert keyseq('') in keyparser.bindings - keyparser._read_config('command') - assert keyseq('ccc') not in keyparser.bindings - assert keyseq('') not in keyparser.bindings - assert keyseq('foo') in keyparser.bindings - assert keyseq('') in keyparser.bindings + key_config_stub.bind(keyseq('new'), 'message-info new', + mode=changed_mode) - def test_read_config_empty_binding(self, keyparser, config_stub): - """Make sure setting an empty binding doesn't crash.""" - keyparser._read_config('normal') - config_stub.val.bindings.commands = {'normal': {'co': ''}} - # The config is re-read automatically - - def test_read_config_modename_none(self, keyparser): - assert keyparser._modename is None - - # No config set so self._modename is None - with pytest.raises(ValueError, match="read_config called with no mode " - "given, but None defined so far!"): - keyparser._read_config(None) - - @pytest.mark.parametrize('mode, changed_mode, expected', [ - ('normal', 'normal', True), ('normal', 'command', False), - ]) - def test_read_config(self, keyparser, key_config_stub, - mode, changed_mode, expected): - keyparser._read_config(mode) - # Sanity checks - assert keyseq('a') in keyparser.bindings - assert keyseq('new') not in keyparser.bindings - - key_config_stub.bind(keyseq('new'), 'message-info new', - mode=changed_mode) - - assert keyseq('a') in keyparser.bindings - assert (keyseq('new') in keyparser.bindings) == expected + assert keyseq('a') in keyparser.bindings + assert (keyseq('new') in keyparser.bindings) == expected class TestHandle: - @pytest.fixture(autouse=True) - def read_config(self, keyinput_bindings, keyparser): - keyparser._read_config('prompt') - - def test_valid_key(self, fake_keyevent, keyparser): + def test_valid_key(self, fake_keyevent, prompt_keyparser): modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier - keyparser.handle(fake_keyevent(Qt.Key_A, modifier)) - keyparser.handle(fake_keyevent(Qt.Key_X, modifier)) - keyparser.execute.assert_called_once_with('message-info ctrla', None) - assert not keyparser._sequence + prompt_keyparser.handle(fake_keyevent(Qt.Key_A, modifier)) + prompt_keyparser.handle(fake_keyevent(Qt.Key_X, modifier)) + prompt_keyparser.execute.assert_called_once_with( + 'message-info ctrla', None) + assert not prompt_keyparser._sequence - def test_valid_key_count(self, fake_keyevent, keyparser): + def test_valid_key_count(self, fake_keyevent, prompt_keyparser): modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier - keyparser.handle(fake_keyevent(Qt.Key_5)) - keyparser.handle(fake_keyevent(Qt.Key_A, modifier)) - keyparser.execute.assert_called_once_with('message-info ctrla', 5) + prompt_keyparser.handle(fake_keyevent(Qt.Key_5)) + prompt_keyparser.handle(fake_keyevent(Qt.Key_A, modifier)) + prompt_keyparser.execute.assert_called_once_with( + 'message-info ctrla', 5) @pytest.mark.parametrize('keys', [ [(Qt.Key_B, Qt.NoModifier), (Qt.Key_C, Qt.NoModifier)], @@ -171,46 +138,47 @@ class TestHandle: # Only modifier [(Qt.Key_Shift, Qt.ShiftModifier)], ]) - def test_invalid_keys(self, fake_keyevent, keyparser, keys): + def test_invalid_keys(self, fake_keyevent, prompt_keyparser, keys): for key, modifiers in keys: - keyparser.handle(fake_keyevent(key, modifiers)) - assert not keyparser.execute.called - assert not keyparser._sequence + prompt_keyparser.handle(fake_keyevent(key, modifiers)) + assert not prompt_keyparser.execute.called + assert not prompt_keyparser._sequence - def test_dry_run(self, fake_keyevent, keyparser): - keyparser.handle(fake_keyevent(Qt.Key_B)) - keyparser.handle(fake_keyevent(Qt.Key_A), dry_run=True) - assert not keyparser.execute.called - assert keyparser._sequence + def test_dry_run(self, fake_keyevent, prompt_keyparser): + prompt_keyparser.handle(fake_keyevent(Qt.Key_B)) + prompt_keyparser.handle(fake_keyevent(Qt.Key_A), dry_run=True) + assert not prompt_keyparser.execute.called + assert prompt_keyparser._sequence - def test_dry_run_count(self, fake_keyevent, keyparser): - keyparser.handle(fake_keyevent(Qt.Key_9), dry_run=True) - assert not keyparser._count + def test_dry_run_count(self, fake_keyevent, prompt_keyparser): + prompt_keyparser.handle(fake_keyevent(Qt.Key_9), dry_run=True) + assert not prompt_keyparser._count - def test_invalid_key(self, fake_keyevent, keyparser): - keyparser.handle(fake_keyevent(Qt.Key_B)) - keyparser.handle(fake_keyevent(0x0)) - assert not keyparser._sequence + def test_invalid_key(self, fake_keyevent, prompt_keyparser): + prompt_keyparser.handle(fake_keyevent(Qt.Key_B)) + prompt_keyparser.handle(fake_keyevent(0x0)) + assert not prompt_keyparser._sequence - def test_valid_keychain(self, handle_text, keyparser): - # Press 'x' which is ignored because of no match - handle_text(Qt.Key_X, + def test_valid_keychain(self, handle_text, prompt_keyparser): + handle_text(prompt_keyparser, + # Press 'x' which is ignored because of no match + Qt.Key_X, # Then start the real chain Qt.Key_B, Qt.Key_A) - keyparser.execute.assert_called_with('message-info ba', None) - assert not keyparser._sequence + prompt_keyparser.execute.assert_called_with('message-info ba', None) + assert not prompt_keyparser._sequence @pytest.mark.parametrize('key, modifiers, number', [ (Qt.Key_0, Qt.NoModifier, 0), (Qt.Key_1, Qt.NoModifier, 1), (Qt.Key_1, Qt.KeypadModifier, 1), ]) - def test_number_press(self, fake_keyevent, keyparser, + def test_number_press(self, fake_keyevent, prompt_keyparser, key, modifiers, number): - keyparser.handle(fake_keyevent(key, modifiers)) + prompt_keyparser.handle(fake_keyevent(key, modifiers)) command = 'message-info {}'.format(number) - keyparser.execute.assert_called_once_with(command, None) - assert not keyparser._sequence + prompt_keyparser.execute.assert_called_once_with(command, None) + assert not prompt_keyparser._sequence @pytest.mark.parametrize('modifiers, text', [ (Qt.NoModifier, '2'), @@ -222,7 +190,6 @@ class TestHandle: config_stub.val.bindings.commands = {'normal': { '2': 'message-info 2', '': 'message-info num-2'}} - keyparser._read_config('normal') keyparser.handle(fake_keyevent(Qt.Key_2, modifiers)) command = 'message-info {}'.format(text) keyparser.execute.assert_called_once_with(command, None) @@ -230,44 +197,42 @@ class TestHandle: def test_umlauts(self, handle_text, keyparser, config_stub): config_stub.val.bindings.commands = {'normal': {'ü': 'message-info ü'}} - keyparser._read_config('normal') - handle_text(Qt.Key_Udiaeresis) + handle_text(keyparser, Qt.Key_Udiaeresis) keyparser.execute.assert_called_once_with('message-info ü', None) - def test_mapping(self, config_stub, handle_text, keyparser): - handle_text(Qt.Key_X) - keyparser.execute.assert_called_once_with('message-info a', None) + def test_mapping(self, config_stub, handle_text, prompt_keyparser): + handle_text(prompt_keyparser, Qt.Key_X) + prompt_keyparser.execute.assert_called_once_with( + 'message-info a', None) def test_mapping_keypad(self, config_stub, fake_keyevent, keyparser): """Make sure falling back to non-numpad keys works with mappings.""" config_stub.val.bindings.commands = {'normal': {'a': 'nop'}} config_stub.val.bindings.key_mappings = {'1': 'a'} - keyparser._read_config('normal') keyparser.handle(fake_keyevent(Qt.Key_1, Qt.KeypadModifier)) keyparser.execute.assert_called_once_with('nop', None) - def test_binding_and_mapping(self, config_stub, handle_text, keyparser): + def test_binding_and_mapping(self, config_stub, handle_text, prompt_keyparser): """with a conflicting binding/mapping, the binding should win.""" - handle_text(Qt.Key_B) - assert not keyparser.execute.called + handle_text(prompt_keyparser, Qt.Key_B) + assert not prompt_keyparser.execute.called def test_mapping_in_key_chain(self, config_stub, handle_text, keyparser): """A mapping should work even as part of a keychain.""" config_stub.val.bindings.commands = {'normal': {'aa': 'message-info aa'}} - keyparser._read_config('normal') - handle_text(Qt.Key_A, Qt.Key_X) + handle_text(keyparser, Qt.Key_A, Qt.Key_X) keyparser.execute.assert_called_once_with('message-info aa', None) - def test_binding_with_shift(self, keyparser, fake_keyevent): + def test_binding_with_shift(self, prompt_keyparser, fake_keyevent): """Simulate a binding which involves shift.""" for key, modifiers in [(Qt.Key_Y, Qt.NoModifier), (Qt.Key_Shift, Qt.ShiftModifier), (Qt.Key_Y, Qt.ShiftModifier)]: - keyparser.handle(fake_keyevent(key, modifiers)) + prompt_keyparser.handle(fake_keyevent(key, modifiers)) - keyparser.execute.assert_called_once_with('yank -s', None) + prompt_keyparser.execute.assert_called_once_with('yank -s', None) def test_partial_before_full_match(self, keyparser, fake_keyevent, config_stub): @@ -278,7 +243,6 @@ class TestHandle: 'a': 'message-info foo' } } - keyparser._read_config('normal') keyparser.handle(fake_keyevent(Qt.Key_A)) keyparser.execute.assert_called_once_with('message-info foo', None) @@ -287,59 +251,61 @@ class TestCount: """Test execute() with counts.""" - @pytest.fixture(autouse=True) - def read_keyparser_config(self, keyinput_bindings, keyparser): - keyparser._read_config('prompt') - - def test_no_count(self, handle_text, keyparser): + def test_no_count(self, handle_text, prompt_keyparser): """Test with no count added.""" - handle_text(Qt.Key_B, Qt.Key_A) - keyparser.execute.assert_called_once_with('message-info ba', None) - assert not keyparser._sequence + handle_text(prompt_keyparser, Qt.Key_B, Qt.Key_A) + prompt_keyparser.execute.assert_called_once_with( + 'message-info ba', None) + assert not prompt_keyparser._sequence - def test_count_0(self, handle_text, keyparser): - handle_text(Qt.Key_0, Qt.Key_B, Qt.Key_A) + def test_count_0(self, handle_text, prompt_keyparser): + handle_text(prompt_keyparser, Qt.Key_0, Qt.Key_B, Qt.Key_A) calls = [mock.call('message-info 0', None), mock.call('message-info ba', None)] - keyparser.execute.assert_has_calls(calls) - assert not keyparser._sequence + prompt_keyparser.execute.assert_has_calls(calls) + assert not prompt_keyparser._sequence - def test_count_42(self, handle_text, keyparser): - handle_text(Qt.Key_4, Qt.Key_2, Qt.Key_B, Qt.Key_A) - keyparser.execute.assert_called_once_with('message-info ba', 42) - assert not keyparser._sequence + def test_count_42(self, handle_text, prompt_keyparser): + handle_text(prompt_keyparser, Qt.Key_4, Qt.Key_2, Qt.Key_B, Qt.Key_A) + prompt_keyparser.execute.assert_called_once_with('message-info ba', 42) + assert not prompt_keyparser._sequence - def test_count_42_invalid(self, handle_text, keyparser): + def test_count_42_invalid(self, handle_text, prompt_keyparser): # Invalid call with ccx gets ignored - handle_text(Qt.Key_4, Qt.Key_2, Qt.Key_C, Qt.Key_C, Qt.Key_X) - assert not keyparser.execute.called - assert not keyparser._sequence + handle_text(prompt_keyparser, + Qt.Key_4, Qt.Key_2, Qt.Key_C, Qt.Key_C, Qt.Key_X) + assert not prompt_keyparser.execute.called + assert not prompt_keyparser._sequence # Valid call with ccc gets the correct count - handle_text(Qt.Key_2, Qt.Key_3, Qt.Key_C, Qt.Key_C, Qt.Key_C) - keyparser.execute.assert_called_once_with('message-info ccc', 23) - assert not keyparser._sequence + handle_text(prompt_keyparser, + Qt.Key_2, Qt.Key_3, Qt.Key_C, Qt.Key_C, Qt.Key_C) + prompt_keyparser.execute.assert_called_once_with( + 'message-info ccc', 23) + assert not prompt_keyparser._sequence - def test_superscript(self, handle_text, keyparser): + def test_superscript(self, handle_text, prompt_keyparser): # https://github.com/qutebrowser/qutebrowser/issues/3743 - handle_text(Qt.Key_twosuperior, Qt.Key_B, Qt.Key_A) + handle_text(prompt_keyparser, Qt.Key_twosuperior, Qt.Key_B, Qt.Key_A) - def test_count_keystring_update(self, qtbot, handle_text, keyparser): + def test_count_keystring_update(self, qtbot, + handle_text, prompt_keyparser): """Make sure the keystring is updated correctly when entering count.""" - with qtbot.waitSignals([keyparser.keystring_updated, - keyparser.keystring_updated]) as blocker: - handle_text(Qt.Key_4, Qt.Key_2) + with qtbot.waitSignals([ + prompt_keyparser.keystring_updated, + prompt_keyparser.keystring_updated]) as blocker: + handle_text(prompt_keyparser, Qt.Key_4, Qt.Key_2) sig1, sig2 = blocker.all_signals_and_args assert sig1.args == ('4',) assert sig2.args == ('42',) - def test_numpad(self, fake_keyevent, keyparser): + def test_numpad(self, fake_keyevent, prompt_keyparser): """Make sure we can enter a count via numpad.""" for key, modifiers in [(Qt.Key_4, Qt.KeypadModifier), (Qt.Key_2, Qt.KeypadModifier), (Qt.Key_B, Qt.NoModifier), (Qt.Key_A, Qt.NoModifier)]: - keyparser.handle(fake_keyevent(key, modifiers)) - keyparser.execute.assert_called_once_with('message-info ba', 42) + prompt_keyparser.handle(fake_keyevent(key, modifiers)) + prompt_keyparser.execute.assert_called_once_with('message-info ba', 42) def test_clear_keystring(qtbot, keyparser): From e9a23498b25ed213afda6e787743327a4573ee60 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Jun 2020 13:51:43 +0200 Subject: [PATCH 233/245] basekeyparser: Improve debug logging --- qutebrowser/keyinput/basekeyparser.py | 4 +++- tests/end2end/features/hints.feature | 4 ++-- tests/unit/keyinput/test_basekeyparser.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index cc7fdc5be..d77a4d3c6 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -195,7 +195,9 @@ class BaseKeyParser(QObject): message: The message to log. """ if self._do_log: - log.keyboard.debug(message) + prefix = '{} for mode {}: '.format(self.__class__.__name__, + self._mode.name) + log.keyboard.debug(prefix + message) def _match_key(self, sequence: keyutils.KeySequence) -> MatchResult: """Try to match a given keystring with any bound keychain. diff --git a/tests/end2end/features/hints.feature b/tests/end2end/features/hints.feature index 190e95f79..d0563a77b 100644 --- a/tests/end2end/features/hints.feature +++ b/tests/end2end/features/hints.feature @@ -323,8 +323,8 @@ Feature: Using hints And I wait until data/hello.txt is loaded And I press the key "," # Waiting here so we don't affect the next test - And I wait for "Releasing inhibition state of normal mode." in the log - Then "Ignoring key ',', because the normal mode is currently inhibited." should be logged + And I wait for "NormalKeyParser for mode normal: Releasing inhibition state of normal mode." in the log + Then "NormalKeyParser for mode normal: Ignoring key ',', because the normal mode is currently inhibited." should be logged Scenario: Turning off auto_follow_timeout When I set hints.auto_follow_timeout to 0 diff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py index 2a53841fe..492206c2d 100644 --- a/tests/unit/keyinput/test_basekeyparser.py +++ b/tests/unit/keyinput/test_basekeyparser.py @@ -64,7 +64,7 @@ class TestDebugLog: def test_log(self, keyparser, caplog): keyparser._debug_log('foo') - assert caplog.messages == ['foo'] + assert caplog.messages == ['BaseKeyParser for mode normal: foo'] def test_no_log(self, keyparser, caplog): keyparser._do_log = False From b25c7d5b36a239f8520670390692c96a2812d300 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Jun 2020 13:52:25 +0200 Subject: [PATCH 234/245] modeparsers: Refactor how hints are handled Instead of binding hints to fake :follow-hint commands, we now use a separate CommandKeyParser and ask that for its match result. If the key matches with the command parser, it is bound in hint mode, so we clear the hint keystring and defer to the command handling instead. If it doesn't, we continue hint handling as usual - however, the HintKeyParser is now not a CommandKeyParser anymore, so we don't have to deal with command parsing (and have a custom execute implementation instead). Closes #4504 Fixes #4392 Fixes #4368 Helps with #5084, though it doesn't completely fix that yet. Supersedes #3742 (fix for #3735) Supersedes #4691 (fix for #4264) --- qutebrowser/keyinput/keyutils.py | 15 ------ qutebrowser/keyinput/modeparsers.py | 29 ++++++++---- tests/helpers/stubs.py | 3 ++ tests/unit/keyinput/test_keyutils.py | 22 --------- tests/unit/keyinput/test_modeparsers.py | 62 ++++++++++++++++++++++--- 5 files changed, 78 insertions(+), 53 deletions(-) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 142bedd2f..b95f4a55d 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -180,21 +180,6 @@ def _is_printable(key: Qt.Key) -> bool: return key <= 0xff and key not in [Qt.Key_Space, _NIL_KEY] -def is_special_hint_mode(key: Qt.Key, modifiers: _ModifierType) -> bool: - """Check whether this key should clear the keychain in hint mode. - - When we press "s", we don't want to be handled as part of - a key chain in hint mode. - """ - _assert_plain_key(key) - _assert_plain_modifier(modifiers) - if is_modifier_key(key): - return False - return not (_is_printable(key) and - modifiers in [Qt.ShiftModifier, Qt.NoModifier, - Qt.KeypadModifier]) - - def is_special(key: Qt.Key, modifiers: _ModifierType) -> bool: """Check whether this key requires special key syntax.""" _assert_plain_key(key) diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index e848250c0..a55639898 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -136,7 +136,7 @@ class NormalKeyParser(CommandKeyParser): self._inhibited = False -class HintKeyParser(CommandKeyParser): +class HintKeyParser(basekeyparser.BaseKeyParser): """KeyChainParser for hints. @@ -151,8 +151,12 @@ class HintKeyParser(CommandKeyParser): hintmanager: hints.HintManager, parent: QObject = None) -> None: super().__init__(mode=usertypes.KeyMode.hint, win_id=win_id, - commandrunner=commandrunner, parent=parent, - supports_count=False) + parent=parent, supports_count=False) + self._command_parser = CommandKeyParser(mode=usertypes.KeyMode.hint, + win_id=win_id, + commandrunner=commandrunner, + parent=self, + supports_count=False) self._hintmanager = hintmanager self._filtertext = '' self._last_press = LastPress.none @@ -198,11 +202,14 @@ class HintKeyParser(CommandKeyParser): if dry_run: return super().handle(e, dry_run=True) - if keyutils.is_special_hint_mode(Qt.Key(e.key()), e.modifiers()): - log.keyboard.debug("Got special key, clearing keychain") - self.clear_keystring() - assert not dry_run + + if (self._command_parser.handle(e, dry_run=True) != + QKeySequence.NoMatch): + log.keyboard.debug("Handling key via command parser") + self.clear_keystring() + return self._command_parser.handle(e) + match = super().handle(e) if match == QKeySequence.PartialMatch: @@ -227,11 +234,15 @@ class HintKeyParser(CommandKeyParser): `self._filtertext`. """ self._read_config() - self.bindings.update({keyutils.KeySequence.parse(s): - 'follow-hint -s ' + s for s in strings}) + self.bindings.update({keyutils.KeySequence.parse(s): s + for s in strings}) if not preserve_filter: self._filtertext = '' + def execute(self, cmdstr: str, count: int = None) -> None: + assert count is None + self._hintmanager.handle_partial_key(cmdstr) + class RegisterKeyParser(CommandKeyParser): diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index 23fe7ac1d..bc8044461 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -657,6 +657,9 @@ class FakeHintManager: def handle_partial_key(self, keystr): self.keystr = keystr + def current_mode(self): + return 'letter' + class FakeWebEngineProfile: diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index a8783f772..689b6e5e8 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -579,28 +579,6 @@ def test_is_printable(key, printable): assert keyutils.is_special(key, Qt.NoModifier) != printable -@pytest.mark.parametrize('key, modifiers, special', [ - (Qt.Key_Escape, Qt.NoModifier, True), - (Qt.Key_Escape, Qt.ShiftModifier, True), - (Qt.Key_Escape, Qt.ControlModifier, True), - (Qt.Key_X, Qt.ControlModifier, True), - (Qt.Key_X, Qt.NoModifier, False), - (Qt.Key_2, Qt.NoModifier, False), - - # Keypad should not reset hint keychain - see #3735 - (Qt.Key_2, Qt.KeypadModifier, False), - - # Modifiers should not reset hint keychain - see #4264 - (Qt.Key_Shift, Qt.ShiftModifier, False), - (Qt.Key_Control, Qt.ControlModifier, False), - (Qt.Key_Alt, Qt.AltModifier, False), - (Qt.Key_Meta, Qt.MetaModifier, False), - (Qt.Key_Mode_switch, Qt.GroupSwitchModifier, False), -]) -def test_is_special_hint_mode(key, modifiers, special): - assert keyutils.is_special_hint_mode(key, modifiers) == special - - @pytest.mark.parametrize('key, modifiers, special', [ (Qt.Key_Escape, Qt.NoModifier, True), (Qt.Key_Escape, Qt.ShiftModifier, True), diff --git a/tests/unit/keyinput/test_modeparsers.py b/tests/unit/keyinput/test_modeparsers.py index ebaafa076..3c4c190ce 100644 --- a/tests/unit/keyinput/test_modeparsers.py +++ b/tests/unit/keyinput/test_modeparsers.py @@ -95,24 +95,38 @@ class TestHintKeyParser: hintmanager=hintmanager, commandrunner=commandrunner) - @pytest.mark.parametrize('bindings, event1, event2, prefix, command', [ + @pytest.mark.parametrize('bindings, event1, event2, prefix, hint', [ ( ['aa', 'as'], [Qt.Key_A], [Qt.Key_S], 'a', - 'follow-hint -s as' + 'as' ), ( ['21', '22'], [Qt.Key_2, Qt.KeypadModifier], [Qt.Key_2, Qt.KeypadModifier], '2', - 'follow-hint -s 22' + '22' + ), + ( + ['äa', 'äs'], + [QKeySequence('ä')[0]], + [Qt.Key_S], + 'ä', + 'äs' + ), + ( + ['не', 'на'], + [QKeySequence('н')[0]], + [QKeySequence('е')[0]], + '<Н>', + 'не', ), ]) - def test_match(self, keyparser, fake_keyevent, commandrunner, hintmanager, - bindings, event1, event2, prefix, command): + def test_match(self, keyparser, fake_keyevent, hintmanager, + bindings, event1, event2, prefix, hint): keyparser.update_bindings(bindings) match = keyparser.handle(fake_keyevent(*event1)) @@ -121,6 +135,40 @@ class TestHintKeyParser: match = keyparser.handle(fake_keyevent(*event2)) assert match == QKeySequence.ExactMatch - assert not hintmanager.keystr + assert hintmanager.keystr == hint - assert commandrunner.commands == [(command, None)] + def test_match_key_mappings(self, config_stub, keyparser, fake_keyevent, + hintmanager): + config_stub.val.bindings.key_mappings = {'α': 'a', 'σ': 's'} + keyparser.update_bindings(['aa', 'as']) + + match = keyparser.handle(fake_keyevent(QKeySequence('α')[0])) + assert match == QKeySequence.PartialMatch + assert hintmanager.keystr == 'a' + + match = keyparser.handle(fake_keyevent(QKeySequence('σ')[0])) + assert match == QKeySequence.ExactMatch + assert hintmanager.keystr == 'as' + + def test_command(self, keyparser, config_stub, fake_keyevent, hintmanager, + commandrunner): + config_stub.val.bindings.commands = { + 'hint': {'abc': 'message-info abc'} + } + + keyparser.update_bindings(['xabcy']) + + steps = [ + (Qt.Key_X, QKeySequence.PartialMatch, 'x'), + (Qt.Key_A, QKeySequence.PartialMatch, ''), + (Qt.Key_B, QKeySequence.PartialMatch, ''), + (Qt.Key_C, QKeySequence.ExactMatch, ''), + ] + for key, expected_match, keystr in steps: + match = keyparser.handle(fake_keyevent(key)) + assert match == expected_match + assert hintmanager.keystr == keystr + if key != Qt.Key_C: + assert not commandrunner.commands + + assert commandrunner.commands == [('message-info abc', None)] From bc88e673952ec1ce6485975183280d1deb21b809 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Jun 2020 14:57:42 +0200 Subject: [PATCH 235/245] Add BindingTrie.__str__ --- qutebrowser/keyinput/basekeyparser.py | 16 ++++++++++++ tests/unit/keyinput/test_bindingtrie.py | 34 +++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index d77a4d3c6..d46499a16 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -96,6 +96,22 @@ class BindingTrie: return utils.get_repr(self, children=self.children, command=self.command) + def __str__(self): + return '\n'.join(self.string_lines(blank=True)) + + def string_lines(self, indent=0, blank=False): + lines = [] + if self.command is not None: + lines.append('{}=> {}'.format(' ' * indent, self.command)) + + for key, child in sorted(self.children.items()): + lines.append('{}{}:'.format(' ' * indent, key)) + lines.extend(child.string_lines(indent=indent+1)) + if blank: + lines.append('') + + return lines + def update(self, mapping: typing.Mapping) -> None: """Add data from the given mapping to the trie.""" for key in mapping: diff --git a/tests/unit/keyinput/test_bindingtrie.py b/tests/unit/keyinput/test_bindingtrie.py index d7b3e4729..9a2ef10b9 100644 --- a/tests/unit/keyinput/test_bindingtrie.py +++ b/tests/unit/keyinput/test_bindingtrie.py @@ -21,6 +21,7 @@ import string import itertools +import textwrap import pytest @@ -45,6 +46,39 @@ def test_matches_single(entered, configured, match_type): assert trie.matches(entered) == result +def test_str(): + bindings = { + keyutils.KeySequence.parse('a'): 'cmd-a', + keyutils.KeySequence.parse('ba'): 'cmd-ba', + keyutils.KeySequence.parse('bb'): 'cmd-bb', + keyutils.KeySequence.parse('cax'): 'cmd-cax', + keyutils.KeySequence.parse('cby'): 'cmd-cby', + } + trie = basekeyparser.BindingTrie() + trie.update(bindings) + + expected = """ + a: + => cmd-a + + b: + a: + => cmd-ba + b: + => cmd-bb + + c: + a: + x: + => cmd-cax + b: + y: + => cmd-cby + """ + + assert str(trie) == textwrap.dedent(expected).lstrip('\n') + + @pytest.mark.parametrize('configured, expected', [ ([], # null match From bbaa67f9a28b01833e27038850ac201548a49eb5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Jun 2020 15:33:39 +0200 Subject: [PATCH 236/245] Update changelog --- doc/changelog.asciidoc | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 420062f43..537c742d2 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -30,6 +30,8 @@ Removed Changed ~~~~~~~ +- New handling of bindings in hint mode which fixes various bugs and allows for + single-letter keybindings in hint mode. - The `tor_identity` userscript now takes the password via a `-p` flag and has a new `-c` flag to customize the Tor control port. - `:config-write-py` now adds a note about `config.py` files being targeted at @@ -77,7 +79,13 @@ Fixed - Crash in rare cases where QtWebKit/QtWebEngine imports fail in unexpected ways. - `:config-write-py` now works with paths starting with `~/...` again. -- New site-specific quirk for a missing `globalThis` in Qt <= 5.12 on Reddit and Spotify +- New site-specific quirk for a missing `globalThis` in Qt <= 5.12 on Reddit + and Spotify. +- When `;` is added to `hints.chars`, using hint labels containing `;;` now + works properly. +- Hint letters outside of ASCII should now work. +- When `bindings.key_mappings` is used with hints, it now works properly with + letters outside of ASCII as well. v1.12.0 (2020-06-01) -------------------- From d6eebbd3f91f73aae660a553633999ed82993096 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Jun 2020 16:00:23 +0200 Subject: [PATCH 237/245] tests: Get rid of fake_keyevent Use keyutils.KeyInfo(...).to_event() instead. --- tests/unit/keyinput/conftest.py | 11 --- tests/unit/keyinput/test_basekeyparser.py | 87 ++++++++++++++--------- tests/unit/keyinput/test_keyutils.py | 6 +- tests/unit/keyinput/test_modeman.py | 5 +- tests/unit/keyinput/test_modeparsers.py | 53 +++++++------- 5 files changed, 88 insertions(+), 74 deletions(-) diff --git a/tests/unit/keyinput/conftest.py b/tests/unit/keyinput/conftest.py index 62ffa07cf..cadc76d8c 100644 --- a/tests/unit/keyinput/conftest.py +++ b/tests/unit/keyinput/conftest.py @@ -50,14 +50,3 @@ def keyinput_bindings(config_stub, key_config_stub): config_stub.val.bindings.default = {} config_stub.val.bindings.commands = dict(BINDINGS) config_stub.val.bindings.key_mappings = dict(MAPPINGS) - - -@pytest.fixture -def fake_keyevent(): - """Fixture that when called will return a mock instance of a QKeyEvent.""" - def func(key, modifiers=Qt.NoModifier, typ=QEvent.KeyPress): - """Generate a new fake QKeyPressEvent.""" - text = keyutils.KeyInfo(key, modifiers).text() - return QKeyEvent(QKeyEvent.KeyPress, key, modifiers, text) - - return func diff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py index 492206c2d..f8caaf1af 100644 --- a/tests/unit/keyinput/test_basekeyparser.py +++ b/tests/unit/keyinput/test_basekeyparser.py @@ -50,11 +50,12 @@ def prompt_keyparser(key_config_stub, keyinput_bindings): @pytest.fixture -def handle_text(fake_keyevent): +def handle_text(): """Helper function to handle multiple fake keypresses.""" def func(kp, *args): - for enumval in args: - kp.handle(fake_keyevent(enumval)) + for key in args: + info = keyutils.KeyInfo(key, Qt.NoModifier) + kp.handle(info.to_event()) return func @@ -117,18 +118,29 @@ def test_read_config(keyparser, key_config_stub, changed_mode, expected): class TestHandle: - def test_valid_key(self, fake_keyevent, prompt_keyparser): + def test_valid_key(self, prompt_keyparser, handle_text): modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier - prompt_keyparser.handle(fake_keyevent(Qt.Key_A, modifier)) - prompt_keyparser.handle(fake_keyevent(Qt.Key_X, modifier)) + + infos = [ + keyutils.KeyInfo(Qt.Key_A, modifier), + keyutils.KeyInfo(Qt.Key_X, modifier), + ] + for info in infos: + prompt_keyparser.handle(info.to_event()) + prompt_keyparser.execute.assert_called_once_with( 'message-info ctrla', None) assert not prompt_keyparser._sequence - def test_valid_key_count(self, fake_keyevent, prompt_keyparser): + def test_valid_key_count(self, prompt_keyparser): modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier - prompt_keyparser.handle(fake_keyevent(Qt.Key_5)) - prompt_keyparser.handle(fake_keyevent(Qt.Key_A, modifier)) + + infos = [ + keyutils.KeyInfo(Qt.Key_5, Qt.NoModifier), + keyutils.KeyInfo(Qt.Key_A, modifier), + ] + for info in infos: + prompt_keyparser.handle(info.to_event()) prompt_keyparser.execute.assert_called_once_with( 'message-info ctrla', 5) @@ -138,25 +150,33 @@ class TestHandle: # Only modifier [(Qt.Key_Shift, Qt.ShiftModifier)], ]) - def test_invalid_keys(self, fake_keyevent, prompt_keyparser, keys): + def test_invalid_keys(self, prompt_keyparser, keys): for key, modifiers in keys: - prompt_keyparser.handle(fake_keyevent(key, modifiers)) + info = keyutils.KeyInfo(key, modifiers) + prompt_keyparser.handle(info.to_event()) assert not prompt_keyparser.execute.called assert not prompt_keyparser._sequence - def test_dry_run(self, fake_keyevent, prompt_keyparser): - prompt_keyparser.handle(fake_keyevent(Qt.Key_B)) - prompt_keyparser.handle(fake_keyevent(Qt.Key_A), dry_run=True) + def test_dry_run(self, prompt_keyparser): + b_info = keyutils.KeyInfo(Qt.Key_B, Qt.NoModifier) + prompt_keyparser.handle(b_info.to_event()) + + a_info = keyutils.KeyInfo(Qt.Key_A, Qt.NoModifier) + prompt_keyparser.handle(a_info.to_event(), dry_run=True) + assert not prompt_keyparser.execute.called assert prompt_keyparser._sequence - def test_dry_run_count(self, fake_keyevent, prompt_keyparser): - prompt_keyparser.handle(fake_keyevent(Qt.Key_9), dry_run=True) + def test_dry_run_count(self, prompt_keyparser): + info = keyutils.KeyInfo(Qt.Key_9, Qt.NoModifier) + prompt_keyparser.handle(info.to_event(), dry_run=True) assert not prompt_keyparser._count - def test_invalid_key(self, fake_keyevent, prompt_keyparser): - prompt_keyparser.handle(fake_keyevent(Qt.Key_B)) - prompt_keyparser.handle(fake_keyevent(0x0)) + def test_invalid_key(self, prompt_keyparser): + keys = [Qt.Key_B, 0x0] + for key in keys: + info = keyutils.KeyInfo(key, Qt.NoModifier) + prompt_keyparser.handle(info.to_event()) assert not prompt_keyparser._sequence def test_valid_keychain(self, handle_text, prompt_keyparser): @@ -173,9 +193,9 @@ class TestHandle: (Qt.Key_1, Qt.NoModifier, 1), (Qt.Key_1, Qt.KeypadModifier, 1), ]) - def test_number_press(self, fake_keyevent, prompt_keyparser, + def test_number_press(self, prompt_keyparser, key, modifiers, number): - prompt_keyparser.handle(fake_keyevent(key, modifiers)) + prompt_keyparser.handle(keyutils.KeyInfo(key, modifiers).to_event()) command = 'message-info {}'.format(number) prompt_keyparser.execute.assert_called_once_with(command, None) assert not prompt_keyparser._sequence @@ -184,13 +204,13 @@ class TestHandle: (Qt.NoModifier, '2'), (Qt.KeypadModifier, 'num-2'), ]) - def test_number_press_keypad(self, fake_keyevent, keyparser, config_stub, + def test_number_press_keypad(self, keyparser, config_stub, modifiers, text): """Make sure a binding overrides the 2 binding.""" config_stub.val.bindings.commands = {'normal': { '2': 'message-info 2', '': 'message-info num-2'}} - keyparser.handle(fake_keyevent(Qt.Key_2, modifiers)) + keyparser.handle(keyutils.KeyInfo(Qt.Key_2, modifiers).to_event()) command = 'message-info {}'.format(text) keyparser.execute.assert_called_once_with(command, None) assert not keyparser._sequence @@ -205,12 +225,13 @@ class TestHandle: prompt_keyparser.execute.assert_called_once_with( 'message-info a', None) - def test_mapping_keypad(self, config_stub, fake_keyevent, keyparser): + def test_mapping_keypad(self, config_stub, keyparser): """Make sure falling back to non-numpad keys works with mappings.""" config_stub.val.bindings.commands = {'normal': {'a': 'nop'}} config_stub.val.bindings.key_mappings = {'1': 'a'} - keyparser.handle(fake_keyevent(Qt.Key_1, Qt.KeypadModifier)) + info = keyutils.KeyInfo(Qt.Key_1, Qt.KeypadModifier) + keyparser.handle(info.to_event()) keyparser.execute.assert_called_once_with('nop', None) def test_binding_and_mapping(self, config_stub, handle_text, prompt_keyparser): @@ -225,17 +246,17 @@ class TestHandle: handle_text(keyparser, Qt.Key_A, Qt.Key_X) keyparser.execute.assert_called_once_with('message-info aa', None) - def test_binding_with_shift(self, prompt_keyparser, fake_keyevent): + def test_binding_with_shift(self, prompt_keyparser): """Simulate a binding which involves shift.""" for key, modifiers in [(Qt.Key_Y, Qt.NoModifier), (Qt.Key_Shift, Qt.ShiftModifier), (Qt.Key_Y, Qt.ShiftModifier)]: - prompt_keyparser.handle(fake_keyevent(key, modifiers)) + info = keyutils.KeyInfo(key, modifiers) + prompt_keyparser.handle(info.to_event()) prompt_keyparser.execute.assert_called_once_with('yank -s', None) - def test_partial_before_full_match(self, keyparser, fake_keyevent, - config_stub): + def test_partial_before_full_match(self, keyparser, config_stub): """Make sure full matches always take precedence over partial ones.""" config_stub.val.bindings.commands = { 'normal': { @@ -243,7 +264,8 @@ class TestHandle: 'a': 'message-info foo' } } - keyparser.handle(fake_keyevent(Qt.Key_A)) + info = keyutils.KeyInfo(Qt.Key_A, Qt.NoModifier) + keyparser.handle(info.to_event()) keyparser.execute.assert_called_once_with('message-info foo', None) @@ -298,13 +320,14 @@ class TestCount: assert sig1.args == ('4',) assert sig2.args == ('42',) - def test_numpad(self, fake_keyevent, prompt_keyparser): + def test_numpad(self, prompt_keyparser): """Make sure we can enter a count via numpad.""" for key, modifiers in [(Qt.Key_4, Qt.KeypadModifier), (Qt.Key_2, Qt.KeypadModifier), (Qt.Key_B, Qt.NoModifier), (Qt.Key_A, Qt.NoModifier)]: - prompt_keyparser.handle(fake_keyevent(key, modifiers)) + info = keyutils.KeyInfo(key, modifiers) + prompt_keyparser.handle(info.to_event()) prompt_keyparser.execute.assert_called_once_with('message-info ba', 42) diff --git a/tests/unit/keyinput/test_keyutils.py b/tests/unit/keyinput/test_keyutils.py index 689b6e5e8..0df721c68 100644 --- a/tests/unit/keyinput/test_keyutils.py +++ b/tests/unit/keyinput/test_keyutils.py @@ -458,11 +458,11 @@ class TestKeySequence: Qt.ControlModifier | Qt.ShiftModifier), (Qt.ShiftModifier, Qt.ShiftModifier), ]) - def test_fake_mac(self, fake_keyevent, modifiers, expected): + def test_fake_mac(self, modifiers, expected): """Make sure Control/Meta are swapped with a simulated Mac.""" seq = keyutils.KeySequence() - event = fake_keyevent(key=Qt.Key_A, modifiers=modifiers) - new = seq.append_event(event) + info = keyutils.KeyInfo(key=Qt.Key_A, modifiers=modifiers) + new = seq.append_event(info.to_event()) assert new[0] == keyutils.KeyInfo(Qt.Key_A, expected) @pytest.mark.parametrize('key', [Qt.Key_unknown, 0x0]) diff --git a/tests/unit/keyinput/test_modeman.py b/tests/unit/keyinput/test_modeman.py index 89296f06b..2954d81ac 100644 --- a/tests/unit/keyinput/test_modeman.py +++ b/tests/unit/keyinput/test_modeman.py @@ -22,6 +22,7 @@ import pytest from PyQt5.QtCore import Qt, QObject, pyqtSignal from qutebrowser.utils import usertypes +from qutebrowser.keyinput import keyutils class FakeKeyparser(QObject): @@ -51,7 +52,7 @@ def modeman(mode_manager): (Qt.Key_A, Qt.ShiftModifier, True), (Qt.Key_A, Qt.ShiftModifier | Qt.ControlModifier, False), ]) -def test_non_alphanumeric(key, modifiers, filtered, fake_keyevent, modeman): +def test_non_alphanumeric(key, modifiers, filtered, modeman): """Make sure non-alphanumeric keys are passed through correctly.""" - evt = fake_keyevent(key=key, modifiers=modifiers) + evt = keyutils.KeyInfo(key=key, modifiers=modifiers).to_event() assert modeman.handle_event(evt) == filtered diff --git a/tests/unit/keyinput/test_modeparsers.py b/tests/unit/keyinput/test_modeparsers.py index 3c4c190ce..1f1bcfe11 100644 --- a/tests/unit/keyinput/test_modeparsers.py +++ b/tests/unit/keyinput/test_modeparsers.py @@ -46,18 +46,18 @@ class TestsNormalKeyParser: kp = modeparsers.NormalKeyParser(win_id=0, commandrunner=commandrunner) return kp - def test_keychain(self, keyparser, fake_keyevent, commandrunner): + def test_keychain(self, keyparser, commandrunner): """Test valid keychain.""" # Press 'z' which is ignored because of no match - keyparser.handle(fake_keyevent(Qt.Key_Z)) # Then start the real chain - keyparser.handle(fake_keyevent(Qt.Key_B)) - keyparser.handle(fake_keyevent(Qt.Key_A)) + chain = keyutils.KeySequence.parse('zba') + for info in chain: + keyparser.handle(info.to_event()) assert commandrunner.commands == [('message-info ba', None)] assert not keyparser._sequence def test_partial_keychain_timeout(self, keyparser, config_stub, - fake_keyevent, qtbot, commandrunner): + qtbot, commandrunner): """Test partial keychain timeout.""" config_stub.val.input.partial_timeout = 100 timer = keyparser._partial_timer @@ -65,7 +65,7 @@ class TestsNormalKeyParser: # Press 'b' for a partial match. # Then we check if the timer has been set up correctly - keyparser.handle(fake_keyevent(Qt.Key_B)) + keyparser.handle(keyutils.KeyInfo(Qt.Key_B, Qt.NoModifier).to_event()) assert timer.isSingleShot() assert timer.interval() == 100 assert timer.isActive() @@ -95,63 +95,63 @@ class TestHintKeyParser: hintmanager=hintmanager, commandrunner=commandrunner) - @pytest.mark.parametrize('bindings, event1, event2, prefix, hint', [ + @pytest.mark.parametrize('bindings, keychain, prefix, hint', [ ( ['aa', 'as'], - [Qt.Key_A], - [Qt.Key_S], + 'as', 'a', 'as' ), ( ['21', '22'], - [Qt.Key_2, Qt.KeypadModifier], - [Qt.Key_2, Qt.KeypadModifier], + '', '2', '22' ), ( ['äa', 'äs'], - [QKeySequence('ä')[0]], - [Qt.Key_S], + 'äs', 'ä', 'äs' ), ( ['не', 'на'], - [QKeySequence('н')[0]], - [QKeySequence('е')[0]], + 'не', '<Н>', 'не', ), ]) - def test_match(self, keyparser, fake_keyevent, hintmanager, - bindings, event1, event2, prefix, hint): + def test_match(self, keyparser, hintmanager, + bindings, keychain, prefix, hint): keyparser.update_bindings(bindings) - match = keyparser.handle(fake_keyevent(*event1)) + seq = keyutils.KeySequence.parse(keychain) + assert len(seq) == 2 + + match = keyparser.handle(seq[0].to_event()) assert match == QKeySequence.PartialMatch assert hintmanager.keystr == prefix - match = keyparser.handle(fake_keyevent(*event2)) + match = keyparser.handle(seq[1].to_event()) assert match == QKeySequence.ExactMatch assert hintmanager.keystr == hint - def test_match_key_mappings(self, config_stub, keyparser, fake_keyevent, - hintmanager): + def test_match_key_mappings(self, config_stub, keyparser, hintmanager): config_stub.val.bindings.key_mappings = {'α': 'a', 'σ': 's'} keyparser.update_bindings(['aa', 'as']) - match = keyparser.handle(fake_keyevent(QKeySequence('α')[0])) + seq = keyutils.KeySequence.parse('ασ') + assert len(seq) == 2 + + match = keyparser.handle(seq[0].to_event()) assert match == QKeySequence.PartialMatch assert hintmanager.keystr == 'a' - match = keyparser.handle(fake_keyevent(QKeySequence('σ')[0])) + match = keyparser.handle(seq[1].to_event()) assert match == QKeySequence.ExactMatch assert hintmanager.keystr == 'as' - def test_command(self, keyparser, config_stub, fake_keyevent, hintmanager, - commandrunner): + def test_command(self, keyparser, config_stub, hintmanager, commandrunner): config_stub.val.bindings.commands = { 'hint': {'abc': 'message-info abc'} } @@ -165,7 +165,8 @@ class TestHintKeyParser: (Qt.Key_C, QKeySequence.ExactMatch, ''), ] for key, expected_match, keystr in steps: - match = keyparser.handle(fake_keyevent(key)) + info = keyutils.KeyInfo(key, Qt.NoModifier) + match = keyparser.handle(info.to_event()) assert match == expected_match assert hintmanager.keystr == keystr if key != Qt.Key_C: From 4d4954173787d8ea8152c243a3d6a03b5022a12f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Jun 2020 16:14:09 +0200 Subject: [PATCH 238/245] Show partial keystrings for all modes in statusbar Closes #2817 --- doc/changelog.asciidoc | 1 + qutebrowser/keyinput/modeman.py | 7 +++++++ qutebrowser/mainwindow/mainwindow.py | 9 +++------ qutebrowser/mainwindow/statusbar/keystring.py | 7 +++++++ qutebrowser/misc/keyhintwidget.py | 6 +++--- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 537c742d2..a90d46954 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -54,6 +54,7 @@ Changed former distinction was mainly an implementation detail, and the accepted values shouldn't have changed. * `input.rocker_gestures` has been renamed to `input.mouse.rocker_gestures`. +- The statusbar now shows partial keychains in all modes (e.g. while hinting) - Small performance improvements. Added diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index 2ec956422..74ab8a27c 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -243,10 +243,15 @@ class ModeManager(QObject): arg1: The mode which has been left. arg2: The new current mode. arg3: The window ID of this mode manager. + keystring_updated: Emitted when the keystring was updated in any mode. + arg 1: The mode in which the keystring has been + updated. + arg 2: The new key string. """ entered = pyqtSignal(usertypes.KeyMode, int) left = pyqtSignal(usertypes.KeyMode, usertypes.KeyMode, int) + keystring_updated = pyqtSignal(usertypes.KeyMode, str) def __init__(self, win_id: int, parent: QObject = None) -> None: super().__init__(parent) @@ -332,6 +337,8 @@ class ModeManager(QObject): assert parser is not None self.parsers[mode] = parser parser.request_leave.connect(self.leave) + parser.keystring_updated.connect( + functools.partial(self.keystring_updated.emit, mode)) def enter(self, mode: usertypes.KeyMode, reason: str = None, diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 41f97868c..cf77866f2 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -505,9 +505,8 @@ class MainWindow(QWidget): message.global_bridge.mode_left) # type: ignore[arg-type] # commands - normal_parser = mode_manager.parsers[usertypes.KeyMode.normal] - normal_parser.keystring_updated.connect( - self.status.keystring.setText) + mode_manager.keystring_updated.connect( + self.status.keystring.on_keystring_updated) self.status.cmd.got_cmd[str].connect( # type: ignore[index] self._commandrunner.run_safely) self.status.cmd.got_cmd[str, int].connect( # type: ignore[index] @@ -518,9 +517,7 @@ class MainWindow(QWidget): self._command_dispatcher.search) # key hint popup - for mode, parser in mode_manager.parsers.items(): - parser.keystring_updated.connect(functools.partial( - self._keyhint.update_keyhint, mode.name)) + mode_manager.keystring_updated.connect(self._keyhint.update_keyhint) # messages message.global_bridge.show_message.connect( diff --git a/qutebrowser/mainwindow/statusbar/keystring.py b/qutebrowser/mainwindow/statusbar/keystring.py index a3b64892f..a64c8e0e2 100644 --- a/qutebrowser/mainwindow/statusbar/keystring.py +++ b/qutebrowser/mainwindow/statusbar/keystring.py @@ -19,9 +19,16 @@ """Keychain string displayed in the statusbar.""" +from PyQt5.QtCore import pyqtSlot + from qutebrowser.mainwindow.statusbar import textbase +from qutebrowser.utils import usertypes class KeyString(textbase.TextBase): """Keychain string displayed in the statusbar.""" + + @pyqtSlot(usertypes.KeyMode, str) + def on_keystring_updated(self, _mode, keystr): + self.setText(keystr) diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py index 89dac83f2..11bb14d66 100644 --- a/qutebrowser/misc/keyhintwidget.py +++ b/qutebrowser/misc/keyhintwidget.py @@ -82,8 +82,8 @@ class KeyHintView(QLabel): self.update_geometry.emit() super().showEvent(e) - @pyqtSlot(str) - def update_keyhint(self, modename, prefix): + @pyqtSlot(usertypes.KeyMode, str) + def update_keyhint(self, mode, prefix): """Show hints for the given prefix (or hide if prefix is empty). Args: @@ -108,7 +108,7 @@ class KeyHintView(QLabel): cmd = objects.commands.get(cmdname) return cmd and cmd.takes_count() - bindings_dict = config.key_instance.get_bindings_for(modename) + bindings_dict = config.key_instance.get_bindings_for(mode.name) bindings = [(k, v) for (k, v) in sorted(bindings_dict.items()) if keyutils.KeySequence.parse(prefix).matches(k) and not blacklisted(str(k)) and From 0ac43077358d30fe088fa6731ec2deb8ab31de3f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Jun 2020 16:38:32 +0200 Subject: [PATCH 239/245] Handle OSErrors gracefully in os.utime() For some reason, some systems remove the IPC socket of qutebrowser, causing it to die after 6h. Now we handle this case more gracefully and also make sure it still exists all 5 minutes. Fixes #996 (finally!), though I still have no idea *why* it is happening... --- doc/changelog.asciidoc | 2 ++ qutebrowser/misc/ipc.py | 12 ++++++++++-- tests/unit/misc/test_ipc.py | 20 +++++++++++++++++++- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index a90d46954..4ebf0493c 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -79,6 +79,8 @@ Fixed (which seems to happen with Qt 5.15 sometimes). - Crash in rare cases where QtWebKit/QtWebEngine imports fail in unexpected ways. +- Crash when something removed qutebrowser's IPC socket file and it's been + running for 6 hours. - `:config-write-py` now works with paths starting with `~/...` again. - New site-specific quirk for a missing `globalThis` in Qt <= 5.12 on Reddit and Spotify. diff --git a/qutebrowser/misc/ipc.py b/qutebrowser/misc/ipc.py index 2cceb2df1..1097859de 100644 --- a/qutebrowser/misc/ipc.py +++ b/qutebrowser/misc/ipc.py @@ -36,7 +36,7 @@ from qutebrowser.utils import log, usertypes, error, standarddir, utils CONNECT_TIMEOUT = 100 # timeout for connecting/disconnecting WRITE_TIMEOUT = 1000 READ_TIMEOUT = 5000 -ATIME_INTERVAL = 60 * 60 * 3 * 1000 # 3 hours +ATIME_INTERVAL = 5000 * 60 # 5 minutes PROTOCOL_VERSION = 1 @@ -397,8 +397,16 @@ class IPCServer(QObject): if not path: log.ipc.error("In update_atime with no server path!") return + log.ipc.debug("Touching {}".format(path)) - os.utime(path) + + try: + os.utime(path) + except OSError as e: + log.ipc.exception("Failed to update IPC socket, trying to " + "re-listen...") + self._server.close() + self.listen() @pyqtSlot() def shutdown(self): diff --git a/tests/unit/misc/test_ipc.py b/tests/unit/misc/test_ipc.py index a598043b0..434cbaf59 100644 --- a/tests/unit/misc/test_ipc.py +++ b/tests/unit/misc/test_ipc.py @@ -320,7 +320,7 @@ class TestListen: @pytest.mark.posix def test_atime_update(self, qtbot, ipc_server): - ipc_server._atime_timer.setInterval(500) # We don't want to wait 6h + ipc_server._atime_timer.setInterval(500) # We don't want to wait ipc_server.listen() old_atime = os.stat(ipc_server._server.fullServerName()).st_atime_ns @@ -348,6 +348,24 @@ class TestListen: ipc_server._atime_timer.timeout.disconnect(ipc_server.update_atime) ipc_server.shutdown() + @pytest.mark.posix + def test_vanished_runtime_file(self, qtbot, caplog, ipc_server): + ipc_server._atime_timer.setInterval(500) # We don't want to wait + ipc_server.listen() + + sockfile = pathlib.Path(ipc_server._server.fullServerName()) + sockfile.unlink() + + with caplog.at_level(logging.ERROR): + with qtbot.waitSignal(ipc_server._atime_timer.timeout, timeout=2000): + pass + + msg = 'Failed to update IPC socket, trying to re-listen...' + assert caplog.messages[-1] == msg + + assert ipc_server._server.isListening() + assert sockfile.exists() + class TestOnError: From 60318271035c53fa90d6c1c0de629e44f0b2b924 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Jun 2020 16:49:58 +0200 Subject: [PATCH 240/245] Fix lint --- qutebrowser/keyinput/basekeyparser.py | 6 ++++-- qutebrowser/misc/ipc.py | 2 +- tests/unit/keyinput/conftest.py | 5 ----- tests/unit/misc/test_ipc.py | 3 ++- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index d46499a16..dea85aede 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -96,10 +96,12 @@ class BindingTrie: return utils.get_repr(self, children=self.children, command=self.command) - def __str__(self): + def __str__(self) -> str: return '\n'.join(self.string_lines(blank=True)) - def string_lines(self, indent=0, blank=False): + def string_lines(self, indent: int = 0, + blank: bool = False) -> typing.Sequence[str]: + """Get a list of strings for a pretty-printed version of this trie.""" lines = [] if self.command is not None: lines.append('{}=> {}'.format(' ' * indent, self.command)) diff --git a/qutebrowser/misc/ipc.py b/qutebrowser/misc/ipc.py index 1097859de..c4cd4f792 100644 --- a/qutebrowser/misc/ipc.py +++ b/qutebrowser/misc/ipc.py @@ -402,7 +402,7 @@ class IPCServer(QObject): try: os.utime(path) - except OSError as e: + except OSError: log.ipc.exception("Failed to update IPC socket, trying to " "re-listen...") self._server.close() diff --git a/tests/unit/keyinput/conftest.py b/tests/unit/keyinput/conftest.py index cadc76d8c..ba7a19f79 100644 --- a/tests/unit/keyinput/conftest.py +++ b/tests/unit/keyinput/conftest.py @@ -21,11 +21,6 @@ import pytest -from PyQt5.QtCore import QEvent, Qt -from PyQt5.QtGui import QKeyEvent - -from qutebrowser.keyinput import keyutils - BINDINGS = {'prompt': {'': 'message-info ctrla', 'a': 'message-info a', diff --git a/tests/unit/misc/test_ipc.py b/tests/unit/misc/test_ipc.py index 434cbaf59..95858f837 100644 --- a/tests/unit/misc/test_ipc.py +++ b/tests/unit/misc/test_ipc.py @@ -357,7 +357,8 @@ class TestListen: sockfile.unlink() with caplog.at_level(logging.ERROR): - with qtbot.waitSignal(ipc_server._atime_timer.timeout, timeout=2000): + with qtbot.waitSignal(ipc_server._atime_timer.timeout, + timeout=2000): pass msg = 'Failed to update IPC socket, trying to re-listen...' From 5dcfd36d4186337a9c6b9061defc9bfe11df99bc Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Jun 2020 17:19:55 +0200 Subject: [PATCH 241/245] Add git branch to :version --- qutebrowser/utils/version.py | 24 ++++++++++++++---------- scripts/setupcommon.py | 22 +++++++++++++--------- tests/unit/utils/test_version.py | 2 +- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 75c6f6ede..78e51d8c1 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -161,6 +161,14 @@ def _git_str() -> typing.Optional[str]: return None +def _call_git(gitpath: str, *args: str): + """Call a git subprocess.""" + return subprocess.run( + ['git'] + list(args), + cwd=gitpath, check=True, + stdout=subprocess.PIPE).stdout.decode('UTF-8').strip() + + def _git_str_subprocess(gitpath: str) -> typing.Optional[str]: """Try to get the git commit ID and timestamp by calling git. @@ -174,16 +182,12 @@ def _git_str_subprocess(gitpath: str) -> typing.Optional[str]: return None try: # https://stackoverflow.com/questions/21017300/21017394#21017394 - commit_hash = subprocess.run( - ['git', 'describe', '--match=NeVeRmAtCh', '--always', '--dirty'], - cwd=gitpath, check=True, - stdout=subprocess.PIPE).stdout.decode('UTF-8').strip() - date = subprocess.run( - ['git', 'show', '-s', '--format=%ci', 'HEAD'], - cwd=gitpath, check=True, - stdout=subprocess.PIPE).stdout.decode('UTF-8').strip() - return '{} ({})'.format(commit_hash, date) - except (subprocess.CalledProcessError, OSError): + commit_hash = _call_git(gitpath, 'describe', '--match=NeVeRmAtCh', + '--always', '--dirty') + date = _call_git(gitpath, 'show', '-s', '--format=%ci', 'HEAD') + branch = _call_git(gitpath, 'rev-parse', '--abbrev-ref', 'HEAD') + return '{} on {} ({})'.format(commit_hash, branch, date) + except (subprocess.CalledProcessError, OSError) as e: return None diff --git a/scripts/setupcommon.py b/scripts/setupcommon.py index e07096546..54a162941 100644 --- a/scripts/setupcommon.py +++ b/scripts/setupcommon.py @@ -38,6 +38,14 @@ BASEDIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), os.path.pardir) +def _call_git(gitpath: str, *args: str): + """Call a git subprocess.""" + return subprocess.run( + ['git'] + list(args), + cwd=gitpath, check=True, + stdout=subprocess.PIPE).stdout.decode('UTF-8').strip() + + def _git_str(): """Try to find out git version. @@ -51,15 +59,11 @@ def _git_str(): return None try: # https://stackoverflow.com/questions/21017300/21017394#21017394 - commit_hash = subprocess.run( - ['git', 'describe', '--match=NeVeRmAtCh', '--always', '--dirty'], - cwd=BASEDIR, check=True, - stdout=subprocess.PIPE).stdout.decode('UTF-8').strip() - date = subprocess.run( - ['git', 'show', '-s', '--format=%ci', 'HEAD'], - cwd=BASEDIR, check=True, - stdout=subprocess.PIPE).stdout.decode('UTF-8').strip() - return '{} ({})'.format(commit_hash, date) + commit_hash = _call_git(BASEDIR, 'describe', '--match=NeVeRmAtCh', + '--always', '--dirty') + date = _call_git(BASEDIR, 'show', '-s', '--format=%ci', 'HEAD') + branch = _call_git(BASEDIR, 'rev-parse', '--abbrev-ref', 'HEAD') + return '{} on {} ({})'.format(commit_hash, branch, date) except (subprocess.CalledProcessError, OSError): return None diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py index f1e0ee0b6..8d3542cd7 100644 --- a/tests/unit/utils/test_version.py +++ b/tests/unit/utils/test_version.py @@ -403,7 +403,7 @@ class TestGitStrSubprocess: def test_real_git(self, git_repo): """Test with a real git repository.""" ret = version._git_str_subprocess(str(git_repo)) - assert ret == '6e4b65a (1970-01-01 01:00:00 +0100)' + assert ret == '6e4b65a on master (1970-01-01 01:00:00 +0100)' def test_missing_dir(self, tmpdir): """Test with a directory which doesn't exist.""" From 2e992d3a74c4942254610ded7ae7723e1afc7e95 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Jun 2020 17:35:04 +0200 Subject: [PATCH 242/245] Fix tests/lint --- qutebrowser/utils/version.py | 4 ++-- scripts/setupcommon.py | 2 +- tests/unit/keyinput/test_modeman.py | 1 + tests/unit/misc/test_keyhints.py | 19 ++++++++++--------- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 78e51d8c1..59ef04f1f 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -161,7 +161,7 @@ def _git_str() -> typing.Optional[str]: return None -def _call_git(gitpath: str, *args: str): +def _call_git(gitpath: str, *args: str) -> str: """Call a git subprocess.""" return subprocess.run( ['git'] + list(args), @@ -187,7 +187,7 @@ def _git_str_subprocess(gitpath: str) -> typing.Optional[str]: date = _call_git(gitpath, 'show', '-s', '--format=%ci', 'HEAD') branch = _call_git(gitpath, 'rev-parse', '--abbrev-ref', 'HEAD') return '{} on {} ({})'.format(commit_hash, branch, date) - except (subprocess.CalledProcessError, OSError) as e: + except (subprocess.CalledProcessError, OSError): return None diff --git a/scripts/setupcommon.py b/scripts/setupcommon.py index 54a162941..65e2a498a 100644 --- a/scripts/setupcommon.py +++ b/scripts/setupcommon.py @@ -38,7 +38,7 @@ BASEDIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), os.path.pardir) -def _call_git(gitpath: str, *args: str): +def _call_git(gitpath, *args): """Call a git subprocess.""" return subprocess.run( ['git'] + list(args), diff --git a/tests/unit/keyinput/test_modeman.py b/tests/unit/keyinput/test_modeman.py index 2954d81ac..b473294f8 100644 --- a/tests/unit/keyinput/test_modeman.py +++ b/tests/unit/keyinput/test_modeman.py @@ -29,6 +29,7 @@ class FakeKeyparser(QObject): """A fake BaseKeyParser which doesn't handle anything.""" + keystring_updated = pyqtSignal(str) request_leave = pyqtSignal(usertypes.KeyMode, str, bool) def __init__(self): diff --git a/tests/unit/misc/test_keyhints.py b/tests/unit/misc/test_keyhints.py index 2e9ea1aaf..a9f8ed311 100644 --- a/tests/unit/misc/test_keyhints.py +++ b/tests/unit/misc/test_keyhints.py @@ -21,6 +21,7 @@ import pytest +from qutebrowser.utils import usertypes from qutebrowser.misc import objects from qutebrowser.misc.keyhintwidget import KeyHintView @@ -57,7 +58,7 @@ def test_show_and_hide(qtbot, keyhint): with qtbot.waitSignal(keyhint.update_geometry): with qtbot.waitExposed(keyhint): keyhint.show() - keyhint.update_keyhint('normal', '') + keyhint.update_keyhint(usertypes.KeyMode.normal, '') assert not keyhint.isVisible() @@ -84,7 +85,7 @@ def test_suggestions(keyhint, config_stub): config_stub.val.bindings.default = default_bindings config_stub.val.bindings.commands = bindings - keyhint.update_keyhint('normal', 'a') + keyhint.update_keyhint(usertypes.KeyMode.normal, 'a') assert keyhint.text() == expected_text( ('a', 'yellow', 'a', 'message-info cmd-aa'), ('a', 'yellow', 'b', 'message-info cmd-ab'), @@ -109,7 +110,7 @@ def test_suggestions_special(keyhint, config_stub): config_stub.val.bindings.default = default_bindings config_stub.val.bindings.commands = bindings - keyhint.update_keyhint('normal', '') + keyhint.update_keyhint(usertypes.KeyMode.normal, '') assert keyhint.text() == expected_text( ('<Ctrl+c>', 'yellow', 'a', 'message-info cmd-Cca'), ('<Ctrl+c>', 'yellow', 'c', 'message-info cmd-Ccc'), @@ -130,7 +131,7 @@ def test_suggestions_with_count(keyhint, config_stub, monkeypatch, stubs): config_stub.val.bindings.default = bindings config_stub.val.bindings.commands = bindings - keyhint.update_keyhint('normal', '2a') + keyhint.update_keyhint(usertypes.KeyMode.normal, '2a') assert keyhint.text() == expected_text( ('a', 'yellow', 'b', 'bar'), ) @@ -146,7 +147,7 @@ def test_special_bindings(keyhint, config_stub): config_stub.val.bindings.default = {} config_stub.val.bindings.commands = bindings - keyhint.update_keyhint('normal', '<') + keyhint.update_keyhint(usertypes.KeyMode.normal, '<') assert keyhint.text() == expected_text( ('<', 'yellow', 'a', 'message-info cmd-<a'), @@ -159,7 +160,7 @@ def test_color_switch(keyhint, config_stub): config_stub.val.colors.keyhint.suffix.fg = '#ABCDEF' config_stub.val.bindings.default = {} config_stub.val.bindings.commands = bindings - keyhint.update_keyhint('normal', 'a') + keyhint.update_keyhint(usertypes.KeyMode.normal, 'a') assert keyhint.text() == expected_text(('a', '#ABCDEF', 'a', 'message-info cmd-aa')) @@ -173,7 +174,7 @@ def test_no_matches(keyhint, config_stub): config_stub.val.bindings.default = {} config_stub.val.bindings.commands = bindings - keyhint.update_keyhint('normal', 'z') + keyhint.update_keyhint(usertypes.KeyMode.normal, 'z') assert not keyhint.text() assert not keyhint.isVisible() @@ -196,7 +197,7 @@ def test_blacklist(keyhint, config_stub, blacklist, expected): config_stub.val.bindings.default = {} config_stub.val.bindings.commands = bindings - keyhint.update_keyhint('normal', 'a') + keyhint.update_keyhint(usertypes.KeyMode.normal, 'a') assert keyhint.text() == expected @@ -213,6 +214,6 @@ def test_delay(qtbot, stubs, monkeypatch, config_stub, key_config_stub): config_stub.val.bindings.commands = bindings keyhint = KeyHintView(0, None) - keyhint.update_keyhint('normal', 'a') + keyhint.update_keyhint(usertypes.KeyMode.normal, 'a') assert timer.isSingleShot() assert timer.interval() == interval From c0084edbc1df7d534a4346f12cff5312724e89b2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Jun 2020 11:08:05 +0200 Subject: [PATCH 243/245] Add workaround for muted indicator with Qt 5.15 See #5525 --- doc/changelog.asciidoc | 3 +++ qutebrowser/browser/webengine/webenginetab.py | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 4ebf0493c..14760d5d6 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -89,6 +89,9 @@ Fixed - Hint letters outside of ASCII should now work. - When `bindings.key_mappings` is used with hints, it now works properly with letters outside of ASCII as well. +- With Qt 5.15, the audible/muted indicators are not updated properly due to a + Qt bug. This release adds a workaround so that at least the muted indicator + is shown properly. v1.12.0 (2020-06-01) -------------------- diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 683b2c1fd..69d6daeb4 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -846,10 +846,15 @@ class WebEngineAudio(browsertab.AbstractAudio): config.instance.changed.connect(self._on_config_changed) def set_muted(self, muted: bool, override: bool = False) -> None: + was_muted = self.is_muted() self._overridden = override assert self._widget is not None page = self._widget.page() page.setAudioMuted(muted) + if was_muted != muted and qtutils.version_check('5.15'): + # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-85118 + # so that the tab title at least updates the muted indicator + self.muted_changed.emit(muted) def is_muted(self): page = self._widget.page() @@ -972,6 +977,7 @@ class _WebEnginePermissions(QObject): permission_str = debug.qenum_key(QWebEnginePage, feature) if not url.isValid(): + # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-85116 log.webview.warning("Ignoring feature permission {} for invalid " "URL {}".format(permission_str, url)) deny_permission() From 0b5f41ac8c91f55830b3a04ac50ca311533b048b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Jun 2020 13:35:24 +0200 Subject: [PATCH 244/245] Re-enable content.dns_prefetch by default This reverts aefdf7565a9b16ec29d3461e7afaadb4afe86f0a - see #4657 --- doc/changelog.asciidoc | 2 ++ doc/help/settings.asciidoc | 2 +- qutebrowser/config/configdata.yml | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 14760d5d6..635f601fb 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -54,6 +54,8 @@ Changed former distinction was mainly an implementation detail, and the accepted values shouldn't have changed. * `input.rocker_gestures` has been renamed to `input.mouse.rocker_gestures`. + * `content.dns_prefetch` is now enabled by default again, since the crashes + it caused are now fixed (Qt 5.15) or worked around. - The statusbar now shows partial keychains in all modes (e.g. while hinting) - Small performance improvements. diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 4d68cf3a7..e2f29cf6f 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -1998,7 +1998,7 @@ This setting supports URL patterns. Type: <> -Default: +pass:[false]+ +Default: +pass:[true]+ On QtWebEngine, this setting requires Qt 5.12 or newer. diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 67d2eef5f..abd3f9d2c 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -444,7 +444,7 @@ content.developer_extras: deleted: true content.dns_prefetch: - default: false + default: true type: Bool backend: QtWebKit: true From f0eed465ab608061212366c6ebc26a0c47803060 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Jun 2020 13:44:00 +0200 Subject: [PATCH 245/245] Work around segfaults when updating QWebEngineProfile HTTP headers With Qt versions between 5.12 and 5.14 (inclusive), calling setHttpUserAgent() or setHttpAcceptLanguage() can cause segfaults on subsequent loads: https://bugreports.qt.io/browse/QTBUG-75884 The easiest way to reproduce this is by using WhatsApp Web and reloading after setting the user-agent. While the bug should've been fixed in Qt 5.12.5, 5.13.1 and 5.14.0, that's not actually the case - only Qt 5.15 fixed it properly. Since we use a request interceptor to set the headers anyways, just don't call those problematic methods at all when updating settings at runtime. Fixes #4657 --- doc/changelog.asciidoc | 3 +++ doc/help/settings.asciidoc | 4 +++- qutebrowser/browser/webengine/webenginesettings.py | 7 ++++++- qutebrowser/config/configdata.yml | 4 +++- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 635f601fb..9596a41ad 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -94,6 +94,9 @@ Fixed - With Qt 5.15, the audible/muted indicators are not updated properly due to a Qt bug. This release adds a workaround so that at least the muted indicator is shown properly. +- As a workaround for crashes with QtWebEngine versions between 5.12 and 5.14 + (inclusive), changing the user agent (`content.headers.user_agent`) exposed + to JS now requires a restart. The corresponding HTTP header is not affected. v1.12.0 (2020-06-01) -------------------- diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index e2f29cf6f..8fde54613 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -2115,7 +2115,9 @@ The following placeholders are defined: The default value is equal to the unchanged user agent of QtWebKit/QtWebEngine. -Note that the value read from JavaScript is always the global value. +Note that the value read from JavaScript is always the global value. With +QtWebEngine between 5.12 and 5.14 (inclusive), changing the value exposed +to JavaScript requires a restart. This setting supports URL patterns. diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index c76a1c90a..b7e67e379 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -326,8 +326,13 @@ def _update_settings(option): """Update global settings when qwebsettings changed.""" global_settings.update_setting(option) + # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-75884 + # (note this isn't actually fixed properly before Qt 5.15) + header_bug_fixed = (not qtutils.version_check('5.12', compiled=False) or + qtutils.version_check('5.15', compiled=False)) + if option in ['content.headers.user_agent', - 'content.headers.accept_language']: + 'content.headers.accept_language'] and header_bug_fixed: default_profile.setter.set_http_headers() if private_profile: private_profile.setter.set_http_headers() diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index abd3f9d2c..b3ffa9c5a 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -585,7 +585,9 @@ content.headers.user_agent: The default value is equal to the unchanged user agent of QtWebKit/QtWebEngine. - Note that the value read from JavaScript is always the global value. + Note that the value read from JavaScript is always the global value. With + QtWebEngine between 5.12 and 5.14 (inclusive), changing the value exposed + to JavaScript requires a restart. content.host_blocking.enabled: default: true