diff --git a/.coveragerc b/.coveragerc index 18fb85ba4..9ba8e8a5e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -12,6 +12,7 @@ exclude_lines = def __repr__ raise AssertionError raise NotImplementedError + raise utils\.Unreachable if __name__ == ["']__main__["']: [xml] diff --git a/.flake8 b/.flake8 index 06883f526..340132d49 100644 --- a/.flake8 +++ b/.flake8 @@ -1,6 +1,7 @@ [flake8] exclude = .*,__pycache__,resources.py # B001: bare except +# B008: Do not perform calls in argument defaults. (fine with some Qt stuff) # B305: .next() (false-positives) # E128: continuation line under-indented for visual indent # E226: missing whitespace around arithmetic operator @@ -33,7 +34,7 @@ exclude = .*,__pycache__,resources.py # D413: Missing blank line after last section (not in pep257?) # A003: Builtin name for class attribute (needed for attrs) ignore = - B001,B305, + B001,B008,B305, E128,E226,E265,E501,E402,E266,E722,E731, F401, N802, diff --git a/.gitignore b/.gitignore index cb244557b..54a0dcae6 100644 --- a/.gitignore +++ b/.gitignore @@ -31,12 +31,12 @@ __pycache__ /prof /venv TODO -/scripts/testbrowser_cpp/webkit/Makefile -/scripts/testbrowser_cpp/webkit/main.o -/scripts/testbrowser_cpp/webkit/testbrowser -/scripts/testbrowser_cpp/webkit/.qmake.stash -/scripts/testbrowser_cpp/webengine/Makefile -/scripts/testbrowser_cpp/webengine/main.o -/scripts/testbrowser_cpp/webengine/testbrowser -/scripts/testbrowser_cpp/webengine/.qmake.stash +/scripts/testbrowser/cpp/webkit/Makefile +/scripts/testbrowser/cpp/webkit/main.o +/scripts/testbrowser/cpp/webkit/testbrowser +/scripts/testbrowser/cpp/webkit/.qmake.stash +/scripts/testbrowser/cpp/webengine/Makefile +/scripts/testbrowser/cpp/webengine/main.o +/scripts/testbrowser/cpp/webengine/testbrowser +/scripts/testbrowser/cpp/webengine/.qmake.stash /scripts/dev/pylint_checkers/qute_pylint.egg-info diff --git a/.pylintrc b/.pylintrc index bca11bf80..b654355c2 100644 --- a/.pylintrc +++ b/.pylintrc @@ -13,38 +13,33 @@ persistent=n [MESSAGES CONTROL] enable=all -disable=no-self-use, - fixme, - global-statement, - locally-disabled, +disable=locally-disabled, locally-enabled, - too-many-ancestors, - too-few-public-methods, - too-many-public-methods, + suppressed-message, + fixme, + no-self-use, cyclic-import, - bad-continuation, - too-many-instance-attributes, blacklisted-name, - too-many-lines, logging-format-interpolation, + logging-not-lazy, broad-except, bare-except, eval-used, exec-used, - ungrouped-imports, - suppressed-message, - too-many-return-statements, - duplicate-code, + global-statement, wrong-import-position, + duplicate-code, no-else-return, - # https://github.com/PyCQA/pylint/issues/1698 - unsupported-membership-test, - unsupported-assignment-operation, - unsubscriptable-object, + too-many-ancestors, + too-many-public-methods, + too-many-instance-attributes, + too-many-lines, + too-many-return-statements, too-many-boolean-expressions, too-many-locals, too-many-branches, - too-many-statements + too-many-statements, + too-few-public-methods [BASIC] function-rgx=[a-z_][a-z0-9_]{2,50}$ @@ -73,10 +68,10 @@ valid-metaclass-classmethod-first-arg=cls [TYPECHECK] ignored-modules=PyQt5,PyQt5.QtWebKit -ignored-classes=_CountingAttr [IMPORTS] # WORKAROUND # For some reason, pylint doesn't know about some Python 3 modules on # AppVeyor... known-standard-library=faulthandler,http,enum,tokenize,posixpath,importlib,types +known-third-party=sip diff --git a/.travis.yml b/.travis.yml index 65d917d73..251842d06 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ matrix: env: TESTENV=py36-pyqt59-cov - os: osx env: TESTENV=py36 OSX=sierra - osx_image: xcode8.3 + osx_image: xcode9.2 language: generic # https://github.com/qutebrowser/qutebrowser/issues/2013 # - os: osx @@ -52,6 +52,10 @@ matrix: language: node_js python: null node_js: "lts/*" + - os: linux + language: generic + env: TESTENV=shellcheck + services: docker fast_finish: true cache: diff --git a/MANIFEST.in b/MANIFEST.in index 54bb613f3..b20c2bc77 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -23,7 +23,7 @@ include qutebrowser/config/configdata.yml prune www prune scripts/dev -prune scripts/testbrowser_cpp +prune scripts/testbrowser/cpp prune .github exclude scripts/asciidoc2html.py exclude doc/notes diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 34ee4ff7c..08dc2fbce 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -15,57 +15,105 @@ 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.1.0 (unreleased) +v1.2.0 (unreleased) ------------------- -Added -~~~~~ - -- There's now a `misc/Makefile` file in releases, which should help - distributions which package qutebrowser, as they can run something like - `make -f misc/Makefile DESTDIR="$pkgdir" install` now. -- New `{current_url}` field for `window.title_format` and `tabs.title.format`. -- New `colors.statusbar.passthrough.fg`/`.bg` settings. -- New `completion.delay` and `completion.min_chars` settings to update the - completion less often. -- New `completion.use_best_match` setting to automatically use the best-matching - command in the completion. -- New `:tab-give` and `:tab-take` commands, to give tabs to another window, or - take them from another window. -- New `config.source(...)` method for `config.py` to source another file. -- New `keyhint.radius` option to configure the edge rounding for the key hint - widget. -- `:edit-url` now handles the `--private` and `--related` flags, which have the - same effect they have with `:open`. -- New `{line}` and `{column}` replacements for `editor.command` to position the - cursor correctly. -- New `qute-pass` userscript as alternative to `password_fill` which allows - selecting accounts via rofi or any other dmenu-compatile application. -- New `qt.highdpi` setting to turn on Qt's High-DPI scaling. -- New `:completion-item-yank` command (bound to ``) to yank the current - completion item text. -- New `tabs.pinned.shrink` setting to (`true` by default) to make it possible - for pinned tabs and normal tabs to have the same size. -- New `content.windowed_fullscreen` setting to show e.g. a fullscreened video in - the window without fullscreening that window. -- New `:edit-command` command to edit the commandline in an editor. -- New `tabs.persist_mode_on_change` setting to keep the current mode when - switching tabs. - Changed ~~~~~~~ -- Some tabs settings got renamed: +- The `hist_importer.py` script now only imports URL schemes qutebrowser can + handle. +- Deleting a prefix (`:`, `/` or `?`) via backspace now leaves command mode. +- Angular 1 elements now get hints assigned. +- `:tab-only` with pinned tabs now still closes unpinned tabs. + +Fixed +~~~~~ + +- Improved fullscreen handling with Qt 5.10. + +v1.1.1 (unreleased) +------------------- + +Fixed +~~~~~ + +- The Makefile now actually works. + +v1.1.0 +------ + +Added +~~~~~ + +- Initial support for Greasemonkey scripts. There are still some rough edges, + but many scripts should already work. +- There's now a `misc/Makefile` file in releases, which should help + distributions which package qutebrowser, as they can run something like + `make -f misc/Makefile DESTDIR="$pkgdir" install` now. +- New fields for `window.title_format` and `tabs.title.format`: + * `{current_url}` + * `{protocol}` +- New settings: + * `colors.statusbar.passthrough.fg`/`.bg` + * `completion.delay` and `completion.min_chars` to update the completion less + often. + * `completion.use_best_match` to automatically use the best-matching + command in the completion. + * `keyhint.radius` to configure the edge rounding for the key hint widget. + * `qt.highdpi` to turn on Qt's High-DPI scaling. + * `tabs.pinned.shrink` (`true` by default) to make it possible + for pinned tabs and normal tabs to have the same size. + * `content.windowed_fullscreen` to show e.g. a fullscreened video in the + window without fullscreening that window. + * `tabs.persist_mode_on_change` to keep the current mode when + switching tabs. + * `session.lazy_restore` which allows to not load pages immediately + when restoring a session. +- New commands: + * `:tab-give` and `:tab-take`, to give tabs to another window, or take them + from another window. + * `:completion-item-yank` (bound to ``) to yank the current + completion item text. + * `:edit-command` to edit the commandline in an editor. + * `search.incremental` for incremental text search. +- New flags for existing commands: + * `-o` flag for `:spawn` to show stdout/stderr in a new tab. + * `--rapid` flag for `:command-accept` (bound to `Ctrl-Enter` by default), + which allows executing a command in the completion without closing it. + * `--private` and `--related` flags for `:edit-url`, which have the + same effect they have with `:open`. + * `--history` for `:completion-item-focus` which causes it to go + through the command history when no text was entered. The default bindings for + cursor keys in the completion changed to use that, so that they can be used + again to navigate through completion items when a text was entered. + * `--file` for `:debug-pyeval` which makes it take a filename instead of a + line of code. +- New `config.source(...)` method for `config.py` to source another file. +- New `{line}` and `{column}` replacements for `editor.command` to position the + cursor correctly. +- New `qute-pass` userscript as alternative to `password_fill` which allows + selecting accounts via rofi or any other dmenu-compatile application. +- New `hist_importer.py` script to import history from Firefox/Chromium. + +Changed +~~~~~~~ + +- Some settings got renamed: * `tabs.width.bar` -> `tabs.width` * `tabs.width.indicator` -> `tabs.indicator.width` * `tabs.indicator_padding` -> `tabs.indicator.padding` + * `session_default_name` -> `session.default_name` + * `ignore_case` -> `search.ignore_case` +- Much improved user stylesheet handling for QtWebEngine which reduces + flickering and updates immediately after setting a stylesheet. - High-DPI favicons are now used when available. - The `asciidoc2html.py` script now uses Pygments (which is already a dependency of qutebrowser) instead of `source-highlight` for syntax highlighting. - The `:buffer` command now doesn't require quoting anymore, similar to `:open`. - The `importer.py` script was largely rewritten and now also supports importing from Firefox' `places.sqlite` file and Chrome/Chromium profiles. -- Various internal refactorings to use Python 3.5 and ECMAscript 6 features +- Various internal refactorings to use Python 3.5 and ECMAscript 6 features. - If the `window.hide_wayland_decoration` setting is False, but `QT_WAYLAND_DISABLE_WINDOWDECORATION` is set in the environment, the decorations are still hidden. @@ -77,36 +125,46 @@ Changed - The `qute://version` page now also shows the uptime of qutebrowser. - qutebrowser now prompts to create a non-existing directory when starting a download. -- Much improved user stylesheet handling which reduces flickering - and updates immediately after setting a stylesheet. -- `:completion-item-focus` now has a `--history` flag which causes it to go - through the command history when no text was entered. The default bindings for - cursor keys in the completion changed to use that, so that they can be used - again to navigate through completion items when a text was entered. -- `:debug-pyeval` now has a `--file` argument so it takes a filename instead of - a line of code. -- `:jseval --file` now searches relative paths in a js/ subdir in qutebrowser's - data dir, e.g. `~/.local/share/qutebrowser/js`. -- The current/default bindings are now shown in the :bind completion. +- `:jseval --file` now searches relative paths in a `js/` subdir in + qutebrowser's data dir, e.g. `~/.local/share/qutebrowser/js`. +- The current/default bindings are now shown in the ``:bind` completion. - Empty categories are now hidden in the `:open` completion. +- Search terms for URLs and titles can now be mixed when filtering the + completion. +- The default font size for the UI got bumped up from 8pt to 10pt. +- Improved matching in the completion: The words entered are now matched in any + order, and mixed matches on URL/tite are possible. +- The system's default encoding (rather than UTF-8) is now used to decode + subprocess output. +- qutebrowser now ensures it's focused again after an external editor is closed. +- The `colors.completion.fg` setting can now be a list, allowing to specify + different colors for the three completion columns. Fixed ~~~~~ - More consistent sizing for favicons with vertical tabs. - Using `:home` on pinned tabs is now prevented. -- Fix crash with unknown file types loaded via qute://help. +- Fix crash with unknown file types loaded via `qute://help`. - Scrolling performance improvements. - Sites like `qute://help` now redirect to `qute://help/` to make sure links work properly. - Fixes for the size calculation of pinned tabs in the tab bar. - Worked around a crash with PyQt 5.9.1 compiled against Qt < 5.9.1 when using - :yank or qute:// URLs. -- Fixed crash when opening `qute://help/img` + `:yank` or `qute://` URLs. +- Fixed crash when opening `qute://help/img`. - Fixed `gU` (`:navigate up`) on `qute://help` and webservers not handling `..` in a URL. - Using e.g. `-s backend webkit` to set the backend now works correctly. - Fixed crash when closing the tab an external editor was opened in. +- When using `:search-next` before a search is finished, no warning about no + results being found is shown anymore. +- Fix `:click-element` with an ID containing non-alphanumeric characters. +- Fix crash when a subprocess outputs data which is not decodable as UTF-8. +- Fix crash when closing a tab immediately after hinting. +- Worked around issues in Qt 5.10 with loading progress never being finished. +- Fixed a crash when writing a flag before a command (e.g. `:-w open `). +- Fixed a crash when clicking certain form elements with QtWebEngine. Deprecated ~~~~~~~~~~ @@ -119,6 +177,8 @@ Removed - The long-deprecated `:prompt-yes`, `:prompt-no`, `:paste-primary` and `:paste` commands have been removed. +- The invocation `:download ` which was deprecated in v0.5.0 was + removed, use `:download --dest ` instead. - The `messages.unfocused` option which wasn't used anymore was removed. - The `x[xtb]` default bindings got removed again as many users accidentally triggered them. diff --git a/doc/faq.asciidoc b/doc/faq.asciidoc index 7794c9d13..0d94796a4 100644 --- a/doc/faq.asciidoc +++ b/doc/faq.asciidoc @@ -221,5 +221,5 @@ My issue is not listed.:: https://github.com/qutebrowser/qutebrowser/issues[the issue tracker] or using the `:report` command. If you are reporting a segfault, make sure you read the - link:doc/stacktrace.asciidoc[guide] on how to report them with all needed + link:stacktrace.asciidoc[guide] on how to report them with all needed information. diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 95caf24f1..098ada8af 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -54,6 +54,7 @@ It is possible to run or bind multiple commands by separating them with `;;`. |<>|Follow the selected text. |<>|Go forward in the history of the current tab. |<>|Toggle fullscreen mode. +|<>|Re-read Greasemonkey scripts from disk. |<>|Show help about a command or setting. |<>|Start hinting. |<>|Show browsing history. @@ -333,12 +334,10 @@ Write the current configuration to a config.py file. [[download]] === download -Syntax: +:download [*--mhtml*] [*--dest* 'dest'] ['url'] ['dest-old']+ +Syntax: +:download [*--mhtml*] [*--dest* 'dest'] ['url']+ Download a given URL, or current page if no URL given. -The form `:download [url] [dest]` is deprecated, use `:download --dest [dest] [url]` instead. - ==== positional arguments * +'url'+: The URL to download. If not given, download the current page. @@ -491,6 +490,12 @@ Toggle fullscreen mode. ==== optional arguments * +*-l*+, +*--leave*+: Only leave fullscreen if it was entered by the page. +[[greasemonkey-reload]] +=== greasemonkey-reload +Re-read Greasemonkey scripts from disk. + +The scripts are read from a 'greasemonkey' subdirectory in qutebrowser's data directory (see `:version`). + [[help]] === help Syntax: +:help [*--tab*] [*--bg*] [*--window*] ['topic']+ @@ -511,7 +516,7 @@ Show help about a command or setting. [[hint]] === hint -Syntax: +:hint [*--rapid*] [*--mode* 'mode'] [*--add-history*] +Syntax: +:hint [*--mode* 'mode'] [*--add-history*] [*--rapid*] ['group'] ['target'] ['args' ['args' ...]]+ Start hinting. @@ -565,11 +570,6 @@ Start hinting. ==== optional arguments -* +*-r*+, +*--rapid*+: Whether to do rapid hinting. With rapid hinting, the hint mode isn't left after a hint is followed, so you can easily - open multiple links. This is only possible with targets - `tab` (with `tabs.background_tabs=true`), `tab-bg`, - `window`, `run`, `hover`, `userscript` and `spawn`. - * +*-m*+, +*--mode*+: The hinting mode to use. - `number`: Use numeric hints. @@ -581,6 +581,11 @@ Start hinting. * +*-a*+, +*--add-history*+: Whether to add the spawned or yanked link to the browsing history. +* +*-r*+, +*--rapid*+: Whether to do rapid hinting. With rapid hinting, the hint mode isn't left after a hint is followed, so you can easily + open multiple links. This is only possible with targets + `tab` (with `tabs.background_tabs=true`), `tab-bg`, + `window`, `run`, `hover`, `userscript` and `spawn`. + ==== note * This command does not split arguments after the last argument and handles quotes literally. @@ -1083,7 +1088,7 @@ Syntax: +:session-save [*--current*] [*--quiet*] [*--force*] [*--only-active-win Save a session. ==== positional arguments -* +'name'+: The name of the session. If not given, the session configured in session_default_name is saved. +* +'name'+: The name of the session. If not given, the session configured in session.default_name is saved. ==== optional arguments @@ -1141,7 +1146,7 @@ Set a mark at the current scroll position in the current tab. [[spawn]] === spawn -Syntax: +:spawn [*--userscript*] [*--verbose*] [*--detach*] 'cmdline'+ +Syntax: +:spawn [*--userscript*] [*--verbose*] [*--output*] [*--detach*] 'cmdline'+ Spawn a command in a shell. @@ -1156,6 +1161,7 @@ Spawn a command in a shell. - `/usr/share/qutebrowser/userscripts` * +*-v*+, +*--verbose*+: Show notifications when the command started/exited. +* +*-o*+, +*--output*+: Whether the output should be shown in a new tab. * +*-d*+, +*--detach*+: Whether the command should be detached from qutebrowser. ==== note @@ -1415,8 +1421,13 @@ How many steps to zoom out. |============== [[command-accept]] === command-accept +Syntax: +:command-accept [*--rapid*]+ + Execute the command currently in the commandline. +==== optional arguments +* +*-r*+, +*--rapid*+: Run the command without closing or clearing the command bar. + [[command-history-next]] === command-history-next Go forward in the commandline history. diff --git a/doc/help/configuring.asciidoc b/doc/help/configuring.asciidoc index 766e6bc7f..a9b7b6ddf 100644 --- a/doc/help/configuring.asciidoc +++ b/doc/help/configuring.asciidoc @@ -264,7 +264,7 @@ get a string: .config.py: [source,python] ---- -print(str(config.configdir / 'config.py') +print(str(config.configdir / 'config.py')) ---- Handling errors diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 3f6a8a016..53af8399d 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -199,7 +199,6 @@ |<>|Scatter hint key chains (like Vimium) or not (like dwb). |<>|Make characters in hint strings uppercase. |<>|Maximum time (in minutes) between two history items for them to be considered being from the same browsing session. -|<>|When to find text on a page case-insensitively. |<>|Which unbound keys to forward to the webview in normal mode. |<>|Leave insert mode if a non-editable element is clicked. |<>|Automatically enter insert mode if an editable element is focused after loading the page. @@ -222,7 +221,10 @@ |<>|Turn on Qt HighDPI scaling. |<>|Show a scrollbar. |<>|Enable smooth scrolling for web pages. -|<>|Name of the session to save by default. +|<>|When to find text on a page case-insensitively. +|<>|Find text on a page incrementally, renewing the search for each typed character. +|<>|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. @@ -425,6 +427,7 @@ Default: * +pass:[<Ctrl-K>]+: +pass:[rl-kill-line]+ * +pass:[<Ctrl-N>]+: +pass:[command-history-next]+ * +pass:[<Ctrl-P>]+: +pass:[command-history-prev]+ +* +pass:[<Ctrl-Return>]+: +pass:[command-accept --rapid]+ * +pass:[<Ctrl-Shift-C>]+: +pass:[completion-item-yank --sel]+ * +pass:[<Ctrl-Shift-Tab>]+: +pass:[completion-item-focus prev-category]+ * +pass:[<Ctrl-Tab>]+: +pass:[completion-item-focus next-category]+ @@ -698,10 +701,15 @@ Default: +pass:[#333333]+ [[colors.completion.fg]] === colors.completion.fg Text color of the completion widget. +May be a single color to use for all columns or a list of three colors, one for each column. -Type: <> +Type: <> -Default: +pass:[white]+ +Default: + +- +pass:[white]+ +- +pass:[white]+ +- +pass:[white]+ [[colors.completion.item.selected.bg]] === colors.completion.item.selected.bg @@ -2325,20 +2333,6 @@ Type: <> Default: +pass:[30]+ -[[ignore_case]] -=== ignore_case -When to find text on a page case-insensitively. - -Type: <> - -Valid values: - - * +always+: Search case-insensitively. - * +never+: Search case-sensitively. - * +smart+: Search case-sensitively if there are capital characters. - -Default: +pass:[smart]+ - [[input.forward_unbound_keys]] === input.forward_unbound_keys Which unbound keys to forward to the webview in normal mode. @@ -2556,8 +2550,30 @@ Type: <> Default: +pass:[false]+ -[[session_default_name]] -=== session_default_name +[[search.ignore_case]] +=== search.ignore_case +When to find text on a page case-insensitively. + +Type: <> + +Valid values: + + * +always+: Search case-insensitively. + * +never+: Search case-sensitively. + * +smart+: Search case-sensitively if there are capital characters. + +Default: +pass:[smart]+ + +[[search.incremental]] +=== search.incremental +Find text on a page incrementally, renewing the search for each typed character. + +Type: <> + +Default: +pass:[true]+ + +[[session.default_name]] +=== session.default_name Name of the session to save by default. If this is set to null, the session which was last loaded is saved. @@ -2565,6 +2581,14 @@ Type: <> Default: empty +[[session.lazy_restore]] +=== session.lazy_restore +Load a restored tab as soon as it takes focus. + +Type: <> + +Default: +pass:[false]+ + [[spellcheck.languages]] === spellcheck.languages Languages to use for spell checking. @@ -2904,8 +2928,9 @@ The following placeholders are defined: * `{scroll_pos}`: Page scroll position. * `{host}`: Host of the current web page. * `{backend}`: Either ''webkit'' or ''webengine'' -* `{private}` : Indicates when private mode is enabled. -* `{current_url}` : URL of the current web page. +* `{private}`: Indicates when private mode is enabled. +* `{current_url}`: URL of the current web page. +* `{protocol}`: Protocol (http/https/...) of the current web page. Type: <> diff --git a/doc/install.asciidoc b/doc/install.asciidoc index 1dba5fa57..4b1fd3f26 100644 --- a/doc/install.asciidoc +++ b/doc/install.asciidoc @@ -21,7 +21,7 @@ Those distributions only have Python 3.4 and a too old Qt version available, while qutebrowser requires Python 3.5 and Qt 5.7.1 or newer. It should be possible to install Python 3.5 e.g. from the -https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa[deadsnakes PPA] or via_ipca +https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa[deadsnakes PPA] or via https://github.com/pyenv/pyenv[pyenv], but nobody tried that yet. If you get qutebrowser running on those distributions, please @@ -35,7 +35,7 @@ Ubuntu 16.04 doesn't come with an up-to-date engine (a new enough QtWebKit, or QtWebEngine). However, it comes with Python 3.5, so you can <>. -Debian Stretch / Ubuntu 17.04 and newer +Debian Stretch / Ubuntu 17.04 and 17.10 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Those versions come with QtWebEngine in the repositories. This makes it possible @@ -54,7 +54,18 @@ Install the packages: # apt install ./qutebrowser_*_all.deb ---- -Some additional hints: +Debian Testing / Ubuntu 18.04 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +On Debian Testing, qutebrowser is in the official repositories, and you can +install it with apt: + +---- +# apt install qutebrowser +---- + +Additional hints +~~~~~~~~~~~~~~~~ - Alternatively, you can <> to get a newer QtWebEngine version. @@ -67,8 +78,7 @@ $ python3 scripts/asciidoc2html.py ---- - If you prefer using QtWebKit, there's an up-to-date version available in - Debian experimental, or from http://repo.paretje.be/unstable/[this repository] - for Debian Stretch. + https://packages.debian.org/buster/libqt5webkit5[Debian Testing]. - If video or sound don't work with QtWebKit, try installing the gstreamer plugins: + ---- @@ -90,6 +100,18 @@ qutebrowser is available in the official repositories: However, note that Fedora 25/26 won't be updated to qutebrowser v1.0, so you might want to <> instead there. +Additional hints +~~~~~~~~~~~~~~~~ + +Fedora only ships free software in the repositories. +To be able to play videos with proprietary codecs with QtWebEngine, you will +need to install an additional package from the RPM Fusion Free repository. +For more information see https://rpmfusion.org/Configuration. + +----- +# dnf install qt5-qtwebengine-freeworld +----- + On Archlinux ------------ @@ -379,8 +401,8 @@ local Qt install instead of installing PyQt in the virtualenv. However, unless you have a new QtWebKit or QtWebEngine available, qutebrowser will not work. It also typically means you'll be using an older release of QtWebEngine. -On Windows, run `tox -e 'mkvenv-win' instead, however make sure that ONLY -Python3 is in your PATH before running tox. +On Windows, run `set PYTHON=C:\path\to\python.exe` (CMD) or ``$Env:PYTHON = +"..."` (Powershell) first. Creating a wrapper script ~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/icons/qutebrowser.xpm b/icons/qutebrowser.xpm index 8fac91045..04e183154 100644 --- a/icons/qutebrowser.xpm +++ b/icons/qutebrowser.xpm @@ -1,207 +1,302 @@ /* XPM */ -static char *qutebrowser[] = { -/* columns rows colors chars-per-pixel */ -"32 32 169 2 ", -" c #0A396E", -". c #0B3C72", -"X c #0B4077", -"o c #0C437B", -"O c #134175", -"+ c #15467C", -"@ c #18477B", -"# c #1A497D", -"$ c #0D4B86", -"% c #0F4E8D", -"& c #124A80", -"* c #1F4F83", -"= c #0E518C", -"- c #1F5084", -"; c #11508C", -": c #0F5193", -"> c #115799", -", c #115B9C", -"< c #204F83", -"1 c #245287", -"2 c #2A598C", -"3 c #325E8F", -"4 c #11609F", -"5 c #346496", -"6 c #3B6898", -"7 c #115CA1", -"8 c #115EAC", -"9 c #1263A3", -"0 c #1260AD", -"q c #136BAC", -"w c #136BB2", -"e c #1366BA", -"r c #196BB2", -"t c #157ABB", -"y c #1577BB", -"u c #2E6DB0", -"i c #387FB1", -"p c #456E9A", -"a c #4873A1", -"s c #4375AA", -"d c #507AA6", -"f c #597EA4", -"g c #4D7EB3", -"h c #156FCB", -"j c #167AC5", -"k c #1675CA", -"l c #177BCE", -"z c #1777D8", -"x c #1476E4", -"c c #167BE6", -"v c #167DE8", -"b c #197EEF", -"n c #1A7FF0", -"m c #1A80BE", -"M c #5F87AF", -"N c #5D8BBA", -"B c #5A84B1", -"V c #6C8FB3", -"C c #6F96BE", -"Z c #1886CC", -"A c #1883D7", -"S c #198DD5", -"D c #1987D9", -"F c #198ADC", -"G c #1A96DC", -"H c #3090D9", -"J c #1682E9", -"K c #1983ED", -"L c #1689E9", -"P c #1A8DEE", -"I c #1B95ED", -"U c #1C9EEA", -"Y c #1B97E4", -"T c #1A84F2", -"R c #1A8BF2", -"E c #1C94F4", -"W c #1D9CF5", -"Q c #3388E6", -"! c #3D90E9", -"~ c #228EF3", -"^ c #229FF6", -"/ c #3294F4", -"( c #3D9FF6", -") c #339CF4", -"_ c #1CA2E5", -"` c #1DABEE", -"' c #1DA4F6", -"] c #1EA9F7", -"[ c #1EADF8", -"{ c #1FB4F9", -"} c #1FB9FA", -"| c #20ACF8", -" . c #27A4F6", -".. c #3DA9F6", -"X. c #20B9FA", -"o. c #2EB6F9", -"O. c #458DC9", -"+. c #5C8DC1", -"@. c #5795C6", -"#. c #709DCB", -"$. c #74A8DD", -"%. c #4A97EA", -"&. c #4896EA", -"*. c #559EEA", -"=. c #439AF5", -"-. c #46A3F6", -";. c #5FA9F6", -":. c #5EA6F3", -">. c #47BCF9", -",. c #51B5F8", -"<. c #58BDF8", -"1. c #68ABEF", -"2. c #7DB9E7", -"3. c #63AEF7", -"4. c #6FB1F7", -"5. c #66B9F8", -"6. c #61B2F6", -"7. c #71B4F7", -"8. c #78B7F4", -"9. c #72BFF9", -"0. c #3BC0FA", -"q. c #6FCEFB", -"w. c #6CC5FA", -"e. c #7BCAF9", -"r. c #89A7C3", -"t. c #83A2C1", -"y. c #98B6D3", -"u. c #9DB9D3", -"i. c #89B6E4", -"p. c #83B6E9", -"a. c #81BDF7", -"s. c #83BFF8", -"d. c #9EC4E9", -"f. c #8CC2F9", -"g. c #85CDFB", -"h. c #87C4F9", -"j. c #92C6F9", -"k. c #95CAFA", -"l. c #9CCBFA", -"z. c #89D7FC", -"x. c #91D9FC", -"c. c #9CDEFD", -"v. c #9ED2FB", -"b. c #A7CAEC", -"n. c #B5CEE3", -"m. c #A1CEFA", -"M. c #AED0F0", -"N. c #ACD6FA", -"B. c #A0DFFC", -"V. c #AFD8FC", -"C. c #B5D9FB", -"Z. c #BCDDFC", -"A. c #BFDCF5", -"S. c #ACE3FD", -"D. c #B5E5FE", -"F. c #BBE2FC", -"G. c #CFE5F5", -"H. c #C3E1FC", -"J. c #CAE6FD", -"K. c #CCEBFD", -"L. c #C4EBFE", -"P. c #D6EDFE", -"I. c #DAEEFD", -"U. c #DEF1FE", -"Y. c #D6F2FE", -"T. c #E4F4FE", -"R. c #E9F6FE", -"E. c #EBF8FF", -"W. c None", -/* pixels */ -"W.W.W.W.W.W.W.W.W.W.W.c.S.L.Y.E.E.S.X.} W.W.W.W.W.W.W.W.W.W.W.W.", -"W.W.W.W.W.W.W.W.W.D.T.E.E.T.L.D.c.z.} } X.} } W.W.W.W.W.W.W.W.W.", -"W.W.W.W.W.W.W.B.T.T.R.T.R.U.0.X.z.S.} } } } { { X.W.W.W.W.W.W.W.", -"W.W.W.W.W.W.x.x.K.T.T.T.L.P.q.o.{ } } ` _ { { { { { W.W.W.W.W.W.", -"W.W.W.W.W.c.P.D.G.u.r.i 9 Z _ { { G 4 X t { { { { { { W.W.W.W.W.", -"W.W.W.W.K.U.n.f O { = t { { { { [ { { W.W.W.W.", -"W.W.W.F.I.t.. ' t { { [ [ [ [ [ >.W.W.W.", -"W.W.x.P.V ' X t ` [ [ [ [ [ [ o.e.W.W.", -"W.W.J.y. X t S Y Z $ ' . y [ [ [ ] [ [ | Z.J.W.W.", -"W.<.e.& , _ ] ] [ ] U . ' . y [ ' [ ] ] ] w.K.J.g.W.", -"W.' S o ' ' [ ' [ ' ] o ' . y Y 9 = = 9 @.J.J.J.F.W.", -"W.| , j ' ' ' ' ' ' ' o ' . $ p A.J.J.g.", -"' .. G ' ' ' ' ' ' ' o ' . M H.H.h.", -",.2. . W ' W ' ' ' ' W . ' . M.A.x.", -"N.M.. . W W W ' W W W W .w 9 I U 0 #.Z.m.", -" .9.O D W W W W ' W j $ % F W W W .5 d Z.C.", -"W W ; 9 9.h.5...Q % o j W W W W W W O. 3 C.N.", -"E W 7 B b.d.a . w E E W W W E W E A @ C.l.", -"I E l u W E W E W E E E E A . - k.6.", -"P E E 7 m.o E E E E E E E E l . = E P ", -"L E E E > . O s.o E E E E E E E E 7 , E L ", -"W.R E R ) #.5 1 6 N i.2 s.+ E E E E E E R L . k R W.", -"W.L R E -.m.m.m.m.m.m.2 m.@ N m.m.s.( R R % X E J W.", -"W.W.K R ~ a.m.l.l.l.l.2 s.+ < i.l.m.j.h % e K W.W.", -"W.W.J R R / l.l.l.l.k.2 s.+ * 5 + 8 R J W.W.", -"W.W.W.v T R 3.k.k.j.k.2 2 j.& . 8 R v W.W.W.", -"W.W.W.W.J T ~ 7.j.j.j.g +.p.j.s.+. . . : z T v W.W.W.W.", -"W.W.W.W.W.c T T =.f.j.j.s.j.j.j.j.$.g s u e h b T T v W.W.W.W.W.", -"W.W.W.W.W.W.c b n 4.f.f.s.m.s.s.s.j.s.j./ T n T b c W.W.W.W.W.W.", -"W.W.W.W.W.W.W.c x 1.s.s.s.s.s.s.s.s.4.=.n T n c c W.W.W.W.W.W.W.", -"W.W.W.W.W.W.W.W.W.&.*.1.a.s.s.s.s.3.n n v x x W.W.W.W.W.W.W.W.W.", -"W.W.W.W.W.W.W.W.W.W.W.%.%.%.%.*.*.Q x x x W.W.W.W.W.W.W.W.W.W.W." -}; +static char * qutebrowser_xpm[] = { +"32 32 267 2", +" c None", +". c #9FD4FD", +"+ c #99CBFE", +"@ c #90C3FE", +"# c #89BFFE", +"$ c #81BCFF", +"% c #80BBFF", +"& c #9BCAFD", +"* c #A9DBFB", +"= c #88D3FB", +"- c #98CBFE", +"; c #81BBFF", +"> c #7EBAFF", +", c #84BDFF", +"' c #8DC2FF", +") c #96C7FE", +"! c #A0CCFE", +"~ c #A9D1FE", +"{ c #CEE5FD", +"] c #C7E3FC", +"^ c #8AD3FB", +"/ c #9DCFFD", +"( c #C3DFFD", +"_ c #CDE4FD", +": c #A3CEFE", +"< c #94C6FE", +"[ c #CAE5FC", +"} c #7DD0FB", +"| c #9ECDFD", +"1 c #A1CDFE", +"2 c #8BC1FF", +"3 c #87BFFF", +"4 c #ADD4FE", +"5 c #C6E1FD", +"6 c #CCE3FC", +"7 c #A7DAFB", +"8 c #9DCBFE", +"9 c #78AFF1", +"0 c #6096D4", +"a c #4B82C0", +"b c #5A84B3", +"c c #6589B1", +"d c #6F92B9", +"e c #90AED0", +"f c #C4DBF5", +"g c #6286AE", +"h c #7D9EC2", +"i c #BADFFC", +"j c #85BDFE", +"k c #78B4F8", +"l c #4C83C0", +"m c #1E4F87", +"n c #0A396E", +"o c #345D8D", +"p c #CDE4FC", +"q c #88A7CA", +"r c #1D497C", +"s c #799BBF", +"t c #8AC1FD", +"u c #5E97D7", +"v c #14457B", +"w c #4F76A0", +"x c #A9D5FC", +"y c #95C9FD", +"z c #4C82C1", +"A c #0A3A6F", +"B c #C9E3FD", +"C c #95CCFC", +"D c #629BDB", +"E c #0B3A6F", +"F c #0C3B6F", +"G c #4E749F", +"H c #8CACCE", +"I c #6185AD", +"J c #CBE4FD", +"K c #89C0FF", +"L c #98CDFA", +"M c #27558A", +"N c #144175", +"O c #9BB8D8", +"P c #335D8C", +"Q c #AFC9E6", +"R c #AFD4FE", +"S c #91C7FD", +"T c #A0C0DE", +"U c #194779", +"V c #80A1C5", +"W c #C8E1F9", +"X c #9CB9D8", +"Y c #7799BE", +"Z c #6489B0", +"` c #7092B9", +" . c #6E9DCF", +".. c #79B5F9", +"+. c #83BDFE", +"@. c #7395BA", +"#. c #315C8B", +"$. c #7C9EC2", +"%. c #C0D9F3", +"&. c #7294BA", +"*. c #5C94D4", +"=. c #91CCFC", +"-. c #88CBFA", +";. c #5179A3", +">. c #6E91B7", +",. c #6084AC", +"'. c #96B3D4", +"). c #275283", +"!. c #0C3C71", +"~. c #629CDC", +"{. c #94C6FD", +"]. c #A7D2FC", +"^. c #36659A", +"/. c #2C5788", +"(. c #9DBAD9", +"_. c #B4CEEA", +":. c #476E9A", +"<. c #7EB9FE", +"[. c #8DC3FD", +"}. c #8CC2FE", +"|. c #2F619B", +"1. c #87A6C9", +"2. c #7A9BC0", +"3. c #CBE2FB", +"4. c #C7DFF8", +"5. c #6C8FB5", +"6. c #113F73", +"7. c #0F3D71", +"8. c #547AA4", +"9. c #9CBAD9", +"0. c #B9D3EE", +"a. c #A3C0DE", +"b. c #31629A", +"c. c #659EE0", +"d. c #87BFFE", +"e. c #C3E0FD", +"f. c #4371A4", +"g. c #7496BB", +"h. c #90AFD1", +"i. c #245081", +"j. c #416A96", +"k. c #B0CBE7", +"l. c #CCE4FD", +"m. c #7DB8FD", +"n. c #1E5088", +"o. c #497EBC", +"p. c #C9E3FC", +"q. c #7193B9", +"r. c #C6E0FB", +"s. c #A2CDFE", +"t. c #97C8FE", +"u. c #A7D0FE", +"v. c #BDDCFD", +"w. c #9EC2E8", +"x. c #416996", +"y. c #366AA6", +"z. c #C0DEFC", +"A. c #A2BFDD", +"B. c #326299", +"C. c #649DDF", +"D. c #71ABED", +"E. c #3569A4", +"F. c #0D3C71", +"G. c #6998CD", +"H. c #30639D", +"I. c #A8D3F8", +"J. c #2B5686", +"K. c #3A679B", +"L. c #ADCAEA", +"M. c #85A6C9", +"N. c #33639B", +"O. c #9CCBFD", +"P. c #86C2F7", +"Q. c #0E3C71", +"R. c #1B4C83", +"S. c #5D95D5", +"T. c #557BA5", +"U. c #85C0F6", +"V. c #55A8EF", +"W. c #94B3D3", +"X. c #1C497C", +"Y. c #13437A", +"Z. c #487DBB", +"`. c #7BB7FB", +" + c #76B1F5", +".+ c #4E85C3", +"++ c #ACD3FE", +"@+ c #2F5989", +"#+ c #7597BC", +"$+ c #53A7EF", +"%+ c #C6E1FC", +"&+ c #B6D5F7", +"*+ c #5890D0", +"=+ c #4076B2", +"-+ c #619ADB", +";+ c #7CB7FC", +">+ c #7DB9FE", +",+ c #5087C6", +"'+ c #134479", +")+ c #23548D", +"!+ c #24558D", +"~+ c #8AAACC", +"{+ c #A2C1E1", +"]+ c #86C1F5", +"^+ c #B4D7FE", +"/+ c #6CA5E8", +"(+ c #22548C", +"_+ c #6D94BF", +":+ c #98B6D6", +"<+ c #134174", +"[+ c #84BDF5", +"}+ c #CAE4FC", +"|+ c #CBE3FD", +"1+ c #8FC3FF", +"2+ c #3F72AD", +"3+ c #49719C", +"4+ c #0C3B70", +"5+ c #9CBBDB", +"6+ c #79B7F3", +"7+ c #BFDCFD", +"8+ c #7FBBFF", +"9+ c #7E9FC3", +"0+ c #77B6F3", +"a+ c #A5CEF7", +"b+ c #9FCBFE", +"c+ c #3267A1", +"d+ c #A4CDF7", +"e+ c #B9D9FA", +"f+ c #C7E1FD", +"g+ c #90C3FF", +"h+ c #15457C", +"i+ c #558CCB", +"j+ c #2E5889", +"k+ c #7B9CC1", +"l+ c #C4DDF6", +"m+ c #BBDAFA", +"n+ c #CDE5FD", +"o+ c #B3D6FE", +"p+ c #80BAFF", +"q+ c #4E84C3", +"r+ c #3E73AF", +"s+ c #78B3F7", +"t+ c #5991D1", +"u+ c #477DBA", +"v+ c #4075B2", +"w+ c #5783B6", +"x+ c #BDD6F0", +"y+ c #A1CBF6", +"z+ c #90C4FF", +"A+ c #BCDBFD", +"B+ c #73B0F1", +"C+ c #C5E0FB", +"D+ c #91C5FF", +"E+ c #AED3FE", +"F+ c #C9E2FC", +"G+ c #76B2F2", +"H+ c #8BBFF9", +"I+ c #81BBFE", +"J+ c #9ECBFE", +"K+ c #84B8F3", +"L+ c #79B4F4", +"M+ c #88BEFA", +"N+ c #83BCFE", +"O+ c #A4CFFC", +"P+ c #A6CDF6", +"Q+ c #82B8F2", +"R+ c #529BEC", +" . + @ # $ % & * = ", +" - ; > > , ' ) ! ~ { { { ] ^ ", +" / ; > > > > ; ( _ : < { { { { { [ } ", +" | 1 2 > > > 2 3 4 5 { { { { { 6 { { { 7 ", +" 8 $ < 9 0 a b c d e { { { { f g h { { { { i ", +" j k l m n n n n n n o { { p q r n s { { { { { i ", +" t u v n n n n n n n n o { { w n n n s { { { { { { x ", +" y z A n n n n n n n n n o { { o n n n s { { { { { { B C ", +" D E n n n F G H I n n n o { { o n n n s { { { { { J K % ", +" L M n n n N O { { s n n n o { { o n n P Q { { { { { R > > S ", +" T n n n n H { { { s n n n o { { o U V 6 W X Y Z ` ...> > +. ", +" @.n n n #.{ { { { s n n n o { { $.%.W &.U n n n n n v *.> > =.", +"-.;.n n n >.{ { { { s n n n ,.{ { { '.).n n n n n n n n !.~.> {.", +"].^.n n n q { { { { s n /.(.{ { _.:.n n n n n n n n n n n m <.[.", +"}.|.n n n H { { { { 1.2.3.{ 4.5.6.n n n 7.8.9.0.a.b.n n n n c.d.", +"e.f.n n n g.{ { { { { { { h.i.n n n n j.k.{ { { l.m.n.n n n o.$ ", +"p.q.n n n /.r.s.t.u.v.w.x.n n n n i.h.{ { { { { { u.o.n n n y.$ ", +"z.A.n n n n B.C.D.u E.F.n n n 6.5.4.{ 3.2.1.{ { { { G.n n n H.d.", +"I.p J.n n n n n n n n n n n K.L.{ { (./.n s { { { { M.n n n N.O.", +"P.{ (.Q.n n n n n n n n R.S.> K _ ,.n n n s { { { { 5.n n n T.U.", +"V.{ { W.X.n n n n n Y.Z.`. +.+> ++o n n n s { { { { @+n n n #+$+", +" %+{ { &+*+Z.=+a -+;+>+,+'+)+> > !+n n n s { { { ~+n n n n {+ ", +" ]+{ { ^+> > > > > /+(+n n )+> > )+n n n _+{ { :+<+n n n o [+ ", +" }+{ |+1+> > > > l n n n )+> > )+n n n 2+~+3+E n n n 4+5+ ", +" 6+{ { 7+8+> > > l n n n )+> > )+n n n n n n n n n F 9+0+ ", +" a+{ { b+> > > l n n n c+> > )+n n n n n n n n r O d+ ", +" e+{ f+g+> > l n h+i+<.> > )+n n n n n E j+k+l+m+ ", +" e+{ n+o+p+q+r+s+> > > > t+u+v+w+2.W.x+{ { e+ ", +" y+{ { z+>+> > > > > > > > > A+{ { { { d+ ", +" B+C+) > > > > > > > > D+E+{ { { F+G+ ", +" H+I+> > > > > > J+{ { { C+K+ ", +" L+M+# N+; 8+O+P+Q+R+ "}; diff --git a/misc/Makefile b/misc/Makefile index fe97eb6bf..714223d10 100644 --- a/misc/Makefile +++ b/misc/Makefile @@ -21,5 +21,5 @@ install: doc/qutebrowser.1.html $(wildcard misc/userscripts/*) install -Dm755 -t "$(DESTDIR)/usr/share/qutebrowser/scripts/" \ $(filter-out scripts/__init__.py scripts/__pycache__ scripts/dev \ - scripts/testbrowser_cpp scripts/asciidoc2html.py scripts/setupcommon.py \ + scripts/testbrowser scripts/asciidoc2html.py scripts/setupcommon.py \ scripts/link_pyqt.py,$(wildcard scripts/*)) diff --git a/misc/requirements/README.md b/misc/requirements/README.md index f38c4443e..6ae986279 100644 --- a/misc/requirements/README.md +++ b/misc/requirements/README.md @@ -1,5 +1,5 @@ This directory contains various `requirements` files which are used by `tox` to -have reproducable tests with pinned versions. +have reproducible tests with pinned versions. The files are generated based on unpinned requirements in `*.txt-raw` files. diff --git a/misc/requirements/requirements-codecov.txt b/misc/requirements/requirements-codecov.txt index 31c319c39..df7a12ed6 100644 --- a/misc/requirements/requirements-codecov.txt +++ b/misc/requirements/requirements-codecov.txt @@ -2,7 +2,7 @@ certifi==2017.11.5 chardet==3.0.4 -codecov==2.0.9 +codecov==2.0.13 coverage==4.4.2 idna==2.6 requests==2.18.4 diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt index a031778ba..0ae43d663 100644 --- a/misc/requirements/requirements-flake8.txt +++ b/misc/requirements/requirements-flake8.txt @@ -1,23 +1,23 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -attrs==17.3.0 +attrs==17.4.0 flake8==3.5.0 -flake8-bugbear==17.4.0 -flake8-builtins==1.0 +flake8-bugbear==17.12.0 +flake8-builtins==1.0.post0 flake8-comprehensions==1.4.1 flake8-copyright==0.2.0 flake8-debugger==3.0.0 flake8-deprecated==1.3 -flake8-docstrings==1.1.0 -flake8-future-import==0.4.3 +flake8-docstrings==1.3.0 +flake8-future-import==0.4.4 flake8-mock==0.3 flake8-per-file-ignores==0.4 -flake8-polyfill==1.0.1 +flake8-polyfill==1.0.2 flake8-string-format==0.2.3 flake8-tidy-imports==1.1.0 flake8-tuple==0.2.13 mccabe==0.6.1 -pep8-naming==0.4.1 +pep8-naming==0.5.0 pycodestyle==2.3.1 pydocstyle==2.1.1 pyflakes==1.6.0 diff --git a/misc/requirements/requirements-pip.txt b/misc/requirements/requirements-pip.txt index 4f4b0986c..54cacc3c3 100644 --- a/misc/requirements/requirements-pip.txt +++ b/misc/requirements/requirements-pip.txt @@ -3,6 +3,6 @@ appdirs==1.4.3 packaging==16.8 pyparsing==2.2.0 -setuptools==38.2.1 +setuptools==38.4.0 six==1.11.0 wheel==0.30.0 diff --git a/misc/requirements/requirements-pyinstaller.txt b/misc/requirements/requirements-pyinstaller.txt index e542e4243..f65e8c62a 100644 --- a/misc/requirements/requirements-pyinstaller.txt +++ b/misc/requirements/requirements-pyinstaller.txt @@ -1,7 +1,7 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -altgraph==0.14 +altgraph==0.15 future==0.16.0 -macholib==1.8 +macholib==1.9 pefile==2017.11.5 -e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=PyInstaller diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index cab15c497..6267fc2b0 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -1,6 +1,6 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -astroid==1.5.3 +astroid==1.6.0 certifi==2017.11.5 chardet==3.0.4 github3.py==0.9.6 @@ -8,7 +8,7 @@ idna==2.6 isort==4.2.15 lazy-object-proxy==1.3.1 mccabe==0.6.1 -pylint==1.7.4 +pylint==1.8.1 ./scripts/dev/pylint_checkers requests==2.18.4 six==1.11.0 diff --git a/misc/requirements/requirements-pyroma.txt b/misc/requirements/requirements-pyroma.txt index d6ed0c190..241273169 100644 --- a/misc/requirements/requirements-pyroma.txt +++ b/misc/requirements/requirements-pyroma.txt @@ -1,4 +1,4 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py docutils==0.14 -pyroma==2.2 +pyroma==2.3 diff --git a/misc/requirements/requirements-tests-git.txt b/misc/requirements/requirements-tests-git.txt index 6b31140bb..6681dd15e 100644 --- a/misc/requirements/requirements-tests-git.txt +++ b/misc/requirements/requirements-tests-git.txt @@ -12,12 +12,6 @@ git+https://github.com/jenisys/parse_type.git hg+https://bitbucket.org/pytest-dev/py git+https://github.com/pytest-dev/pytest.git@features git+https://github.com/pytest-dev/pytest-bdd.git - -# This is broken at the moment because logfail tries to access -# LogCaptureHandler -# git+https://github.com/eisensheng/pytest-catchlog.git -pytest-catchlog==1.2.2 - git+https://github.com/pytest-dev/pytest-cov.git git+https://github.com/pytest-dev/pytest-faulthandler.git git+https://github.com/pytest-dev/pytest-instafail.git diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 358dd72a6..574dbdb28 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -1,8 +1,8 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -attrs==17.3.0 +attrs==17.4.0 beautifulsoup4==4.6.0 -cheroot==5.10.0 +cheroot==6.0.0 click==6.7 # colorama==0.3.9 coverage==4.4.2 @@ -11,29 +11,29 @@ fields==5.0.0 Flask==0.12.2 glob2==0.6 hunter==2.0.2 -hypothesis==3.38.5 +hypothesis==3.44.16 itsdangerous==0.24 -# Jinja2==2.9.6 +# Jinja2==2.10 Mako==1.0.7 # MarkupSafe==1.0 parse==1.8.2 parse-type==0.4.2 +pluggy==0.6.0 py==1.5.2 py-cpuinfo==3.3.0 -pytest==3.2.5 +pytest==3.3.1 # rq.filter: != 3.3.2 pytest-bdd==2.19.0 pytest-benchmark==3.1.1 -pytest-catchlog==1.2.2 pytest-cov==2.5.1 pytest-faulthandler==1.3.1 pytest-instafail==0.3.0 pytest-mock==1.6.3 -pytest-qt==2.3.0 +pytest-qt==2.3.1 pytest-repeat==0.4.1 -pytest-rerunfailures==3.1 -pytest-travis-fold==1.2.0 +pytest-rerunfailures==4.0 +pytest-travis-fold==1.3.0 pytest-xvfb==1.0.0 PyVirtualDisplay==0.2.1 six==1.11.0 vulture==0.26 -Werkzeug==0.12.2 +Werkzeug==0.14.1 diff --git a/misc/requirements/requirements-tests.txt-raw b/misc/requirements/requirements-tests.txt-raw index bc44bc8e1..174eeb7df 100644 --- a/misc/requirements/requirements-tests.txt-raw +++ b/misc/requirements/requirements-tests.txt-raw @@ -4,10 +4,9 @@ coverage Flask hunter hypothesis -pytest +pytest==3.3.1 pytest-bdd pytest-benchmark -pytest-catchlog pytest-cov pytest-faulthandler pytest-instafail @@ -20,3 +19,4 @@ pytest-xvfb vulture #@ ignore: Jinja2, MarkupSafe, colorama +#@ filter: pytest != 3.3.2 diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt index 1308c8afd..d2b3a719b 100644 --- a/misc/requirements/requirements-tox.txt +++ b/misc/requirements/requirements-tox.txt @@ -2,5 +2,6 @@ pluggy==0.6.0 py==1.5.2 +six==1.11.0 tox==2.9.1 virtualenv==15.1.0 diff --git a/misc/userscripts/cast b/misc/userscripts/cast index da68297d8..f7b64df70 100755 --- a/misc/userscripts/cast +++ b/misc/userscripts/cast @@ -144,7 +144,7 @@ fi pkill -f "${program_}" # start youtube download in stream mode (-o -) into temporary file -youtube-dl -qo - "$1" > ${file_to_cast} & +youtube-dl -qo - "$1" > "${file_to_cast}" & ytdl_pid=$! msg info "Casting $1" >> "$QUTE_FIFO" @@ -153,4 +153,4 @@ tail -F "${file_to_cast}" | ${program_} - # cleanup remaining background process and file on disk kill ${ytdl_pid} -rm -rf ${tmpdir} +rm -rf "${tmpdir}" diff --git a/misc/userscripts/dmenu_qutebrowser b/misc/userscripts/dmenu_qutebrowser index 9c809d5ad..82e6d2f18 100755 --- a/misc/userscripts/dmenu_qutebrowser +++ b/misc/userscripts/dmenu_qutebrowser @@ -41,7 +41,7 @@ [ -z "$QUTE_URL" ] && QUTE_URL='http://google.com' url=$(echo "$QUTE_URL" | cat - "$QUTE_CONFIG_DIR/quickmarks" "$QUTE_DATA_DIR/history" | dmenu -l 15 -p qutebrowser) -url=$(echo "$url" | sed -E 's/[^ ]+ +//g' | egrep "https?:" || echo "$url") +url=$(echo "$url" | sed -E 's/[^ ]+ +//g' | grep -E "https?:" || echo "$url") [ -z "${url// }" ] && exit diff --git a/misc/userscripts/format_json b/misc/userscripts/format_json index f756850f1..0d476b327 100755 --- a/misc/userscripts/format_json +++ b/misc/userscripts/format_json @@ -1,4 +1,5 @@ #!/bin/sh +set -euo pipefail # # Behavior: # Userscript for qutebrowser which will take the raw JSON text of the current @@ -19,29 +20,23 @@ # # Bryan Gilbert, 2017 +# do not run pygmentize on files larger than this amount of bytes +MAX_SIZE_PRETTIFY=10485760 # 10 MB # default style to monokai if none is provided STYLE=${1:-monokai} -# format json using jq -FORMATTED_JSON="$(cat "$QUTE_TEXT" | jq '.')" -# if jq command failed or formatted json is empty, assume failure and terminate -if [ $? -ne 0 ] || [ -z "$FORMATTED_JSON" ]; then - echo "Invalid json, aborting..." - exit 1 +TEMP_FILE="$(mktemp)" +jq . "$QUTE_TEXT" >"$TEMP_FILE" + +# try GNU stat first and then OSX stat if the former fails +FILE_SIZE=$( + stat --printf="%s" "$TEMP_FILE" 2>/dev/null || + stat -f%z "$TEMP_FILE" 2>/dev/null +) +if [ "$FILE_SIZE" -lt "$MAX_SIZE_PRETTIFY" ]; then + pygmentize -l json -f html -O full,style="$STYLE" <"$TEMP_FILE" >"${TEMP_FILE}_" + mv -f "${TEMP_FILE}_" "$TEMP_FILE" fi -# calculate the filesize of the json document -FILE_SIZE=$(ls -s --block-size=1048576 "$QUTE_TEXT" | cut -d' ' -f1) - -# use pygments to pretty-up the json (syntax highlight) if file is less than 10MB -if [ "$FILE_SIZE" -lt "10" ]; then - FORMATTED_JSON="$(echo "$FORMATTED_JSON" | pygmentize -l json -f html -O full,style=$STYLE)" -fi - -# create a temp file and write the formatted json to that file -TEMP_FILE="$(mktemp --suffix '.html')" -echo "$FORMATTED_JSON" > $TEMP_FILE - - # send the command to qutebrowser to open the new file containing the formatted json echo "open -t file://$TEMP_FILE" >> "$QUTE_FIFO" diff --git a/misc/userscripts/open_download b/misc/userscripts/open_download index 6c1213b65..ecc1d7209 100755 --- a/misc/userscripts/open_download +++ b/misc/userscripts/open_download @@ -76,6 +76,7 @@ crop-first-column() { ls-files() { # add the slash at the end of the download dir enforces to follow the # symlink, if the DOWNLOAD_DIR itself is a symlink + # shellcheck disable=SC2010 ls -Q --quoting-style escape -h -o -1 -A -t "${DOWNLOAD_DIR}/" \ | grep '^[-]' \ | cut -d' ' -f3- \ @@ -91,10 +92,10 @@ if [ "${#entries[@]}" -eq 0 ] ; then die "Download directory »${DOWNLOAD_DIR}« empty" fi -line=$(printf "%s\n" "${entries[@]}" \ +line=$(printf '%s\n' "${entries[@]}" \ | crop-first-column 55 \ | column -s $'\t' -t \ - | $ROFI_CMD "${rofi_default_args[@]}" $ROFI_ARGS) || true + | $ROFI_CMD "${rofi_default_args[@]}" "$ROFI_ARGS") || true if [ -z "$line" ]; then exit 0 fi diff --git a/misc/userscripts/password_fill b/misc/userscripts/password_fill index af394ac2c..8dba68c2b 100755 --- a/misc/userscripts/password_fill +++ b/misc/userscripts/password_fill @@ -64,7 +64,7 @@ die() { javascript_escape() { # print the first argument in an escaped way, such that it can safely # be used within javascripts double quotes - sed "s,[\\\'\"],\\\&,g" <<< "$1" + sed "s,[\\\\'\"],\\\\&,g" <<< "$1" } # ======================================================= # @@ -178,7 +178,7 @@ choose_entry_menu() { if [ "$nr" -eq 1 ] && ! ((menu_if_one_entry)) ; then file="${files[0]}" else - file=$( printf "%s\n" "${files[@]}" | "${MENU_COMMAND[@]}" ) + file=$( printf '%s\n' "${files[@]}" | "${MENU_COMMAND[@]}" ) fi } @@ -236,7 +236,7 @@ pass_backend() { if ((match_line)) ; then # add entries with matching URL-tag while read -r -d "" passfile ; do - if $GPG "${GPG_OPTS}" -d "$passfile" \ + if $GPG "${GPG_OPTS[@]}" -d "$passfile" \ | grep --max-count=1 -iE "${match_line_pattern}${url}" > /dev/null then passfile="${passfile#$PREFIX}" @@ -269,7 +269,7 @@ pass_backend() { break fi fi - done < <($GPG "${GPG_OPTS}" -d "$path" ) + done < <($GPG "${GPG_OPTS[@]}" -d "$path" ) } } # ======================================================= @@ -283,8 +283,8 @@ secret_backend() { query_entries() { local domain="$1" while read -r line ; do - if [[ "$line" =~ "attribute.username = " ]] ; then - files+=("$domain ${line#${BASH_REMATCH[0]}}") + if [[ "$line" == "attribute.username = "* ]] ; then + files+=("$domain ${line:21}") fi done < <( secret-tool search --unlock --all domain "$domain" 2>&1 ) } @@ -303,6 +303,7 @@ pass_backend QUTE_CONFIG_DIR=${QUTE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/qutebrowser/} PWFILL_CONFIG=${PWFILL_CONFIG:-${QUTE_CONFIG_DIR}/password_fill_rc} if [ -f "$PWFILL_CONFIG" ] ; then + # shellcheck source=/dev/null source "$PWFILL_CONFIG" fi init @@ -311,7 +312,7 @@ simplify_url "$QUTE_URL" query_entries "${simple_url}" no_entries_found # remove duplicates -mapfile -t files < <(printf "%s\n" "${files[@]}" | sort | uniq ) +mapfile -t files < <(printf '%s\n' "${files[@]}" | sort | uniq ) choose_entry if [ -z "$file" ] ; then # choose_entry didn't want any of these entries diff --git a/misc/userscripts/qutedmenu b/misc/userscripts/qutedmenu index 3f8b13514..de1b8d641 100755 --- a/misc/userscripts/qutedmenu +++ b/misc/userscripts/qutedmenu @@ -35,17 +35,12 @@ get_selection() { # Main # https://github.com/halfwit/dotfiles/blob/master/.config/dmenu/font -if [[ -s $confdir/dmenu/font ]]; then - read -r font < "$confdir"/dmenu/font -fi +[[ -s $confdir/dmenu/font ]] && read -r font < "$confdir"/dmenu/font -if [[ $font ]]; then - opts+=(-fn "$font") -fi +[[ $font ]] && opts+=(-fn "$font") -if [[ -s $optsfile ]]; then - source "$optsfile" -fi +# shellcheck source=/dev/null +[[ -s $optsfile ]] && source "$optsfile" url=$(get_selection) url=${url/*http/http} diff --git a/misc/userscripts/rss b/misc/userscripts/rss index 222d990a2..f8feebee7 100755 --- a/misc/userscripts/rss +++ b/misc/userscripts/rss @@ -32,7 +32,7 @@ add_feed () { if grep -Fq "$1" "feeds"; then notice "$1 is saved already." else - printf "%s\n" "$1" >> "feeds" + printf '%s\n' "$1" >> "feeds" fi } @@ -57,7 +57,7 @@ notice () { # Update a database of a feed and open new URLs read_items () { - cd read_urls + cd read_urls || return 1 feed_file="$(echo "$1" | tr -d /)" feed_temp_file="$(mktemp "$feed_file.tmp.XXXXXXXXXX")" feed_new_items="$(mktemp "$feed_file.new.XXXXXXXXXX")" @@ -75,7 +75,7 @@ read_items () { cat "$feed_new_items" >> "$feed_file" sort -o "$feed_file" "$feed_file" rm "$feed_temp_file" "$feed_new_items" - fi | while read item; do + fi | while read -r item; do echo "open -t $item" > "$QUTE_FIFO" done } @@ -85,7 +85,7 @@ if [ ! -d "$config_dir/read_urls" ]; then mkdir -p "$config_dir/read_urls" fi -cd "$config_dir" +cd "$config_dir" || exit 1 if [ $# != 0 ]; then for arg in "$@"; do @@ -115,7 +115,7 @@ if < /dev/null grep --help 2>&1 | grep -q -- -a; then text_only="-a" fi -while read feed_url; do +while read -r feed_url; do read_items "$feed_url" & done < "$config_dir/feeds" diff --git a/misc/userscripts/taskadd b/misc/userscripts/taskadd index 6add71c68..b1ded245c 100755 --- a/misc/userscripts/taskadd +++ b/misc/userscripts/taskadd @@ -25,12 +25,10 @@ [[ $QUTE_MODE == 'hints' ]] && title=$QUTE_SELECTED_TEXT || title=$QUTE_TITLE # try to add the task and grab the output -msg="$(task add $title $@ 2>&1)" - -if [[ $? == 0 ]]; then +if msg="$(task add "$title" "$*" 2>&1)"; then # annotate the new task with the url, send the output back to the browser task +LATEST annotate "$QUTE_URL" - echo "message-info '$msg'" >> $QUTE_FIFO + echo "message-info '$msg'" >> "$QUTE_FIFO" else - echo "message-error '$msg'" >> $QUTE_FIFO + echo "message-error '$msg'" >> "$QUTE_FIFO" fi diff --git a/misc/userscripts/view_in_mpv b/misc/userscripts/view_in_mpv index 9eb6ff7c6..f465fc4e4 100755 --- a/misc/userscripts/view_in_mpv +++ b/misc/userscripts/view_in_mpv @@ -50,7 +50,7 @@ msg() { MPV_COMMAND=${MPV_COMMAND:-mpv} # Warning: spaces in single flags are not supported MPV_FLAGS=${MPV_FLAGS:- --force-window --no-terminal --keep-open=yes --ytdl --ytdl-raw-options=yes-playlist=} -video_command=( "$MPV_COMMAND" $MPV_FLAGS ) +IFS=" " read -r -a video_command <<< "$MPV_COMMAND $MPV_FLAGS" js() { cat < don't filter event - # Mouse cursor hidden (overrideCursor not None) -> filter event - return qApp.overrideCursor() is not None - def eventFilter(self, obj, event): """Handle an event. diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index c8e595baf..eb0e55c4b 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -487,6 +487,7 @@ class AbstractHistory: raise NotImplementedError def back(self, count=1): + """Go back in the tab's history.""" idx = self.current_idx() - count if idx >= 0: self._go_to_item(self._item_at(idx)) @@ -495,6 +496,7 @@ class AbstractHistory: raise WebTabError("At beginning of history.") def forward(self, count=1): + """Go forward in the tab's history.""" idx = self.current_idx() + count if idx < len(self): self._go_to_item(self._item_at(idx)) @@ -704,8 +706,8 @@ class AbstractTab(QWidget): # This only gives us some mild protection against re-using events, but # it's certainly better than a segfault. if getattr(evt, 'posted', False): - raise AssertionError("Can't re-use an event which was already " - "posted!") + raise utils.Unreachable("Can't re-use an event which was already " + "posted!") recipient = self.event_target() evt.posted = True QApplication.postEvent(recipient, evt) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 0de903004..2a43a1726 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -39,7 +39,7 @@ from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate, webelem, downloads) from qutebrowser.keyinput import modeman from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils, - objreg, utils, debug, standarddir) + objreg, utils, standarddir) from qutebrowser.utils.usertypes import KeyMode from qutebrowser.misc import editor, guiprocess from qutebrowser.completion.models import urlmodel, miscmodels @@ -518,7 +518,7 @@ class CommandDispatcher: return newtab @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('index', completion=miscmodels.buffer) + @cmdutils.argument('index', completion=miscmodels.other_buffer) def tab_take(self, index): """Take a tab from another window. @@ -673,7 +673,7 @@ class CommandDispatcher: self._open(new_url, tab, bg, window, related=True) else: # pragma: no cover raise ValueError("Got called with invalid value {} for " - "`where'.".format(where)) + "`where'.".format(where)) except navigate.Error as e: raise cmdexc.CommandError(e) @@ -953,22 +953,25 @@ class CommandDispatcher: (prev and i < cur_idx) or (next_ and i > cur_idx)) - # Check to see if we are closing any pinned tabs - if not force: - for i, tab in enumerate(self._tabbed_browser.widgets()): - if _to_close(i) and tab.data.pinned: - self._tabbed_browser.tab_close_prompt_if_pinned( - tab, - force, - lambda: self.tab_only( - prev=prev, next_=next_, force=True)) - return - + # close as many tabs as we can first_tab = True + pinned_tabs_cleanup = False for i, tab in enumerate(self._tabbed_browser.widgets()): if _to_close(i): - self._tabbed_browser.close_tab(tab, new_undo=first_tab) - first_tab = False + if force or not tab.data.pinned: + self._tabbed_browser.close_tab(tab, new_undo=first_tab) + first_tab = False + else: + pinned_tabs_cleanup = tab + + # Check to see if we would like to close any pinned tabs + if pinned_tabs_cleanup: + self._tabbed_browser.tab_close_prompt_if_pinned( + pinned_tabs_cleanup, + force, + lambda: self.tab_only( + prev=prev, next_=next_, force=True), + text="Are you sure you want to close pinned tabs?") @cmdutils.register(instance='command-dispatcher', scope='window') def undo(self): @@ -1177,7 +1180,8 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window', maxsplit=0, no_replace_variables=True) - def spawn(self, cmdline, userscript=False, verbose=False, detach=False): + def spawn(self, cmdline, userscript=False, verbose=False, + output=False, detach=False): """Spawn a command in a shell. Args: @@ -1188,6 +1192,7 @@ class CommandDispatcher: (or `$XDG_DATA_DIR`) - `/usr/share/qutebrowser/userscripts` verbose: Show notifications when the command started/exited. + output: Whether the output should be shown in a new tab. detach: Whether the command should be detached from qutebrowser. cmdline: The commandline to execute. """ @@ -1214,6 +1219,11 @@ class CommandDispatcher: else: proc.start(cmd, args) + if output: + tabbed_browser = objreg.get('tabbed-browser', scope='window', + window='last-focused') + tabbed_browser.openurl(QUrl('qute://spawn-output'), newtab=True) + @cmdutils.register(instance='command-dispatcher', scope='window') def home(self): """Open main startpage in current tab.""" @@ -1418,27 +1428,14 @@ class CommandDispatcher: raise cmdexc.CommandError(e) @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('dest_old', hide=True) - def download(self, url=None, dest_old=None, *, mhtml_=False, dest=None): + def download(self, url=None, *, mhtml_=False, dest=None): """Download a given URL, or current page if no URL given. - The form `:download [url] [dest]` is deprecated, use `:download --dest - [dest] [url]` instead. - Args: url: The URL to download. If not given, download the current page. - dest_old: (deprecated) Same as dest. dest: The file path to write the download to, or None to ask. mhtml_: Download the current page and all assets as mhtml file. """ - if dest_old is not None: - message.warning(":download [url] [dest] is deprecated - use " - ":download --dest [dest] [url]") - if dest is not None: - raise cmdexc.CommandError("Can't give two destinations for the" - " download.") - dest = dest_old - # FIXME:qtwebengine do this with the QtWebEngine download manager? download_manager = objreg.get('qtnetwork-download-manager', scope='window', window=self._win_id) @@ -1528,6 +1525,7 @@ class CommandDispatcher: dest = os.path.expanduser(dest) def callback(data): + """Write the data to disk.""" try: with open(dest, 'w', encoding='utf-8') as f: f.write(data) @@ -1649,6 +1647,8 @@ class CommandDispatcher: except webelem.Error as e: raise cmdexc.CommandError(str(e)) + mainwindow.raise_window(objreg.last_focused_window(), alert=False) + @cmdutils.register(instance='command-dispatcher', maxsplit=0, scope='window') def insert_text(self, text): @@ -1742,7 +1742,8 @@ class CommandDispatcher: elif going_up and tab.scroller.pos_px().y() > old_scroll_pos.y(): message.info("Search hit TOP, continuing at BOTTOM") else: - message.warning("Text '{}' not found on page!".format(text)) + message.warning("Text '{}' not found on page!".format(text), + replace=True) @cmdutils.register(instance='command-dispatcher', scope='window', maxsplit=0) @@ -1762,7 +1763,7 @@ class CommandDispatcher: return options = { - 'ignore_case': config.val.ignore_case, + 'ignore_case': config.val.search.ignore_case, 'reverse': reverse, } @@ -2045,6 +2046,7 @@ class CommandDispatcher: jseval_cb = None else: def jseval_cb(out): + """Show the data returned from JS.""" if out is None: # Getting the actual error (if any) seems to be difficult. # The error does end up in @@ -2197,11 +2199,4 @@ class CommandDispatcher: return window = self._tabbed_browser.window() - if window.isFullScreen(): - window.setWindowState( - window.state_before_fullscreen & ~Qt.WindowFullScreen) - else: - window.state_before_fullscreen = window.windowState() - window.showFullScreen() - log.misc.debug('state before fullscreen: {}'.format( - debug.qflags_key(Qt, window.state_before_fullscreen))) + window.setWindowState(window.windowState() ^ Qt.WindowFullScreen) diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index 5a2daae7e..c064d700e 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -103,6 +103,8 @@ def immediate_download_path(prompt_download_directory=None): if not prompt_download_directory: return download_dir() + return None + def _path_suggestion(filename): """Get the suggested file path. @@ -180,7 +182,7 @@ def transform_path(path): path = utils.expand_windows_drive(path) # Drive dependent working directories are not supported, e.g. # E:filename is invalid - if re.match(r'[A-Z]:[^\\]', path, re.IGNORECASE): + if re.search(r'^[A-Z]:[^\\]', path, re.IGNORECASE): return None # Paths like COM1, ... # See https://github.com/qutebrowser/qutebrowser/issues/82 @@ -990,7 +992,7 @@ class DownloadModel(QAbstractListModel): if not count: count = len(self) raise cmdexc.CommandError("Download {} is already done!" - .format(count)) + .format(count)) download.cancel() @cmdutils.register(instance='download-model', scope='window') diff --git a/qutebrowser/browser/greasemonkey.py b/qutebrowser/browser/greasemonkey.py new file mode 100644 index 000000000..9a82d6a93 --- /dev/null +++ b/qutebrowser/browser/greasemonkey.py @@ -0,0 +1,224 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2017 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 . + +"""Load, parse and make available Greasemonkey scripts.""" + +import re +import os +import json +import fnmatch +import functools +import glob + +import attr +from PyQt5.QtCore import pyqtSignal, QObject, QUrl + +from qutebrowser.utils import log, standarddir, jinja, objreg +from qutebrowser.commands import cmdutils + + +def _scripts_dir(): + """Get the directory of the scripts.""" + return os.path.join(standarddir.data(), 'greasemonkey') + + +class GreasemonkeyScript: + + """Container class for userscripts, parses metadata blocks.""" + + def __init__(self, properties, code): + self._code = code + self.includes = [] + self.excludes = [] + self.description = None + self.name = None + self.namespace = None + self.run_at = None + self.script_meta = None + self.runs_on_sub_frames = True + for name, value in properties: + if name == 'name': + self.name = value + elif name == 'namespace': + self.namespace = value + elif name == 'description': + self.description = value + elif name in ['include', 'match']: + self.includes.append(value) + elif name in ['exclude', 'exclude_match']: + self.excludes.append(value) + elif name == 'run-at': + self.run_at = value + elif name == 'noframes': + self.runs_on_sub_frames = False + + HEADER_REGEX = r'// ==UserScript==|\n+// ==/UserScript==\n' + PROPS_REGEX = r'// @(?P[^\s]+)\s*(?P.*)' + + @classmethod + def parse(cls, source): + """GreasemonkeyScript factory. + + Takes a userscript source and returns a GreasemonkeyScript. + Parses the Greasemonkey metadata block, if present, to fill out + attributes. + """ + matches = re.split(cls.HEADER_REGEX, source, maxsplit=2) + try: + _head, props, _code = matches + except ValueError: + props = "" + script = cls(re.findall(cls.PROPS_REGEX, props), source) + script.script_meta = props + if not props: + script.includes = ['*'] + return script + + def code(self): + """Return the processed JavaScript code of this script. + + Adorns the source code with GM_* methods for Greasemonkey + compatibility and wraps it in an IFFE to hide it within a + lexical scope. Note that this means line numbers in your + browser's debugger/inspector will not match up to the line + numbers in the source script directly. + """ + return jinja.js_environment.get_template( + 'greasemonkey_wrapper.js').render( + scriptName="/".join([self.namespace or '', self.name]), + scriptInfo=self._meta_json(), + scriptMeta=self.script_meta, + scriptSource=self._code) + + def _meta_json(self): + return json.dumps({ + 'name': self.name, + 'description': self.description, + 'matches': self.includes, + 'includes': self.includes, + 'excludes': self.excludes, + 'run-at': self.run_at, + }) + + +@attr.s +class MatchingScripts(object): + + """All userscripts registered to run on a particular url.""" + + url = attr.ib() + start = attr.ib(default=attr.Factory(list)) + end = attr.ib(default=attr.Factory(list)) + idle = attr.ib(default=attr.Factory(list)) + + +class GreasemonkeyManager(QObject): + + """Manager of userscripts and a Greasemonkey compatible environment. + + Signals: + scripts_reloaded: Emitted when scripts are reloaded from disk. + Any cached or already-injected scripts should be + considered obselete. + """ + + scripts_reloaded = pyqtSignal() + # https://wiki.greasespot.net/Include_and_exclude_rules#Greaseable_schemes + # Limit the schemes scripts can run on due to unreasonable levels of + # exploitability + greaseable_schemes = ['http', 'https', 'ftp', 'file'] + + def __init__(self, parent=None): + super().__init__(parent) + self.load_scripts() + + @cmdutils.register(name='greasemonkey-reload', + instance='greasemonkey') + def load_scripts(self): + """Re-read Greasemonkey scripts from disk. + + The scripts are read from a 'greasemonkey' subdirectory in + qutebrowser's data directory (see `:version`). + """ + self._run_start = [] + self._run_end = [] + self._run_idle = [] + + scripts_dir = os.path.abspath(_scripts_dir()) + log.greasemonkey.debug("Reading scripts from: {}".format(scripts_dir)) + for script_filename in glob.glob(os.path.join(scripts_dir, '*.js')): + if not os.path.isfile(script_filename): + continue + script_path = os.path.join(scripts_dir, script_filename) + with open(script_path, encoding='utf-8') as script_file: + script = GreasemonkeyScript.parse(script_file.read()) + if not script.name: + script.name = script_filename + + if script.run_at == 'document-start': + self._run_start.append(script) + elif script.run_at == 'document-end': + self._run_end.append(script) + elif script.run_at == 'document-idle': + self._run_idle.append(script) + else: + log.greasemonkey.warning("Script {} has invalid run-at " + "defined, defaulting to " + "document-end" + .format(script_path)) + # Default as per + # https://wiki.greasespot.net/Metadata_Block#.40run-at + self._run_end.append(script) + log.greasemonkey.debug("Loaded script: {}".format(script.name)) + self.scripts_reloaded.emit() + + def scripts_for(self, url): + """Fetch scripts that are registered to run for url. + + returns a tuple of lists of scripts meant to run at (document-start, + document-end, document-idle) + """ + if url.scheme() not in self.greaseable_schemes: + return MatchingScripts(url, [], [], []) + match = functools.partial(fnmatch.fnmatch, + url.toString(QUrl.FullyEncoded)) + tester = (lambda script: + any(match(pat) for pat in script.includes) and + not any(match(pat) for pat in script.excludes)) + return MatchingScripts( + url, + [script for script in self._run_start if tester(script)], + [script for script in self._run_end if tester(script)], + [script for script in self._run_idle if tester(script)] + ) + + def all_scripts(self): + """Return all scripts found in the configured script directory.""" + return self._run_start + self._run_end + self._run_idle + + +def init(): + """Initialize Greasemonkey support.""" + gm_manager = GreasemonkeyManager() + objreg.register('greasemonkey', gm_manager) + + try: + os.mkdir(_scripts_dir()) + except FileExistsError: + pass diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 6d40f531a..14cc2b574 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -390,10 +390,8 @@ class HintManager(QObject): def _cleanup(self): """Clean up after hinting.""" - # pylint: disable=not-an-iterable for label in self._context.all_labels: label.cleanup() - # pylint: enable=not-an-iterable text = self._get_text() message_bridge = objreg.get('message-bridge', scope='window', @@ -621,8 +619,9 @@ class HintManager(QObject): @cmdutils.register(instance='hintmanager', scope='tab', name='hint', star_args_optional=True, maxsplit=2) @cmdutils.argument('win_id', win_id=True) - def start(self, rapid=False, group=webelem.Group.all, target=Target.normal, - *args, win_id, mode=None, add_history=False): + def start(self, # pylint: disable=keyword-arg-before-vararg + group=webelem.Group.all, target=Target.normal, + *args, win_id, mode=None, add_history=False, rapid=False): """Start hinting. Args: @@ -809,7 +808,6 @@ class HintManager(QObject): log.hints.debug("Filtering hints on {!r}".format(filterstr)) visible = [] - # pylint: disable=not-an-iterable for label in self._context.all_labels: try: if self._filter_matches(filterstr, str(label.elem)): @@ -821,7 +819,6 @@ class HintManager(QObject): label.hide() except webelem.Error: pass - # pylint: enable=not-an-iterable if not visible: # Whoops, filtered all hints diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py index 1901548bf..04bc1be15 100644 --- a/qutebrowser/browser/history.py +++ b/qutebrowser/browser/history.py @@ -32,7 +32,7 @@ from qutebrowser.misc import objects, sql # increment to indicate that HistoryCompletion must be regenerated -_USER_VERSION = 1 +_USER_VERSION = 2 class CompletionHistory(sql.SqlTable): @@ -102,7 +102,8 @@ class WebHistory(sql.SqlTable): data = {'url': [], 'title': [], 'last_atime': []} # select the latest entry for each url q = sql.Query('SELECT url, title, max(atime) AS atime FROM History ' - 'WHERE NOT redirect GROUP BY url ORDER BY atime asc') + 'WHERE NOT redirect and url NOT LIKE "qute://back%" ' + 'GROUP BY url ORDER BY atime asc') for entry in q.run(): data['url'].append(self._format_completion_url(QUrl(entry.url))) data['title'].append(entry.title) @@ -149,8 +150,8 @@ class WebHistory(sql.SqlTable): if force: self._do_clear() else: - message.confirm_async(self._do_clear, title="Clear all browsing " - "history?") + message.confirm_async(yes_action=self._do_clear, + title="Clear all browsing history?") def _do_clear(self): with self._handle_sql_errors(): @@ -171,7 +172,9 @@ class WebHistory(sql.SqlTable): @pyqtSlot(QUrl, QUrl, str) def add_from_tab(self, url, requested_url, title): """Add a new history entry as slot, called from a BrowserTab.""" - if url.scheme() == 'data' or requested_url.scheme() == 'data': + if any(url.scheme() == 'data' or + (url.scheme(), url.host()) == ('qute', 'back') + for url in (url, requested_url)): return if url.isEmpty(): # things set via setHtml @@ -268,6 +271,7 @@ class WebHistory(sql.SqlTable): return def action(): + """Actually run the import.""" with debug.log_time(log.init, 'Import old history file to sqlite'): try: self._read(path) @@ -340,7 +344,7 @@ class WebHistory(sql.SqlTable): f.write('\n'.join(lines)) message.info("Dumped history to {}".format(dest)) except OSError as e: - raise cmdexc.CommandError('Could not write history: {}', e) + raise cmdexc.CommandError('Could not write history: {}'.format(e)) def init(parent=None): diff --git a/qutebrowser/browser/inspector.py b/qutebrowser/browser/inspector.py index c88242bb8..9c583f4b3 100644 --- a/qutebrowser/browser/inspector.py +++ b/qutebrowser/browser/inspector.py @@ -94,6 +94,7 @@ class AbstractWebInspector(QWidget): raise NotImplementedError def toggle(self, page): + """Show/hide the inspector.""" if self._widget.isVisible(): self.hide() else: diff --git a/qutebrowser/browser/mouse.py b/qutebrowser/browser/mouse.py index 619f75120..d08f191a8 100644 --- a/qutebrowser/browser/mouse.py +++ b/qutebrowser/browser/mouse.py @@ -19,15 +19,13 @@ """Mouse handling for a browser tab.""" +from PyQt5.QtCore import QObject, QEvent, Qt, QTimer from qutebrowser.config import config from qutebrowser.utils import message, log, usertypes from qutebrowser.keyinput import modeman -from PyQt5.QtCore import QObject, QEvent, Qt, QTimer - - class ChildEventFilter(QObject): """An event filter re-adding MouseEventFilter on ChildEvent. diff --git a/qutebrowser/browser/network/pac.py b/qutebrowser/browser/network/pac.py index 69158fe62..a3f0813c8 100644 --- a/qutebrowser/browser/network/pac.py +++ b/qutebrowser/browser/network/pac.py @@ -59,6 +59,7 @@ def _js_slot(*args): def _decorator(method): @functools.wraps(method) def new_method(self, *args, **kwargs): + """Call the underlying function.""" try: return method(self, *args, **kwargs) except: diff --git a/qutebrowser/browser/pdfjs.py b/qutebrowser/browser/pdfjs.py index d003cefb1..cd5bcac0a 100644 --- a/qutebrowser/browser/pdfjs.py +++ b/qutebrowser/browser/pdfjs.py @@ -82,7 +82,7 @@ def fix_urls(asset): ('viewer.css', 'qute://pdfjs/web/viewer.css'), ('compatibility.js', 'qute://pdfjs/web/compatibility.js'), ('locale/locale.properties', - 'qute://pdfjs/web/locale/locale.properties'), + 'qute://pdfjs/web/locale/locale.properties'), ('l10n.js', 'qute://pdfjs/web/l10n.js'), ('../build/pdf.js', 'qute://pdfjs/build/pdf.js'), ('debugger.js', 'qute://pdfjs/web/debugger.js'), diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py index 0e079b633..378bc72b5 100644 --- a/qutebrowser/browser/qtnetworkdownloads.py +++ b/qutebrowser/browser/qtnetworkdownloads.py @@ -303,8 +303,7 @@ class DownloadItem(downloads.AbstractDownloadItem): """Handle QNetworkReply errors.""" if code == QNetworkReply.OperationCanceledError: return - else: - self._die(self._reply.errorString()) + self._die(self._reply.errorString()) @pyqtSlot() def _on_read_timer_timeout(self): @@ -399,7 +398,7 @@ class DownloadManager(downloads.AbstractDownloadManager): """ if not url.isValid(): urlutils.invalid_url_error(url, "start download") - return + return None req = QNetworkRequest(url) if user_agent is not None: req.setHeader(QNetworkRequest.UserAgentHeader, user_agent) diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index 3fb6459a5..8bcb7ff37 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -29,6 +29,7 @@ import os import time import textwrap import mimetypes +import urllib import pkg_resources from PyQt5.QtCore import QUrlQuery, QUrl @@ -41,6 +42,7 @@ from qutebrowser.misc import objects pyeval_output = ":pyeval was never called" +spawn_output = ":spawn was never called" _HANDLERS = {} @@ -111,6 +113,7 @@ class add_handler: # noqa: N801,N806 pylint: disable=invalid-name return function def wrapper(self, *args, **kwargs): + """Call the underlying function.""" if self._backend is not None and objects.backend != self._backend: return self.wrong_backend_handler(*args, **kwargs) else: @@ -136,7 +139,7 @@ def data_for_url(url): A (mimetype, data) tuple. """ norm_url = url.adjusted(QUrl.NormalizePathSegments | - QUrl.StripTrailingSlash) + QUrl.StripTrailingSlash) if norm_url != url: raise Redirect(norm_url) @@ -267,6 +270,13 @@ def qute_pyeval(_url): return 'text/html', html +@add_handler('spawn-output') +def qute_spawn_output(_url): + """Handler for qute://spawn-output.""" + html = jinja.render('pre.html', title='spawn output', content=spawn_output) + return 'text/html', html + + @add_handler('version') @add_handler('verizon') def qute_version(_url): @@ -425,6 +435,18 @@ def qute_settings(url): return 'text/html', html +@add_handler('back') +def qute_back(url): + """Handler for qute://back. + + Simple page to free ram / lazy load a site, goes back on focusing the tab. + """ + html = jinja.render( + 'back.html', + title='Suspended: ' + urllib.parse.unquote(url.fragment())) + return 'text/html', html + + @add_handler('configdiff') def qute_configdiff(url): """Handler for qute://configdiff.""" @@ -433,7 +455,7 @@ def qute_configdiff(url): return 'text/html', configdiff.get_diff() except OSError as e: error = (b'Failed to read old config: ' + - str(e.strerror).encode('utf-8')) + str(e.strerror).encode('utf-8')) return 'text/plain', error else: data = config.instance.dump_userconfig().encode('utf-8') diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py index ab1b6ad9f..b6bfefe7b 100644 --- a/qutebrowser/browser/shared.py +++ b/qutebrowser/browser/shared.py @@ -22,7 +22,7 @@ import html from qutebrowser.config import config -from qutebrowser.utils import usertypes, message, log, objreg, jinja +from qutebrowser.utils import usertypes, message, log, objreg, jinja, utils from qutebrowser.mainwindow import mainwindow @@ -182,7 +182,7 @@ def ignore_certificate_errors(url, errors, abort_on): return False else: raise ValueError("Invalid ssl_strict value {!r}".format(ssl_strict)) - raise AssertionError("Not reached") + raise utils.Unreachable def feature_permission(url, option, msg, yes_action, no_action, abort_on): diff --git a/qutebrowser/browser/urlmarks.py b/qutebrowser/browser/urlmarks.py index b7c93a994..5e2c60dfb 100644 --- a/qutebrowser/browser/urlmarks.py +++ b/qutebrowser/browser/urlmarks.py @@ -26,8 +26,8 @@ to a file on shutdown, so it makes sense to keep them as strings here. """ import os -import html import os.path +import html import functools import collections diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py index 6030f2708..30355ad1a 100644 --- a/qutebrowser/browser/webelem.py +++ b/qutebrowser/browser/webelem.py @@ -42,7 +42,9 @@ Group = enum.Enum('Group', ['all', 'links', 'images', 'url', 'inputs']) SELECTORS = { Group.all: ('a, area, textarea, select, input:not([type=hidden]), button, ' 'frame, iframe, link, [onclick], [onmousedown], [role=link], ' - '[role=option], [role=button], img'), + '[role=option], [role=button], img, ' + # Angular 1 selectors + '[ng-click], [ngClick], [data-ng-click], [x-ng-click]'), Group.links: 'a[href], area[href], link[href], [role=link][href]', Group.images: 'img', Group.url: '[src], [href]', @@ -60,7 +62,7 @@ class Error(Exception): pass -class OrphanedError(Exception): +class OrphanedError(Error): """Raised when a webelement's parent has vanished.""" diff --git a/qutebrowser/browser/webengine/spell.py b/qutebrowser/browser/webengine/spell.py index ee2fb7813..9166180d4 100644 --- a/qutebrowser/browser/webengine/spell.py +++ b/qutebrowser/browser/webengine/spell.py @@ -30,7 +30,7 @@ from qutebrowser.utils import log def version(filename): """Extract the version number from the dictionary file name.""" version_re = re.compile(r".+-(?P[0-9]+-[0-9]+?)\.bdic") - match = version_re.match(filename) + match = version_re.fullmatch(filename) if match is None: raise ValueError('the given dictionary file name is malformed: {}' .format(filename)) diff --git a/qutebrowser/browser/webengine/webengineelem.py b/qutebrowser/browser/webengine/webengineelem.py index 0e54ca80e..d6d74ebe4 100644 --- a/qutebrowser/browser/webengine/webengineelem.py +++ b/qutebrowser/browser/webengine/webengineelem.py @@ -211,11 +211,11 @@ class WebEngineElement(webelem.AbstractWebElement): def _click_js(self, _click_target): # FIXME:qtwebengine Have a proper API for this # pylint: disable=protected-access - settings = self._tab._widget.settings() + view = self._tab._widget # pylint: enable=protected-access attribute = QWebEngineSettings.JavascriptCanOpenWindows - could_open_windows = settings.testAttribute(attribute) - settings.setAttribute(attribute, True) + could_open_windows = view.settings().testAttribute(attribute) + view.settings().setAttribute(attribute, True) # Get QtWebEngine do apply the settings # (it does so with a 0ms QTimer...) @@ -226,6 +226,12 @@ class WebEngineElement(webelem.AbstractWebElement): QEventLoop.ExcludeUserInputEvents) def reset_setting(_arg): - settings.setAttribute(attribute, could_open_windows) + """Set the JavascriptCanOpenWindows setting to its old value.""" + try: + view.settings().setAttribute(attribute, could_open_windows) + except RuntimeError: + # Happens if this callback gets called during QWebEnginePage + # destruction, i.e. if the tab was closed in the meantime. + pass self._js_call('click', callback=reset_setting) diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index 4bf525c46..f8b54e065 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -244,6 +244,43 @@ def _init_profiles(): private_profile.setSpellCheckEnabled(True) +def inject_userscripts(): + """Register user JavaScript files with the global profiles.""" + # The Greasemonkey metadata block support in QtWebEngine only starts at + # Qt 5.8. With 5.7.1, we need to inject the scripts ourselves in response + # to urlChanged. + if not qtutils.version_check('5.8'): + return + + # Since we are inserting scripts into profile.scripts they won't + # just get replaced by new gm scripts like if we were injecting them + # ourselves so we need to remove all gm scripts, while not removing + # any other stuff that might have been added. Like the one for + # stylesheets. + greasemonkey = objreg.get('greasemonkey') + for profile in [default_profile, private_profile]: + scripts = profile.scripts() + for script in scripts.toList(): + if script.name().startswith("GM-"): + log.greasemonkey.debug('Removing script: {}' + .format(script.name())) + removed = scripts.remove(script) + assert removed, script.name() + + # Then add the new scripts. + for script in greasemonkey.all_scripts(): + # @run-at (and @include/@exclude/@match) is parsed by + # QWebEngineScript. + new_script = QWebEngineScript() + new_script.setWorldId(QWebEngineScript.MainWorld) + new_script.setSourceCode(script.code()) + new_script.setName("GM-{}".format(script.name)) + new_script.setRunsOnSubFrames(script.runs_on_sub_frames) + log.greasemonkey.debug('adding script: {}' + .format(new_script.name())) + scripts.insert(new_script) + + def init(args): """Initialize the global QWebSettings.""" if args.enable_webengine_inspector: diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 89ba958a7..9328698bc 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -24,7 +24,8 @@ import functools import html as html_utils import sip -from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QPoint, QPointF, QUrl, QTimer +from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QEvent, QPoint, QPointF, + QUrl, QTimer) from PyQt5.QtGui import QKeyEvent from PyQt5.QtNetwork import QAuthenticator from PyQt5.QtWidgets import QApplication @@ -69,6 +70,10 @@ def init(): download_manager.install(webenginesettings.private_profile) objreg.register('webengine-download-manager', download_manager) + greasemonkey = objreg.get('greasemonkey') + greasemonkey.scripts_reloaded.connect(webenginesettings.inject_userscripts) + webenginesettings.inject_userscripts() + # Mapping worlds from usertypes.JsWorld to QWebEngineScript world IDs. _JS_WORLD_MAP = { @@ -121,18 +126,35 @@ class WebEnginePrinting(browsertab.AbstractPrinting): class WebEngineSearch(browsertab.AbstractSearch): - """QtWebEngine implementations related to searching on the page.""" + """QtWebEngine implementations related to searching on the page. + + Attributes: + _flags: The QWebEnginePage.FindFlags of the last search. + _pending_searches: How many searches have been started but not called + back yet. + """ def __init__(self, parent=None): super().__init__(parent) self._flags = QWebEnginePage.FindFlags(0) + self._pending_searches = 0 def _find(self, text, flags, callback, caller): """Call findText on the widget.""" self.search_displayed = True + self._pending_searches += 1 def wrapped_callback(found): """Wrap the callback to do debug logging.""" + self._pending_searches -= 1 + if self._pending_searches > 0: + # See https://github.com/qutebrowser/qutebrowser/issues/2442 + # and https://github.com/qt/qtwebengine/blob/5.10/src/core/web_contents_adapter.cpp#L924-L934 + log.webview.debug("Ignoring cancelled search callback with " + "{} pending searches".format( + self._pending_searches)) + return + found_text = 'found' if found else "didn't find" if flags: flag_text = 'with flags {}'.format(debug.qflags_key( @@ -518,7 +540,15 @@ class WebEngineElements(browsertab.AbstractElements): class WebEngineTab(browsertab.AbstractTab): - """A QtWebEngine tab in the browser.""" + """A QtWebEngine tab in the browser. + + Signals: + _load_finished_fake: + Used in place of unreliable loadFinished + """ + + # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-65223 + _load_finished_fake = pyqtSignal(bool) def __init__(self, *, win_id, mode_manager, private, parent=None): super().__init__(win_id=win_id, mode_manager=mode_manager, @@ -772,6 +802,24 @@ class WebEngineTab(browsertab.AbstractTab): } self.renderer_process_terminated.emit(status_map[status], exitcode) + @pyqtSlot(int) + def _on_load_progress_workaround(self, perc): + """Use loadProgress(100) to emit loadFinished(True). + + See https://bugreports.qt.io/browse/QTBUG-65223 + """ + if perc == 100 and self.load_status() != usertypes.LoadStatus.error: + self._load_finished_fake.emit(True) + + @pyqtSlot(bool) + def _on_load_finished_workaround(self, ok): + """Use only loadFinished(False). + + See https://bugreports.qt.io/browse/QTBUG-65223 + """ + if not ok: + self._load_finished_fake.emit(False) + def _connect_signals(self): view = self._widget page = view.page() @@ -780,9 +828,6 @@ class WebEngineTab(browsertab.AbstractTab): page.linkHovered.connect(self.link_hovered) page.loadProgress.connect(self._on_load_progress) page.loadStarted.connect(self._on_load_started) - page.loadFinished.connect(self._on_history_trigger) - page.loadFinished.connect(self._restore_zoom) - page.loadFinished.connect(self._on_load_finished) page.certificate_error.connect(self._on_ssl_errors) page.authenticationRequired.connect(self._on_authentication_required) page.proxyAuthenticationRequired.connect( @@ -795,6 +840,19 @@ class WebEngineTab(browsertab.AbstractTab): view.renderProcessTerminated.connect( self._on_render_process_terminated) view.iconChanged.connect(self.icon_changed) + # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-65223 + if qtutils.version_check('5.10', compiled=False): + page.loadProgress.connect(self._on_load_progress_workaround) + self._load_finished_fake.connect(self._on_history_trigger) + self._load_finished_fake.connect(self._restore_zoom) + self._load_finished_fake.connect(self._on_load_finished) + page.loadFinished.connect(self._on_load_finished_workaround) + else: + # for older Qt versions which break with the above + page.loadProgress.connect(self._on_load_progress) + page.loadFinished.connect(self._on_history_trigger) + page.loadFinished.connect(self._restore_zoom) + page.loadFinished.connect(self._on_load_finished) def event_target(self): return self._widget.focusProxy() diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py index 56bd1eb5a..b313fc36c 100644 --- a/qutebrowser/browser/webengine/webview.py +++ b/qutebrowser/browser/webengine/webview.py @@ -23,12 +23,14 @@ import functools from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, PYQT_VERSION from PyQt5.QtGui import QPalette -from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage +from PyQt5.QtWebEngineWidgets import (QWebEngineView, QWebEnginePage, + QWebEngineScript) from qutebrowser.browser import shared from qutebrowser.browser.webengine import certificateerror, webenginesettings from qutebrowser.config import config -from qutebrowser.utils import log, debug, usertypes, jinja, urlutils, message +from qutebrowser.utils import (log, debug, usertypes, jinja, urlutils, message, + objreg, qtutils) class WebEngineView(QWebEngineView): @@ -135,6 +137,7 @@ class WebEnginePage(QWebEnginePage): self._theme_color = theme_color self._set_bg_color() config.instance.changed.connect(self._set_bg_color) + self.urlChanged.connect(self._inject_userjs) @config.change_filter('colors.webpage.bg') def _set_bg_color(self): @@ -300,3 +303,43 @@ class WebEnginePage(QWebEnginePage): message.error(msg) return False return True + + @pyqtSlot('QUrl') + def _inject_userjs(self, url): + """Inject userscripts registered for `url` into the current page.""" + if qtutils.version_check('5.8'): + # Handled in webenginetab with the builtin Greasemonkey + # support. + return + + # Using QWebEnginePage.scripts() to hold the user scripts means + # we don't have to worry ourselves about where to inject the + # page but also means scripts hang around for the tab lifecycle. + # So clear them here. + scripts = self.scripts() + for script in scripts.toList(): + if script.name().startswith("GM-"): + log.greasemonkey.debug("Removing script: {}" + .format(script.name())) + removed = scripts.remove(script) + assert removed, script.name() + + def _add_script(script, injection_point): + new_script = QWebEngineScript() + new_script.setInjectionPoint(injection_point) + new_script.setWorldId(QWebEngineScript.MainWorld) + new_script.setSourceCode(script.code()) + new_script.setName("GM-{}".format(script.name)) + new_script.setRunsOnSubFrames(script.runs_on_sub_frames) + log.greasemonkey.debug("Adding script: {}" + .format(new_script.name())) + scripts.insert(new_script) + + greasemonkey = objreg.get('greasemonkey') + matching_scripts = greasemonkey.scripts_for(url) + for script in matching_scripts.start: + _add_script(script, QWebEngineScript.DocumentCreation) + for script in matching_scripts.end: + _add_script(script, QWebEngineScript.DocumentReady) + for script in matching_scripts.idle: + _add_script(script, QWebEngineScript.Deferred) diff --git a/qutebrowser/browser/webkit/http.py b/qutebrowser/browser/webkit/http.py index 08cad7a44..8997f4b0c 100644 --- a/qutebrowser/browser/webkit/http.py +++ b/qutebrowser/browser/webkit/http.py @@ -22,11 +22,11 @@ import os.path +from PyQt5.QtNetwork import QNetworkRequest + from qutebrowser.utils import log from qutebrowser.browser.webkit import rfc6266 -from PyQt5.QtNetwork import QNetworkRequest - def parse_content_disposition(reply): """Parse a content_disposition header. @@ -57,9 +57,7 @@ def parse_content_disposition(reply): is_inline = content_disposition.is_inline() # Then try to get filename from url if not filename: - path = reply.url().path() - if path is not None: - filename = path.rstrip('/') + filename = reply.url().path().rstrip('/') # If that fails as well, use a fallback if not filename: filename = 'qutebrowser-download' diff --git a/qutebrowser/browser/webkit/network/filescheme.py b/qutebrowser/browser/webkit/network/filescheme.py index a8cade1db..a971e3257 100644 --- a/qutebrowser/browser/webkit/network/filescheme.py +++ b/qutebrowser/browser/webkit/network/filescheme.py @@ -132,5 +132,6 @@ class FileSchemeHandler(schemehandler.SchemeHandler): data = dirbrowser_html(path) return networkreply.FixedDataNetworkReply( request, data, 'text/html', self.parent()) + return None except UnicodeEncodeError: return None diff --git a/qutebrowser/browser/webkit/rfc6266.py b/qutebrowser/browser/webkit/rfc6266.py index 1f71b23e5..f83413ee2 100644 --- a/qutebrowser/browser/webkit/rfc6266.py +++ b/qutebrowser/browser/webkit/rfc6266.py @@ -270,6 +270,7 @@ class _ContentDisposition: elif 'filename' in self.assocs: # XXX Reject non-ascii (parsed via qdtext) here? return self.assocs['filename'] + return None def is_inline(self): """Return if the file should be handled inline. diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index 43bec3456..6b8f067fd 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -19,6 +19,7 @@ """Wrapper over our (QtWebKit) WebView.""" +import re import functools import xml.etree.ElementTree @@ -541,10 +542,15 @@ class WebKitElements(browsertab.AbstractElements): def find_id(self, elem_id, callback): def find_id_cb(elems): + """Call the real callback with the found elements.""" if not elems: callback(None) else: callback(elems[0]) + + # Escape non-alphanumeric characters in the selector + # https://www.w3.org/TR/CSS2/syndata.html#value-def-identifier + elem_id = re.sub(r'[^a-zA-Z0-9_-]', r'\\\g<0>', elem_id) self.find_css('#' + elem_id, find_id_cb) def find_focused(self, callback): diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index 7e1d991b9..89407fcdf 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -86,6 +86,21 @@ class BrowserPage(QWebPage): self.on_save_frame_state_requested) self.restoreFrameStateRequested.connect( self.on_restore_frame_state_requested) + self.loadFinished.connect( + functools.partial(self._inject_userjs, self.mainFrame())) + self.frameCreated.connect(self._connect_userjs_signals) + + @pyqtSlot('QWebFrame*') + def _connect_userjs_signals(self, frame): + """Connect userjs related signals to `frame`. + + Connect the signals used as triggers for injecting user + JavaScripts into the passed QWebFrame. + """ + log.greasemonkey.debug("Connecting to frame {} ({})" + .format(frame, frame.url().toDisplayString())) + frame.loadFinished.connect( + functools.partial(self._inject_userjs, frame)) def javaScriptPrompt(self, frame, js_msg, default): """Override javaScriptPrompt to use qutebrowser prompts.""" @@ -283,6 +298,38 @@ class BrowserPage(QWebPage): else: self.error_occurred = False + def _inject_userjs(self, frame): + """Inject user JavaScripts into the page. + + Args: + frame: The QWebFrame to inject the user scripts into. + """ + url = frame.url() + if url.isEmpty(): + url = frame.requestedUrl() + + log.greasemonkey.debug("_inject_userjs called for {} ({})" + .format(frame, url.toDisplayString())) + + greasemonkey = objreg.get('greasemonkey') + scripts = greasemonkey.scripts_for(url) + # QtWebKit has trouble providing us with signals representing + # page load progress at reasonable times, so we just load all + # scripts on the same event. + toload = scripts.start + scripts.end + scripts.idle + + if url.isEmpty(): + # This happens during normal usage like with view source but may + # also indicate a bug. + log.greasemonkey.debug("Not running scripts for frame with no " + "url: {}".format(frame)) + assert not toload, toload + + for script in toload: + if frame is self.mainFrame() or script.runs_on_sub_frames: + log.webview.debug('Running GM script: {}'.format(script.name)) + frame.evaluateJavaScript(script.code()) + @pyqtSlot('QWebFrame*', 'QWebPage::Feature') def _on_feature_permission_requested(self, frame, feature): """Ask the user for approval for geolocation/notifications.""" diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index afb6253db..b4ff1cde8 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -190,6 +190,7 @@ class Command: return True elif arg_info.win_id: return True + return False def _inspect_func(self): """Inspect the function to get useful informations from it. diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index 095b49108..890a0275e 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -126,7 +126,7 @@ class CommandParser: new_cmd += ' ' return new_cmd - def _parse_all_gen(self, text, aliases=True, *args, **kwargs): + def _parse_all_gen(self, text, *args, aliases=True, **kwargs): """Split a command on ;; and parse all parts. If the first command in the commandline is a non-split one, it only diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py index bc0e4991f..4e187750d 100644 --- a/qutebrowser/completion/completer.py +++ b/qutebrowser/completion/completer.py @@ -35,6 +35,7 @@ class CompletionInfo: config = attr.ib() keyconf = attr.ib() + win_id = attr.ib() class Completer(QObject): @@ -43,6 +44,7 @@ class Completer(QObject): Attributes: _cmd: The statusbar Command object this completer belongs to. + _win_id: The id of the window that owns this object. _timer: The timer used to trigger the completion update. _last_cursor_pos: The old cursor position so we avoid double completion updates. @@ -50,9 +52,10 @@ class Completer(QObject): _last_completion_func: The completion function used for the last text. """ - def __init__(self, cmd, parent=None): + def __init__(self, *, cmd, win_id, parent=None): super().__init__(parent) self._cmd = cmd + self._win_id = win_id self._timer = QTimer() self._timer.setSingleShot(True) self._timer.setInterval(0) @@ -84,8 +87,6 @@ class Completer(QObject): # cursor on a flag or after an explicit split (--) return None log.completion.debug("Before removing flags: {}".format(before_cursor)) - before_cursor = [x for x in before_cursor if not x.startswith('-')] - log.completion.debug("After removing flags: {}".format(before_cursor)) if not before_cursor: # '|' or 'set|' log.completion.debug('Starting command completion') @@ -96,6 +97,9 @@ class Completer(QObject): log.completion.debug("No completion for unknown command: {}" .format(before_cursor[0])) return None + + before_cursor = [x for x in before_cursor if not x.startswith('-')] + log.completion.debug("After removing flags: {}".format(before_cursor)) argpos = len(before_cursor) - 1 try: func = cmd.get_pos_arg_info(argpos).completion @@ -131,9 +135,7 @@ class Completer(QObject): return [], '', [] parser = runners.CommandParser() result = parser.parse(text, fallback=True, keep=True) - # pylint: disable=not-an-iterable parts = [x for x in result.cmdline if x] - # pylint: enable=not-an-iterable pos = self._cmd.cursorPosition() - len(self._cmd.prefix()) pos = min(pos, len(text)) # Qt treats 2-byte UTF-16 chars as 2 chars log.completion.debug('partitioning {} around position {}'.format(parts, @@ -152,8 +154,7 @@ class Completer(QObject): "partitioned: {} '{}' {}".format(prefix, center, postfix)) return prefix, center, postfix - # We should always return above - assert False, parts + raise utils.Unreachable("Not all parts consumed: {}".format(parts)) @pyqtSlot(str) def on_selection_changed(self, text): @@ -206,7 +207,7 @@ class Completer(QObject): log.completion.debug("Ignoring update because the length of " "the text is less than completion.min_chars.") elif (self._cmd.cursorPosition() == self._last_cursor_pos and - self._cmd.text() == self._last_text): + self._cmd.text() == self._last_text): log.completion.debug("Ignoring update because there were no " "changes.") else: @@ -247,10 +248,11 @@ class Completer(QObject): if func != self._last_completion_func: self._last_completion_func = func args = (x for x in before_cursor[1:] if not x.startswith('-')) - with debug.log_time(log.completion, - 'Starting {} completion'.format(func.__name__)): + with debug.log_time(log.completion, 'Starting {} completion' + .format(func.__name__)): info = CompletionInfo(config=config.instance, - keyconf=config.key_instance) + keyconf=config.key_instance, + win_id=self._win_id) model = func(*args, info=info) with debug.log_time(log.completion, 'Set completion model'): completion.set_model(model) diff --git a/qutebrowser/completion/completiondelegate.py b/qutebrowser/completion/completiondelegate.py index 6688a2dfa..b4f9c5a33 100644 --- a/qutebrowser/completion/completiondelegate.py +++ b/qutebrowser/completion/completiondelegate.py @@ -138,10 +138,10 @@ class CompletionItemDelegate(QStyledItemDelegate): self._painter.translate(text_rect.left(), text_rect.top()) self._get_textdoc(index) - self._draw_textdoc(text_rect) + self._draw_textdoc(text_rect, index.column()) self._painter.restore() - def _draw_textdoc(self, rect): + def _draw_textdoc(self, rect, col): """Draw the QTextDocument of an item. Args: @@ -156,7 +156,9 @@ class CompletionItemDelegate(QStyledItemDelegate): elif not self._opt.state & QStyle.State_Enabled: color = config.val.colors.completion.category.fg else: - color = config.val.colors.completion.fg + colors = config.val.colors.completion.fg + # if multiple colors are set, use different colors per column + color = colors[col % len(colors)] self._painter.setPen(color) ctx = QAbstractTextDocumentLayout.PaintContext() diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index 29471597f..29f4f6653 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -23,7 +23,7 @@ Defines a CompletionView which uses CompletionFiterModel and CompletionModel subclasses to provide completions. """ -from PyQt5.QtWidgets import QStyle, QTreeView, QSizePolicy, QStyleFactory +from PyQt5.QtWidgets import QTreeView, QSizePolicy, QStyleFactory from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, QSize from qutebrowser.config import config @@ -152,12 +152,12 @@ class CompletionView(QTreeView): column_widths = self.model().column_widths pixel_widths = [(width * perc // 100) for perc in column_widths] - if self.verticalScrollBar().isVisible(): - delta = self.style().pixelMetric(QStyle.PM_ScrollBarExtent) + 5 - if pixel_widths[-1] > delta: - pixel_widths[-1] -= delta - else: - pixel_widths[-2] -= delta + delta = self.verticalScrollBar().sizeHint().width() + if pixel_widths[-1] > delta: + pixel_widths[-1] -= delta + else: + pixel_widths[-2] -= delta + for i, w in enumerate(pixel_widths): assert w >= 0, i self.setColumnWidth(i, w) @@ -180,6 +180,7 @@ class CompletionView(QTreeView): return self.model().last_item() else: return self.model().first_item() + while True: idx = self.indexAbove(idx) if upwards else self.indexBelow(idx) # wrap around if we arrived at beginning/end @@ -193,6 +194,8 @@ class CompletionView(QTreeView): # Item is a real item, not a category header -> success return idx + raise utils.Unreachable + def _next_category_idx(self, upwards): """Get the index of the previous/next category. @@ -222,6 +225,8 @@ class CompletionView(QTreeView): self.scrollTo(idx) return idx.child(0, 0) + raise utils.Unreachable + @cmdutils.register(instance='completion', modes=[usertypes.KeyMode.command], scope='window') @cmdutils.argument('which', choices=['next', 'prev', 'next-category', diff --git a/qutebrowser/completion/models/configmodel.py b/qutebrowser/completion/models/configmodel.py index 53a2adc19..445a57a66 100644 --- a/qutebrowser/completion/models/configmodel.py +++ b/qutebrowser/completion/models/configmodel.py @@ -60,7 +60,8 @@ def value(optname, *_values, info): opt = info.config.get_opt(optname) default = opt.typ.to_str(opt.default) - cur_cat = listcategory.ListCategory("Current/Default", + cur_cat = listcategory.ListCategory( + "Current/Default", [(current, "Current value"), (default, "Default value")]) model.add_category(cur_cat) diff --git a/qutebrowser/completion/models/histcategory.py b/qutebrowser/completion/models/histcategory.py index b993b40de..57a2aa936 100644 --- a/qutebrowser/completion/models/histcategory.py +++ b/qutebrowser/completion/models/histcategory.py @@ -19,8 +19,6 @@ """A completion category that queries the SQL History store.""" -import re - from PyQt5.QtSql import QSqlQueryModel from qutebrowser.misc import sql @@ -36,21 +34,7 @@ class HistoryCategory(QSqlQueryModel): """Create a new History completion category.""" super().__init__(parent=parent) self.name = "History" - - # replace ' in timestamp-format to avoid breaking the query - timestamp_format = config.val.completion.timestamp_format - timefmt = ("strftime('{}', last_atime, 'unixepoch', 'localtime')" - .format(timestamp_format.replace("'", "`"))) - - self._query = sql.Query(' '.join([ - "SELECT url, title, {}".format(timefmt), - "FROM CompletionHistory", - # the incoming pattern will have literal % and _ escaped with '\' - # we need to tell sql to treat '\' as an escape character - "WHERE (url LIKE :pat escape '\\' or title LIKE :pat escape '\\')", - self._atime_expr(), - "ORDER BY last_atime DESC", - ]), forward_only=False) + self._query = None # advertise that this model filters by URL and title self.columns_to_filter = [0, 1] @@ -86,11 +70,36 @@ class HistoryCategory(QSqlQueryModel): # escape to treat a user input % or _ as a literal, not a wildcard pattern = pattern.replace('%', '\\%') pattern = pattern.replace('_', '\\_') - # treat spaces as wildcards to match any of the typed words - pattern = re.sub(r' +', '%', pattern) - pattern = '%{}%'.format(pattern) + words = ['%{}%'.format(w) for w in pattern.split(' ')] + + # build a where clause to match all of the words in any order + # given the search term "a b", the WHERE clause would be: + # ((url || title) LIKE '%a%') AND ((url || title) LIKE '%b%') + where_clause = ' AND '.join( + "(url || title) LIKE :{} escape '\\'".format(i) + for i in range(len(words))) + + # replace ' in timestamp-format to avoid breaking the query + timestamp_format = config.val.completion.timestamp_format + timefmt = ("strftime('{}', last_atime, 'unixepoch', 'localtime')" + .format(timestamp_format.replace("'", "`"))) + + if not self._query or len(words) != len(self._query.boundValues()): + # if the number of words changed, we need to generate a new query + # otherwise, we can reuse the prepared query for performance + self._query = sql.Query(' '.join([ + "SELECT url, title, {}".format(timefmt), + "FROM CompletionHistory", + # the incoming pattern will have literal % and _ escaped + # we need to tell sql to treat '\' as an escape character + 'WHERE ({})'.format(where_clause), + self._atime_expr(), + "ORDER BY last_atime DESC", + ]), forward_only=False) + with debug.log_time('sql', 'Running completion query'): - self._query.run(pat=pattern) + self._query.run(**{ + str(i): w for i, w in enumerate(words)}) self.setQuery(self._query) def removeRows(self, row, _count, _parent=None): diff --git a/qutebrowser/completion/models/miscmodels.py b/qutebrowser/completion/models/miscmodels.py index 28ff0ddac..22c9000c3 100644 --- a/qutebrowser/completion/models/miscmodels.py +++ b/qutebrowser/completion/models/miscmodels.py @@ -94,10 +94,11 @@ def session(*, info=None): # pylint: disable=unused-argument return model -def buffer(*, info=None): # pylint: disable=unused-argument - """A model to complete on open tabs across all windows. +def _buffer(skip_win_id=None): + """Helper to get the completion model for buffer/other_buffer. - Used for switching the buffer command. + Args: + skip_win_id: The id of the window to skip, or None to include all. """ def delete_buffer(data): """Close the selected tab.""" @@ -109,6 +110,8 @@ def buffer(*, info=None): # pylint: disable=unused-argument model = completionmodel.CompletionModel(column_widths=(6, 40, 54)) for win_id in objreg.window_registry: + if skip_win_id and win_id == skip_win_id: + continue tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) if tabbed_browser.shutting_down: @@ -120,19 +123,37 @@ def buffer(*, info=None): # pylint: disable=unused-argument tab.url().toDisplayString(), tabbed_browser.page_title(idx))) cat = listcategory.ListCategory("{}".format(win_id), tabs, - delete_func=delete_buffer) + delete_func=delete_buffer) model.add_category(cat) return model -def window(*, info=None): # pylint: disable=unused-argument +def buffer(*, info=None): # pylint: disable=unused-argument + """A model to complete on open tabs across all windows. + + Used for switching the buffer command. + """ + return _buffer() + + +def other_buffer(*, info): + """A model to complete on open tabs across all windows except the current. + + Used for the tab-take command. + """ + return _buffer(skip_win_id=info.win_id) + + +def window(*, info): """A model to complete on all open windows.""" model = completionmodel.CompletionModel(column_widths=(6, 30, 64)) windows = [] for win_id in objreg.window_registry: + if win_id == info.win_id: + continue tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) tab_titles = (tab.title() for tab in tabbed_browser.widgets()) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 7b9341343..c170d0705 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -104,13 +104,17 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name if self._function: @functools.wraps(func) def wrapper(option=None): + """Call the underlying function.""" if self._check_match(option): return func() + return None else: @functools.wraps(func) def wrapper(wrapper_self, option=None): + """Call the underlying function.""" if self._check_match(option): return func(wrapper_self) + return None return wrapper @@ -461,7 +465,8 @@ class ConfigContainer: def __setattr__(self, attr, value): """Set the given option in the config.""" if attr.startswith('_'): - return super().__setattr__(attr, value) + super().__setattr__(attr, value) + return name = self._join(attr) with self._handle_error('setting', name): diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 143cbd49a..b265ab8fc 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -174,6 +174,7 @@ def _parse_yaml_backends(name, node): elif isinstance(node, dict): return _parse_yaml_backends_dict(name, node) _raise_invalid_node(name, 'backends', node) + raise utils.Unreachable def _read_yaml(yaml_data): diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 08c854ed3..a118a8b59 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -34,6 +34,9 @@ history_gap_interval: `:history`. Use -1 to disable separation. ignore_case: + renamed: search.ignore_case + +search.ignore_case: type: name: String valid_values: @@ -43,6 +46,11 @@ ignore_case: default: smart desc: When to find text on a page case-insensitively. +search.incremental: + type: Bool + default: True + desc: Find text on a page incrementally, renewing the search for each typed character. + new_instance_open_target: type: name: String @@ -79,6 +87,9 @@ new_instance_open_target_window: When `new_instance_open_target` is not set to `window`, this is ignored. session_default_name: + renamed: session.default_name + +session.default_name: type: name: SessionName none_ok: true @@ -88,6 +99,11 @@ session_default_name: If this is set to null, the session which was last loaded is saved. +session.lazy_restore: + type: Bool + default: false + desc: Load a restored tab as soon as it takes focus. + backend: type: name: String @@ -1292,6 +1308,7 @@ tabs.title.format: - host - private - current_url + - protocol none_ok: true desc: | Format to use for the tab title. @@ -1306,8 +1323,9 @@ tabs.title.format: * `{scroll_pos}`: Page scroll position. * `{host}`: Host of the current web page. * `{backend}`: Either ''webkit'' or ''webengine'' - * `{private}` : Indicates when private mode is enabled. - * `{current_url}` : URL of the current web page. + * `{private}`: Indicates when private mode is enabled. + * `{current_url}`: URL of the current web page. + * `{protocol}`: Protocol (http/https/...) of the current web page. tabs.title.format_pinned: default: '{index}' @@ -1324,6 +1342,7 @@ tabs.title.format_pinned: - host - private - current_url + - protocol none_ok: true desc: Format to use for the tab title for pinned tabs. The same placeholders like for `tabs.title.format` are defined. @@ -1467,6 +1486,7 @@ window.title_format: - backend - private - current_url + - protocol default: '{perc}{title}{title_sep}qutebrowser' desc: | Format to use for the window title. The same placeholders like for @@ -1520,9 +1540,15 @@ zoom.text_only: ## colors colors.completion.fg: - default: white - type: QtColor - desc: Text color of the completion widget. + default: ["white", "white", "white"] + type: + name: ListOrValue + valtype: QtColor + desc: >- + Text color of the completion widget. + + May be a single color to use for all columns or a list of three colors, + one for each column. colors.completion.odd.bg: default: '#444444' @@ -2287,6 +2313,7 @@ bindings.default: : completion-item-yank : completion-item-yank --sel : command-accept + : command-accept --rapid : rl-backward-char : rl-forward-char : rl-backward-word diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index 27c898611..0e9572f55 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -136,7 +136,7 @@ class YamlConfig(QObject): with open(self._filename, 'r', encoding='utf-8') as f: yaml_data = utils.yaml_load(f) except FileNotFoundError: - return {} + return except OSError as e: desc = configexc.ConfigErrorDesc("While reading", e) raise configexc.ConfigFileErrors('autoconfig.yml', [desc]) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 8d0af173b..71c32f59e 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -499,7 +499,7 @@ class ListOrValue(BaseType): _show_valtype = True - def __init__(self, valtype, none_ok=False, *args, **kwargs): + def __init__(self, valtype, *args, none_ok=False, **kwargs): super().__init__(none_ok) assert not isinstance(valtype, (List, ListOrValue)), valtype self.listtype = List(valtype, none_ok=none_ok, *args, **kwargs) @@ -963,7 +963,7 @@ class Font(BaseType): # Gets set when the config is initialized. monospace_fonts = None font_regex = re.compile(r""" - ^( + ( ( # style (?P'); +//# sourceMappingURL=angular.min.js.map diff --git a/tests/end2end/data/hints/html/angular1.html b/tests/end2end/data/hints/html/angular1.html new file mode 100644 index 000000000..4693a634a --- /dev/null +++ b/tests/end2end/data/hints/html/angular1.html @@ -0,0 +1,22 @@ + + + + + + + +
+ +
+ + + + + diff --git a/tests/end2end/data/issue2569.html b/tests/end2end/data/issue2569.html index 8f613be2d..a21690bd6 100644 --- a/tests/end2end/data/issue2569.html +++ b/tests/end2end/data/issue2569.html @@ -15,6 +15,10 @@
+ +
+ +
diff --git a/tests/end2end/features/conftest.py b/tests/end2end/features/conftest.py index 7f5b4e2a6..5b97ba2a4 100644 --- a/tests/end2end/features/conftest.py +++ b/tests/end2end/features/conftest.py @@ -439,14 +439,21 @@ def expect_message(quteproc, server, category, message): @bdd.then(bdd.parsers.re(r'(?Pregex )?"(?P[^"]+)" should ' - r'be logged')) -def should_be_logged(quteproc, server, is_regex, pattern): + r'be logged( with level (?P.*))?')) +def should_be_logged(quteproc, server, is_regex, pattern, loglevel): """Expect the given pattern on regex in the log.""" if is_regex: pattern = re.compile(pattern) else: pattern = pattern.replace('(port)', str(server.port)) - line = quteproc.wait_for(message=pattern) + + args = { + 'message': pattern, + } + if loglevel: + args['loglevel'] = getattr(logging, loglevel.upper()) + + line = quteproc.wait_for(**args) line.expected = True diff --git a/tests/end2end/features/downloads.feature b/tests/end2end/features/downloads.feature index 69f47603b..26e3421a1 100644 --- a/tests/end2end/features/downloads.feature +++ b/tests/end2end/features/downloads.feature @@ -242,15 +242,6 @@ Feature: Downloading things from a website. ## Wrong invocations - Scenario: :download with deprecated dest-old argument - When I run :download http://localhost:(port)/ deprecated-argument - Then the warning ":download [url] [dest] is deprecated - use :download --dest [dest] [url]" should be shown - - Scenario: Two destinations given - When I run :download --dest destination2 http://localhost:(port)/ destination1 - Then the warning ":download [url] [dest] is deprecated - use :download --dest [dest] [url]" should be shown - And the error "Can't give two destinations for the download." should be shown - Scenario: :download --mhtml with a URL given When I run :download --mhtml http://foobar/ Then the error "Can only download the current page as mhtml." should be shown @@ -536,7 +527,7 @@ Feature: Downloading things from a website. And I open data/downloads/download.bin without waiting And I wait for the download prompt for "*" And I run :prompt-accept (tmpdir)(dirsep)downloads - And I open data/downloads/download.bin without waiting + And I open data/downloads/download2.bin without waiting And I wait for the download prompt for "*" And I directly open the download And I open data/downloads/download.bin without waiting diff --git a/tests/end2end/features/editor.feature b/tests/end2end/features/editor.feature index 21f3df425..15da4a6cd 100644 --- a/tests/end2end/features/editor.feature +++ b/tests/end2end/features/editor.feature @@ -116,7 +116,8 @@ Feature: Opening external editors Then the javascript message "text: foobar" should be logged # Could not get signals working on Windows - @posix + # There's no guarantee that the tab gets deleted... + @posix @flaky Scenario: Spawning an editor and closing the tab When I set up a fake editor that waits And I open data/editor.html diff --git a/tests/end2end/features/javascript.feature b/tests/end2end/features/javascript.feature index a309d6187..c74811b4b 100644 --- a/tests/end2end/features/javascript.feature +++ b/tests/end2end/features/javascript.feature @@ -90,6 +90,12 @@ Feature: Javascript stuff And I wait for "Sending fake click to *" in the log Then no crash should happen + Scenario: Clicking on form element with value child + When I open data/issue2569.html + And I run :click-element id valueform + And I wait for "Sending fake click to *" in the log + Then no crash should happen + Scenario: Clicking on svg element When I open data/issue2569.html And I run :click-element id icon @@ -123,3 +129,25 @@ Feature: Javascript stuff And I wait for "[*/data/javascript/windowsize.html:*] loaded" in the log And I run :tab-next Then the window sizes should be the same + + Scenario: Have a GreaseMonkey script run at page start + When I have a GreaseMonkey file saved for document-start with noframes unset + And I run :greasemonkey-reload + And I open data/hints/iframe.html + # This second reload is required in webengine < 5.8 for scripts + # registered to run at document-start, some sort of timing issue. + And I run :reload + Then the javascript message "Script is running on /data/hints/iframe.html" should be logged + + Scenario: Have a GreaseMonkey script running on frames + When I have a GreaseMonkey file saved for document-end with noframes unset + And I run :greasemonkey-reload + And I open data/hints/iframe.html + Then the javascript message "Script is running on /data/hints/html/wrapped.html" should be logged + + @flaky + Scenario: Have a GreaseMonkey script running on noframes + When I have a GreaseMonkey file saved for document-end with noframes set + And I run :greasemonkey-reload + And I open data/hints/iframe.html + Then the javascript message "Script is running on /data/hints/html/wrapped.html" should not be logged diff --git a/tests/end2end/features/marks.feature b/tests/end2end/features/marks.feature index 605bd3971..f7da07255 100644 --- a/tests/end2end/features/marks.feature +++ b/tests/end2end/features/marks.feature @@ -86,7 +86,7 @@ Feature: Setting positional marks And I wait until the scroll position changed to 10/10 Then the page should be scrolled to 10 10 - @qtwebengine_todo: Does not emit loaded signal for fragments? + @qtwebengine_skip: Does not emit loaded signal for fragments? Scenario: Jumping back after following a link When I hint with args "links normal" and follow s And I wait until data/marks.html#bottom is loaded diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index 70505c1f8..8f21b7421 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -9,6 +9,13 @@ Feature: Various utility commands. And I run :command-accept Then the message "Hello World" should be shown + Scenario: :set-cmd-text and :command-accept --rapid + When I run :set-cmd-text :message-info "Hello World" + And I run :command-accept --rapid + And I run :command-accept + Then the message "Hello World" should be shown + And the message "Hello World" should be shown + Scenario: :set-cmd-text with two commands When I run :set-cmd-text :message-info test ;; message-error error And I run :command-accept @@ -449,6 +456,11 @@ Feature: Various utility commands. And I run :click-element id qute-input Then "Entering mode KeyMode.insert (reason: clicking input)" should be logged + Scenario: Clicking an element by ID with dot + When I open data/click_element.html + And I run :click-element id foo.bar + Then the javascript message "id with dot" should be logged + Scenario: Clicking an element with tab target When I open data/click_element.html And I run :tab-only diff --git a/tests/end2end/features/qutescheme.feature b/tests/end2end/features/qutescheme.feature index 51db7f767..755c103e7 100644 --- a/tests/end2end/features/qutescheme.feature +++ b/tests/end2end/features/qutescheme.feature @@ -99,26 +99,28 @@ Feature: Special qute:// pages # qute://settings Scenario: Focusing input fields in qute://settings and entering valid value - When I set ignore_case to never + When I set search.ignore_case to never And I open qute://settings # scroll to the right - the table does not fit in the default screen And I run :scroll-to-perc -x 100 - And I run :jseval document.getElementById('input-ignore_case').value = '' - And I run :click-element id input-ignore_case + And I run :jseval document.getElementById('input-search.ignore_case').value = '' + And I run :click-element id input-search.ignore_case And I wait for "Entering mode KeyMode.insert *" in the log And I press the keys "always" And I press the key "" # an explicit Tab to unfocus the input field seems to stabilize the tests And I press the key "" - And I wait for "Config option changed: ignore_case *" in the log - Then the option ignore_case should be set to always + And I wait for "Config option changed: search.ignore_case *" in the log + Then the option search.ignore_case should be set to always + # Sometimes, an unrelated value gets set + @flaky Scenario: Focusing input fields in qute://settings and entering invalid value When I open qute://settings # scroll to the right - the table does not fit in the default screen And I run :scroll-to-perc -x 100 - And I run :jseval document.getElementById('input-ignore_case').value = '' - And I run :click-element id input-ignore_case + And I run :jseval document.getElementById('input-search.ignore_case').value = '' + And I run :click-element id input-search.ignore_case And I wait for "Entering mode KeyMode.insert *" in the log And I press the keys "foo" And I press the key "" diff --git a/tests/end2end/features/search.feature b/tests/end2end/features/search.feature index 7779ff28e..ae3f07999 100644 --- a/tests/end2end/features/search.feature +++ b/tests/end2end/features/search.feature @@ -22,7 +22,7 @@ Feature: Searching on a page Then "Bar" should be found Scenario: Searching with --reverse - When I set ignore_case to always + When I set search.ignore_case to always And I run :search -r foo And I wait for "search found foo with flags FindBackward" in the log Then "Foo" should be found @@ -52,28 +52,28 @@ Feature: Searching on a page And I wait for "search didn't find blub" in the log Then the warning "Text 'blub' not found on page!" should be shown - ## ignore_case + ## search.ignore_case - Scenario: Searching text with ignore_case = always - When I set ignore_case to always + Scenario: Searching text with search.ignore_case = always + When I set search.ignore_case to always And I run :search bar And I wait for "search found bar" in the log Then "Bar" should be found - Scenario: Searching text with ignore_case = never - When I set ignore_case to never + Scenario: Searching text with search.ignore_case = never + When I set search.ignore_case to never And I run :search bar And I wait for "search found bar with flags FindCaseSensitively" in the log Then "bar" should be found - Scenario: Searching text with ignore_case = smart (lower-case) - When I set ignore_case to smart + Scenario: Searching text with search.ignore_case = smart (lower-case) + When I set search.ignore_case to smart And I run :search bar And I wait for "search found bar" in the log Then "Bar" should be found - Scenario: Searching text with ignore_case = smart (upper-case) - When I set ignore_case to smart + Scenario: Searching text with search.ignore_case = smart (upper-case) + When I set search.ignore_case to smart And I run :search Foo And I wait for "search found Foo with flags FindCaseSensitively" in the log Then "Foo" should be found # even though foo was first @@ -81,7 +81,7 @@ Feature: Searching on a page ## :search-next Scenario: Jumping to next match - When I set ignore_case to always + When I set search.ignore_case to always And I run :search foo And I wait for "search found foo" in the log And I run :search-next @@ -89,7 +89,7 @@ Feature: Searching on a page Then "Foo" should be found Scenario: Jumping to next match with count - When I set ignore_case to always + When I set search.ignore_case to always And I run :search baz And I wait for "search found baz" in the log And I run :search-next with count 2 @@ -97,7 +97,7 @@ Feature: Searching on a page Then "BAZ" should be found Scenario: Jumping to next match with --reverse - When I set ignore_case to always + When I set search.ignore_case to always And I run :search --reverse foo And I wait for "search found foo with flags FindBackward" in the log And I run :search-next @@ -121,7 +121,7 @@ Feature: Searching on a page # https://github.com/qutebrowser/qutebrowser/issues/2438 Scenario: Jumping to next match after clearing - When I set ignore_case to always + When I set search.ignore_case to always And I run :search foo And I wait for "search found foo" in the log And I run :search @@ -132,7 +132,7 @@ Feature: Searching on a page ## :search-prev Scenario: Jumping to previous match - When I set ignore_case to always + When I set search.ignore_case to always And I run :search foo And I wait for "search found foo" in the log And I run :search-next @@ -142,7 +142,7 @@ Feature: Searching on a page Then "foo" should be found Scenario: Jumping to previous match with count - When I set ignore_case to always + When I set search.ignore_case to always And I run :search baz And I wait for "search found baz" in the log And I run :search-next @@ -154,7 +154,7 @@ Feature: Searching on a page Then "baz" should be found Scenario: Jumping to previous match with --reverse - When I set ignore_case to always + When I set search.ignore_case to always And I run :search --reverse foo And I wait for "search found foo with flags FindBackward" in the log And I run :search-next @@ -225,11 +225,15 @@ Feature: Searching on a page Then the following tabs should be open: - data/search.html (active) + # Following a link selected via JS doesn't work in Qt 5.10 anymore. + @qt!=5.10 Scenario: Follow a manually selected link When I run :jseval --file (testdata)/search_select.js And I run :follow-selected Then data/hello.txt should be loaded + # Following a link selected via JS doesn't work in Qt 5.10 anymore. + @qt!=5.10 Scenario: Follow a manually selected link in a new tab When I run :window-only And I run :jseval --file (testdata)/search_select.js diff --git a/tests/end2end/features/sessions.feature b/tests/end2end/features/sessions.feature index 5ec6e168a..50054c665 100644 --- a/tests/end2end/features/sessions.feature +++ b/tests/end2end/features/sessions.feature @@ -279,7 +279,7 @@ Feature: Saving and loading sessions Scenario: Saving session with --quiet When I run :session-save --quiet quiet_session - Then "Saved session quiet_session." should not be logged + Then "Saved session quiet_session." should be logged with level debug And the session quiet_session should exist Scenario: Saving session with --only-active-window diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature index b38d87a6e..1c670878e 100644 --- a/tests/end2end/features/tabs.feature +++ b/tests/end2end/features/tabs.feature @@ -1183,7 +1183,7 @@ Feature: Tab management And I run :tab-pin And I run :tab-next And I run :tab-only - And I wait for "*want to close a pinned tab*" in the log + And I wait for "*want to close pinned tabs*" in the log And I run :prompt-accept yes Then the following tabs should be open: - data/numbers/1.txt (active) (pinned) @@ -1195,7 +1195,7 @@ Feature: Tab management And I run :tab-pin And I run :tab-next And I run :tab-only - And I wait for "*want to close a pinned tab*" in the log + And I wait for "*want to close pinned tabs*" in the log And I run :prompt-accept no Then the following tabs should be open: - data/numbers/1.txt (active) (pinned) diff --git a/tests/end2end/features/test_editor_bdd.py b/tests/end2end/features/test_editor_bdd.py index 1aa253ac1..260197a46 100644 --- a/tests/end2end/features/test_editor_bdd.py +++ b/tests/end2end/features/test_editor_bdd.py @@ -26,6 +26,8 @@ import signal import pytest_bdd as bdd bdd.scenarios('editor.feature') +from qutebrowser.utils import utils + @bdd.when(bdd.parsers.parse('I set up a fake editor replacing "{text}" by ' '"{replacement}"')) @@ -49,7 +51,7 @@ def set_up_editor_replacement(quteproc, server, tmpdir, text, replacement): @bdd.when(bdd.parsers.parse('I set up a fake editor returning "{text}"')) -def set_up_editor(quteproc, server, tmpdir, text): +def set_up_editor(quteproc, tmpdir, text): """Set up editor.command to a small python script inserting a text.""" script = tmpdir / 'script.py' script.write(textwrap.dedent(""" @@ -63,14 +65,15 @@ def set_up_editor(quteproc, server, tmpdir, text): @bdd.when(bdd.parsers.parse('I set up a fake editor returning empty text')) -def set_up_editor_empty(quteproc, server, tmpdir): +def set_up_editor_empty(quteproc, tmpdir): """Set up editor.command to a small python script inserting empty text.""" - set_up_editor(quteproc, server, tmpdir, "") + set_up_editor(quteproc, tmpdir, "") @bdd.when(bdd.parsers.parse('I set up a fake editor that waits')) -def set_up_editor_wait(quteproc, server, tmpdir): +def set_up_editor_wait(quteproc, tmpdir): """Set up editor.command to a small python script inserting a text.""" + assert not utils.is_windows pidfile = tmpdir / 'editor_pid' script = tmpdir / 'script.py' script.write(textwrap.dedent(""" @@ -90,7 +93,7 @@ def set_up_editor_wait(quteproc, server, tmpdir): @bdd.when(bdd.parsers.parse('I kill the waiting editor')) -def kill_editor_wait(quteproc, server, tmpdir): +def kill_editor_wait(tmpdir): """Kill the waiting editor.""" pidfile = tmpdir / 'editor_pid' pid = int(pidfile.read()) diff --git a/tests/end2end/features/test_javascript_bdd.py b/tests/end2end/features/test_javascript_bdd.py index 9f6c021ce..631a422a5 100644 --- a/tests/end2end/features/test_javascript_bdd.py +++ b/tests/end2end/features/test_javascript_bdd.py @@ -17,6 +17,8 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . +import os.path + import pytest_bdd as bdd bdd.scenarios('javascript.feature') @@ -29,3 +31,38 @@ def check_window_sizes(quteproc): hidden_size = hidden.message.split()[-1] visible_size = visible.message.split()[-1] assert hidden_size == visible_size + + +test_gm_script = r""" +// ==UserScript== +// @name qutebrowser test userscript +// @namespace invalid.org +// @include http://localhost:*/data/hints/iframe.html +// @include http://localhost:*/data/hints/html/wrapped.html +// @exclude ??? +// @run-at {stage} +// {frames} +// ==/UserScript== +console.log("Script is running on " + window.location.pathname); +""" + + +@bdd.when(bdd.parsers.parse("I have a GreaseMonkey file saved for {stage} " + "with noframes {frameset}")) +def create_greasemonkey_file(quteproc, stage, frameset): + script_path = os.path.join(quteproc.basedir, 'data', 'greasemonkey') + try: + os.mkdir(script_path) + except FileExistsError: + pass + file_path = os.path.join(script_path, 'test.user.js') + if frameset == "set": + frames = "@noframes" + elif frameset == "unset": + frames = "" + else: + raise ValueError("noframes can only be set or unset, " + "not {}".format(frameset)) + with open(file_path, 'w', encoding='utf-8') as f: + f.write(test_gm_script.format(stage=stage, + frames=frames)) diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py index 27c347ca4..a2947e5af 100644 --- a/tests/end2end/fixtures/quteprocess.py +++ b/tests/end2end/fixtures/quteprocess.py @@ -20,10 +20,10 @@ """Fixtures to run qutebrowser in a QProcess and communicate.""" import os +import os.path import re import sys import time -import os.path import datetime import logging import tempfile @@ -36,7 +36,7 @@ import pytest from PyQt5.QtCore import pyqtSignal, QUrl, qVersion from qutebrowser.misc import ipc -from qutebrowser.utils import log, utils, javascript +from qutebrowser.utils import log, utils, javascript, qtutils from helpers import utils as testutils from end2end.fixtures import testprocess @@ -44,11 +44,11 @@ from end2end.fixtures import testprocess instance_counter = itertools.count() -def is_ignored_qt_message(message): +def is_ignored_qt_message(pytestconfig, message): """Check if the message is listed in qt_log_ignore.""" - regexes = pytest.config.getini('qt_log_ignore') + regexes = pytestconfig.getini('qt_log_ignore') for regex in regexes: - if re.match(regex, message): + if re.search(regex, message): return True return False @@ -154,7 +154,7 @@ def is_ignored_chromium_message(line): 'Invalid node channel message *', # Makes tests fail on Quantumcross' machine ('CreatePlatformSocket() returned an error, errno=97: Address family' - 'not supported by protocol'), + 'not supported by protocol'), # Qt 5.9 with debug Chromium @@ -169,7 +169,7 @@ def is_ignored_chromium_message(line): # /tmp/pytest-of-florian/pytest-32/test_webengine_download_suffix0/ # downloads/download.bin: Operation not supported ('Could not set extended attribute user.xdg.* on file *: ' - 'Operation not supported'), + 'Operation not supported'), # [5947:5947:0605/192837.856931:ERROR:render_process_impl.cc(112)] # WebFrame LEAKED 1 TIMES 'WebFrame LEAKED 1 TIMES', @@ -207,7 +207,7 @@ class LogLine(testprocess.Line): expected: Whether the message was expected or not. """ - def __init__(self, data): + def __init__(self, pytestconfig, data): super().__init__(data) try: line = json.loads(data) @@ -229,7 +229,7 @@ class LogLine(testprocess.Line): self.traceback = line.get('traceback') self.message = line['message'] - self.expected = is_ignored_qt_message(self.message) + self.expected = is_ignored_qt_message(pytestconfig, self.message) self.use_color = False def __str__(self): @@ -299,14 +299,13 @@ class QuteProc(testprocess.Process): 'message'] def __init__(self, request, *, parent=None): - super().__init__(parent) + super().__init__(request, parent) self._ipc_socket = None self.basedir = None self._focus_ready = False self._load_ready = False self._instance_id = next(instance_counter) self._run_counter = itertools.count() - self.request = request def _is_ready(self, what): """Called by _parse_line if loading/focusing is done. @@ -357,9 +356,10 @@ class QuteProc(testprocess.Process): if not self._load_ready: log_line.waited_for = True self._is_ready('load') - elif log_line.category == 'misc' and any(testutils.pattern_match( - pattern=pattern, value=log_line.message) for pattern in - start_okay_messages_focus): + elif (log_line.category == 'misc' and any( + testutils.pattern_match(pattern=pattern, + value=log_line.message) + for pattern in start_okay_messages_focus)): self._is_ready('focus') elif (log_line.category == 'init' and log_line.module == 'standarddir' and @@ -371,11 +371,11 @@ class QuteProc(testprocess.Process): def _parse_line(self, line): try: - log_line = LogLine(line) + log_line = LogLine(self.request.config, line) except testprocess.InvalidLine: if not line.strip(): return None - elif (is_ignored_qt_message(line) or + elif (is_ignored_qt_message(self.request.config, line) or is_ignored_lowlevel_message(line) or is_ignored_chromium_message(line) or self.request.node.get_marker('no_invalid_lines')): @@ -527,6 +527,7 @@ class QuteProc(testprocess.Process): super().before_test() self.send_cmd(':config-clear') self._init_settings() + self.clear_data() def _init_settings(self): """Adjust some qutebrowser settings after starting.""" @@ -687,9 +688,12 @@ class QuteProc(testprocess.Process): raise ValueError("Invalid URL {}: {}".format(url, qurl.errorString())) - if qurl == QUrl('about:blank'): + if (qurl == QUrl('about:blank') and + not qtutils.version_check('5.10', compiled=False)): # For some reason, we don't get a LoadStatus.success for # about:blank sometimes. + # However, if we do this for Qt 5.10, we get general testsuite + # instability as site loads get reported with about:blank... pattern = "Changing title for idx * to 'about:blank'" else: # We really need the same representation that the webview uses in diff --git a/tests/end2end/fixtures/test_quteprocess.py b/tests/end2end/fixtures/test_quteprocess.py index b537960f4..aa3fb5857 100644 --- a/tests/end2end/fixtures/test_quteprocess.py +++ b/tests/end2end/fixtures/test_quteprocess.py @@ -45,6 +45,10 @@ class FakeConfig: '--qute-delay': 0, '--color': True, '--verbose': False, + '--capture': None, + } + INI = { + 'qt_log_ignore': [], } def __init__(self): @@ -53,6 +57,9 @@ class FakeConfig: def getoption(self, name): return self.ARGS[name] + def getini(self, name): + return self.INI[name] + class FakeNode: @@ -222,8 +229,8 @@ def test_quteprocess_quitting(qtbot, quteproc_process): {'category': 'py.warnings'}, id='resourcewarning'), ]) -def test_log_line_parse(data, attrs): - line = quteprocess.LogLine(data) +def test_log_line_parse(pytestconfig, data, attrs): + line = quteprocess.LogLine(pytestconfig, data) for name, expected in attrs.items(): actual = getattr(line, name) assert actual == expected, name @@ -241,8 +248,8 @@ def test_log_line_parse(data, attrs): pytest.param( {'created': 86400, 'msecs': 0, 'levelname': 'DEBUG', 'name': 'foo', 'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 10, - 'message': 'quux', 'traceback': 'Traceback (most recent call ' - 'last):\n here be dragons'}, + 'message': 'quux', 'traceback': ('Traceback (most recent call ' + 'last):\n here be dragons')}, False, False, '{timestamp} DEBUG foo bar:qux:10 quux\n' 'Traceback (most recent call last):\n' @@ -283,9 +290,10 @@ def test_log_line_parse(data, attrs): '\033[36mfoo bar:qux:10\033[0m \033[37mquux\033[0m', id='expected error colorized'), ]) -def test_log_line_formatted(data, colorized, expect_error, expected): +def test_log_line_formatted(pytestconfig, + data, colorized, expect_error, expected): line = json.dumps(data) - record = quteprocess.LogLine(line) + record = quteprocess.LogLine(pytestconfig, line) record.expected = expect_error ts = datetime.datetime.fromtimestamp(data['created']).strftime('%H:%M:%S') ts += '.{:03.0f}'.format(data['msecs']) @@ -293,9 +301,9 @@ def test_log_line_formatted(data, colorized, expect_error, expected): assert record.formatted_str(colorized=colorized) == expected -def test_log_line_no_match(): +def test_log_line_no_match(pytestconfig): with pytest.raises(testprocess.InvalidLine): - quteprocess.LogLine("Hello World!") + quteprocess.LogLine(pytestconfig, "Hello World!") class TestClickElementByText: diff --git a/tests/end2end/fixtures/test_testprocess.py b/tests/end2end/fixtures/test_testprocess.py index 1811b7fb1..6ceb032af 100644 --- a/tests/end2end/fixtures/test_testprocess.py +++ b/tests/end2end/fixtures/test_testprocess.py @@ -51,8 +51,8 @@ class PythonProcess(testprocess.Process): """A testprocess which runs the given Python code.""" - def __init__(self): - super().__init__() + def __init__(self, request): + super().__init__(request) self.proc.setReadChannel(QProcess.StandardOutput) self.code = None @@ -103,22 +103,22 @@ class NoReadyPythonProcess(PythonProcess): @pytest.fixture -def pyproc(): - proc = PythonProcess() +def pyproc(request): + proc = PythonProcess(request) yield proc proc.terminate() @pytest.fixture -def quit_pyproc(): - proc = QuitPythonProcess() +def quit_pyproc(request): + proc = QuitPythonProcess(request) yield proc proc.terminate() @pytest.fixture -def noready_pyproc(): - proc = NoReadyPythonProcess() +def noready_pyproc(request): + proc = NoReadyPythonProcess(request) yield proc proc.terminate() @@ -149,9 +149,9 @@ def test_process_never_started(qtbot, quit_pyproc): quit_pyproc.after_test() -def test_wait_signal_raising(qtbot): +def test_wait_signal_raising(request, qtbot): """testprocess._wait_signal should raise by default.""" - proc = testprocess.Process() + proc = testprocess.Process(request) with pytest.raises(qtbot.TimeoutError): with proc._wait_signal(proc.proc.started, timeout=0): pass diff --git a/tests/end2end/fixtures/testprocess.py b/tests/end2end/fixtures/testprocess.py index a4b136193..3fb259e47 100644 --- a/tests/end2end/fixtures/testprocess.py +++ b/tests/end2end/fixtures/testprocess.py @@ -73,12 +73,11 @@ class Line: waited_for = attr.ib(False) -def _render_log(data, threshold=100): +def _render_log(data, *, verbose, threshold=100): """Shorten the given log without -v and convert to a string.""" 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) - verbose = pytest.config.getoption('--verbose') if len(data) > threshold and not verbose and not is_exception: msg = '[{} lines suppressed, use -v to show]'.format( len(data) - threshold) @@ -105,15 +104,17 @@ def pytest_runtest_makereport(item, call): # is actually a tuple. This is handled similarily in pytest-qt too. return - if pytest.config.getoption('--capture') == 'no': + if item.config.getoption('--capture') == 'no': # Already printed live return + verbose = item.config.getoption('--verbose') if quteproc_log is not None: report.longrepr.addsection("qutebrowser output", - _render_log(quteproc_log)) + _render_log(quteproc_log, verbose=verbose)) if server_log is not None: - report.longrepr.addsection("server output", _render_log(server_log)) + report.longrepr.addsection("server output", + _render_log(server_log, verbose=verbose)) class Process(QObject): @@ -128,6 +129,7 @@ class Process(QObject): _started: Whether the process was ever started. proc: The QProcess for the underlying process. exit_expected: Whether the process is expected to quit. + request: The request object for the current test. Signals: ready: Emitted when the server finished starting up. @@ -138,8 +140,9 @@ class Process(QObject): new_data = pyqtSignal(object) KEYS = ['data'] - def __init__(self, parent=None): + def __init__(self, request, parent=None): super().__init__(parent) + self.request = request self.captured_log = [] self._started = False self._invalid = [] @@ -150,7 +153,7 @@ class Process(QObject): def _log(self, line): """Add the given line to the captured log output.""" - if pytest.config.getoption('--capture') == 'no': + if self.request.config.getoption('--capture') == 'no': print(line) self.captured_log.append(line) @@ -225,6 +228,8 @@ class Process(QObject): """Start the process and wait until it started.""" self._start(args, env=env) self._started = True + verbose = self.request.config.getoption('--verbose') + timeout = 60 if 'CI' in os.environ else 20 for _ in range(timeout): with self._wait_signal(self.ready, timeout=1000, @@ -236,14 +241,15 @@ class Process(QObject): return # _start ensures it actually started, but it might quit shortly # afterwards - raise ProcessExited('\n' + _render_log(self.captured_log)) + raise ProcessExited('\n' + _render_log(self.captured_log, + verbose=verbose)) if blocker.signal_triggered: self._after_start() return raise WaitForTimeout("Timed out while waiting for process start.\n" + - _render_log(self.captured_log)) + _render_log(self.captured_log, verbose=verbose)) def _start(self, args, env): """Actually start the process.""" @@ -300,8 +306,16 @@ class Process(QObject): def terminate(self): """Clean up and shut down the process.""" - self.proc.terminate() - self.proc.waitForFinished() + if not self.is_running(): + return + + if quteutils.is_windows: + self.proc.kill() + else: + self.proc.terminate() + + ok = self.proc.waitForFinished() + assert ok def is_running(self): """Check if the process is currently running.""" @@ -327,13 +341,13 @@ class Process(QObject): if expected is None: return True elif isinstance(expected, regex_type): - return expected.match(value) + return expected.search(value) elif isinstance(value, (bytes, str)): return utils.pattern_match(pattern=expected, value=value) else: return value == expected - def _wait_for_existing(self, override_waited_for, **kwargs): + def _wait_for_existing(self, override_waited_for, after, **kwargs): """Check if there are any line in the history for wait_for. Return: either the found line or None. @@ -345,7 +359,15 @@ class Process(QObject): value = getattr(line, key) matches.append(self._match_data(value, expected)) - if all(matches) and (not line.waited_for or override_waited_for): + if after is None: + too_early = False + else: + too_early = ((line.timestamp, line.msecs) < + (after.timestamp, after.msecs)) + + if (all(matches) and + (not line.waited_for or override_waited_for) and + not too_early): # If we waited for this line, chances are we don't mean the # same thing the next time we use wait_for and it matches # this line again. @@ -363,7 +385,7 @@ class Process(QObject): __tracebackhide__ = lambda e: e.errisinstance(WaitForTimeout) message = kwargs.get('message', None) if message is not None: - elided = quteutils.elide(repr(message), 50) + elided = quteutils.elide(repr(message), 100) self._log("\n----> Waiting for {} in the log".format(elided)) spy = QSignalSpy(self.new_data) @@ -388,6 +410,8 @@ class Process(QObject): self._log("----> found it") return match + raise quteutils.Unreachable + def _wait_for_match(self, spy, kwargs): """Try matching the kwargs with the given QSignalSpy.""" for args in spy: @@ -422,7 +446,7 @@ class Process(QObject): pass def wait_for(self, timeout=None, *, override_waited_for=False, - do_skip=False, divisor=1, **kwargs): + do_skip=False, divisor=1, after=None, **kwargs): """Wait until a given value is found in the data. Keyword arguments to this function get interpreted as attributes of the @@ -435,6 +459,7 @@ class Process(QObject): again. do_skip: If set, call pytest.skip on a timeout. divisor: A factor to decrease the timeout by. + after: If it's an existing line, ensure it's after the given one. Return: The matched line. @@ -456,7 +481,8 @@ class Process(QObject): for key in kwargs: assert key in self.KEYS - existing = self._wait_for_existing(override_waited_for, **kwargs) + existing = self._wait_for_existing(override_waited_for, after, + **kwargs) if existing is not None: return existing else: diff --git a/tests/end2end/fixtures/webserver.py b/tests/end2end/fixtures/webserver.py index 85a6af070..93ef04f03 100644 --- a/tests/end2end/fixtures/webserver.py +++ b/tests/end2end/fixtures/webserver.py @@ -137,8 +137,8 @@ class WebserverProcess(testprocess.Process): KEYS = ['verb', 'path'] - def __init__(self, script, parent=None): - super().__init__(parent) + def __init__(self, request, script, parent=None): + super().__init__(request, parent) self._script = script self.port = utils.random_port() self.new_data.connect(self.new_request) @@ -172,19 +172,14 @@ class WebserverProcess(testprocess.Process): def _default_args(self): return [str(self.port)] - def cleanup(self): - """Clean up and shut down the process.""" - self.proc.terminate() - self.proc.waitForFinished() - @pytest.fixture(scope='session', autouse=True) -def server(qapp): +def server(qapp, request): """Fixture for an server object which ensures clean setup/teardown.""" - server = WebserverProcess('webserver_sub') + server = WebserverProcess(request, 'webserver_sub') server.start() yield server - server.cleanup() + server.terminate() @pytest.fixture(autouse=True) @@ -203,9 +198,9 @@ def ssl_server(request, qapp): This needs to be explicitly used in a test, and overwrites the server log used in that test. """ - server = WebserverProcess('webserver_sub_ssl') + server = WebserverProcess(request, 'webserver_sub_ssl') request.node._server_log = server.captured_log server.start() yield server server.after_test() - server.cleanup() + server.terminate() diff --git a/tests/end2end/test_dirbrowser.py b/tests/end2end/test_dirbrowser.py index 16da8d0bc..ea92e2ca2 100644 --- a/tests/end2end/test_dirbrowser.py +++ b/tests/end2end/test_dirbrowser.py @@ -195,7 +195,6 @@ def test_enter_folder_smoke(dir_layout, quteproc): @pytest.mark.parametrize('folder', DirLayout.layout_folders()) def test_enter_folder(dir_layout, quteproc, folder): - # pylint: disable=not-an-iterable quteproc.open_url(dir_layout.file_url()) quteproc.click_element_by_text(text=folder) expected_url = urlutils.file_url(dir_layout.path(folder)) @@ -208,4 +207,3 @@ def test_enter_folder(dir_layout, quteproc, folder): assert foldernames == folders filenames = [item.text for item in page.files] assert filenames == files - # pylint: enable=not-an-iterable diff --git a/tests/end2end/test_hints_html.py b/tests/end2end/test_hints_html.py index ff4837020..fb30abefe 100644 --- a/tests/end2end/test_hints_html.py +++ b/tests/end2end/test_hints_html.py @@ -71,8 +71,8 @@ def _parse_file(test_name): allowed_keys = {'target', 'qtwebengine_todo'} if not set(data.keys()).issubset(allowed_keys): raise InvalidFile(test_name, "expected keys {} but found {}".format( - ', '.join(allowed_keys), - ', '.join(set(data.keys())))) + ', '.join(allowed_keys), + ', '.join(set(data.keys())))) if 'target' not in data: raise InvalidFile(test_name, "'target' key not found") @@ -106,11 +106,7 @@ def test_hints(test_name, zoom_text_only, zoom_level, find_implementation, quteproc.set_setting('hints.find_implementation', find_implementation) quteproc.send_cmd(':zoom {}'.format(zoom_level)) # follow hint - if 'button' in test_name: - # We are hinting buttons, link hinting will not work - quteproc.send_cmd(':hint all normal') - else: - quteproc.send_cmd(':hint links normal') + quteproc.send_cmd(':hint all normal') quteproc.wait_for(message='hints: a', category='hints') quteproc.send_cmd(':follow-hint a') quteproc.wait_for_load_finished('data/' + parsed.target) diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py index dc9486142..ef4808718 100644 --- a/tests/end2end/test_invocations.py +++ b/tests/end2end/test_invocations.py @@ -359,14 +359,14 @@ def test_qute_settings_persistence(short_tmpdir, request, quteproc_new): args = _base_args(request.config) + ['--basedir', str(short_tmpdir)] quteproc_new.start(args) quteproc_new.open_path( - 'qute://settings/set?option=ignore_case&value=always') - assert quteproc_new.get_setting('ignore_case') == 'always' + 'qute://settings/set?option=search.ignore_case&value=always') + assert quteproc_new.get_setting('search.ignore_case') == 'always' quteproc_new.send_cmd(':quit') quteproc_new.wait_for_quit() quteproc_new.start(args) - assert quteproc_new.get_setting('ignore_case') == 'always' + assert quteproc_new.get_setting('search.ignore_case') == 'always' @pytest.mark.no_xvfb diff --git a/tests/end2end/test_mhtml_e2e.py b/tests/end2end/test_mhtml_e2e.py index 4317271f5..012c2ea2a 100644 --- a/tests/end2end/test_mhtml_e2e.py +++ b/tests/end2end/test_mhtml_e2e.py @@ -20,8 +20,8 @@ """Test mhtml downloads based on sample files.""" import os -import re import os.path +import re import collections import pytest diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py index 5cbf01aad..715e0168d 100644 --- a/tests/helpers/fixtures.py +++ b/tests/helpers/fixtures.py @@ -35,6 +35,10 @@ import types import attr import pytest import py.path # pylint: disable=no-name-in-module +from PyQt5.QtCore import QEvent, QSize, Qt +from PyQt5.QtGui import QKeyEvent +from PyQt5.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout +from PyQt5.QtNetwork import QNetworkCookieJar import helpers.stubs as stubsmod import helpers.utils @@ -44,11 +48,6 @@ from qutebrowser.browser.webkit import cookies from qutebrowser.misc import savemanager, sql from qutebrowser.keyinput import modeman -from PyQt5.QtCore import QEvent, QSize, Qt -from PyQt5.QtGui import QKeyEvent -from PyQt5.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout -from PyQt5.QtNetwork import QNetworkCookieJar - class WinRegistryHelper: diff --git a/tests/helpers/logfail.py b/tests/helpers/logfail.py index 3d8e3afb8..ba7ed24b8 100644 --- a/tests/helpers/logfail.py +++ b/tests/helpers/logfail.py @@ -22,16 +22,7 @@ import logging import pytest - -try: - import pytest_catchlog as catchlog_mod -except ImportError: - # When using pytest for pyflakes/pep8/..., the plugin won't be available - # but conftest.py will still be loaded. - # - # However, LogFailHandler.emit will never be used in that case, so we just - # ignore the ImportError. - pass +import _pytest.logging class LogFailHandler(logging.Handler): @@ -50,8 +41,8 @@ class LogFailHandler(logging.Handler): return for h in root_logger.handlers: - if isinstance(h, catchlog_mod.LogCaptureHandler): - catchlog_handler = h + if isinstance(h, _pytest.logging.LogCaptureHandler): + capture_handler = h break else: # The LogCaptureHandler is not available anymore during fixture @@ -59,7 +50,7 @@ class LogFailHandler(logging.Handler): return if (logger.level == record.levelno or - catchlog_handler.level == record.levelno): + capture_handler.level == record.levelno): # caplog.at_level(...) was used with the level of this message, # i.e. it was expected. return diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index 878c9e166..3f6a23958 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -24,7 +24,7 @@ from unittest import mock import attr -from PyQt5.QtCore import pyqtSignal, QPoint, QProcess, QObject +from PyQt5.QtCore import pyqtSignal, QPoint, QProcess, QObject, QUrl from PyQt5.QtNetwork import (QNetworkRequest, QAbstractNetworkCache, QNetworkCacheMetaData) from PyQt5.QtWidgets import QCommonStyle, QLineEdit, QWidget, QTabBar @@ -127,17 +127,6 @@ class FakeQApplication: self.activeWindow = lambda: active_window -class FakeUrl: - - """QUrl stub which provides .path(), isValid() and host().""" - - def __init__(self, path=None, valid=True, host=None, url=None): - self.path = mock.Mock(return_value=path) - self.isValid = mock.Mock(returl_value=valid) - self.host = mock.Mock(returl_value=host) - self.url = mock.Mock(return_value=url) - - class FakeNetworkReply: """QNetworkReply stub which provides a Content-Disposition header.""" @@ -148,7 +137,7 @@ class FakeNetworkReply: def __init__(self, headers=None, url=None): if url is None: - url = FakeUrl() + url = QUrl() if headers is None: self.headers = {} else: @@ -244,7 +233,7 @@ class FakeWebTab(browsertab.AbstractTab): """Fake AbstractTab to use in tests.""" - def __init__(self, url=FakeUrl(), title='', tab_id=0, *, + def __init__(self, url=QUrl(), title='', tab_id=0, *, scroll_pos_perc=(0, 0), load_status=usertypes.LoadStatus.success, progress=0, can_go_back=None, can_go_forward=None): diff --git a/tests/helpers/test_logfail.py b/tests/helpers/test_logfail.py index b95dec1d6..48aaaa201 100644 --- a/tests/helpers/test_logfail.py +++ b/tests/helpers/test_logfail.py @@ -23,7 +23,6 @@ import logging import pytest -import pytest_catchlog def test_log_debug(): @@ -64,24 +63,3 @@ def test_log_expected_wrong_logger(caplog): with pytest.raises(pytest.fail.Exception): with caplog.at_level(logging.ERROR, logger): logging.error('foo') - - -@pytest.fixture -def skipping_fixture(): - pytest.skip("Skipping to test caplog workaround.") - - -def test_caplog_bug_workaround_1(caplog, skipping_fixture): - pass - - -def test_caplog_bug_workaround_2(): - """Make sure caplog_bug_workaround works correctly after a skipped test. - - There should be only one capturelog handler. - """ - caplog_handler = None - for h in logging.getLogger().handlers: - if isinstance(h, pytest_catchlog.LogCaptureHandler): - assert caplog_handler is None - caplog_handler = h diff --git a/tests/unit/browser/test_history.py b/tests/unit/browser/test_history.py index 1b706331a..c5734d34a 100644 --- a/tests/unit/browser/test_history.py +++ b/tests/unit/browser/test_history.py @@ -153,13 +153,13 @@ def test_delete_url(hist, raw, escaped): 'url, atime, title, redirect, history_url, completion_url', [ ('http://www.example.com', 12346, 'the title', False, - 'http://www.example.com', 'http://www.example.com'), + 'http://www.example.com', 'http://www.example.com'), ('http://www.example.com', 12346, 'the title', True, - 'http://www.example.com', None), + 'http://www.example.com', None), ('http://www.example.com/sp ce', 12346, 'the title', False, - 'http://www.example.com/sp%20ce', 'http://www.example.com/sp ce'), + 'http://www.example.com/sp%20ce', 'http://www.example.com/sp ce'), ('https://user:pass@example.com', 12346, 'the title', False, - 'https://user@example.com', 'https://user@example.com'), + 'https://user@example.com', 'https://user@example.com'), ] ) def test_add_url(qtbot, hist, url, atime, title, redirect, history_url, diff --git a/tests/unit/browser/test_qutescheme.py b/tests/unit/browser/test_qutescheme.py index 6fdaad83c..e7c6cc9eb 100644 --- a/tests/unit/browser/test_qutescheme.py +++ b/tests/unit/browser/test_qutescheme.py @@ -118,7 +118,7 @@ class TestHistoryHandler: (72*60*60, 0) ]) def test_qutehistory_data(self, start_time_offset, expected_item_count, - now): + now): """Ensure qute://history/data returns correct items.""" start_time = now - start_time_offset url = QUrl("qute://history/data?start_time=" + str(start_time)) diff --git a/tests/unit/browser/test_tab.py b/tests/unit/browser/test_tab.py index a2a34d9ce..d67ee4c8f 100644 --- a/tests/unit/browser/test_tab.py +++ b/tests/unit/browser/test_tab.py @@ -20,6 +20,7 @@ import pytest from qutebrowser.browser import browsertab +from qutebrowser.utils import utils pytestmark = pytest.mark.usefixtures('redirect_webengine_data') @@ -54,7 +55,7 @@ def tab(request, qtbot, tab_registry, cookiejar_and_cache, mode_manager): 'qutebrowser.browser.webengine.webenginetab') tab_class = webenginetab.WebEngineTab else: - assert False + raise utils.Unreachable t = tab_class(win_id=0, mode_manager=mode_manager) qtbot.add_widget(t) @@ -67,7 +68,7 @@ class Zoom(browsertab.AbstractZoom): pass def factor(self): - assert False + raise utils.Unreachable class Tab(browsertab.AbstractTab): diff --git a/tests/unit/commands/test_userscripts.py b/tests/unit/commands/test_userscripts.py index 32f9a1bb9..3b6a69710 100644 --- a/tests/unit/commands/test_userscripts.py +++ b/tests/unit/commands/test_userscripts.py @@ -64,6 +64,7 @@ def runner(request, runtime_tmpdir): if (not utils.is_posix and request.param is userscripts._POSIXUserscriptRunner): pytest.skip("Requires a POSIX os") + raise utils.Unreachable else: return request.param() diff --git a/tests/unit/completion/test_completer.py b/tests/unit/completion/test_completer.py index a32241621..4b39b7032 100644 --- a/tests/unit/completion/test_completer.py +++ b/tests/unit/completion/test_completer.py @@ -65,7 +65,8 @@ def completer_obj(qtbot, status_command_stub, config_stub, monkeypatch, stubs, """Create the completer used for testing.""" monkeypatch.setattr(completer, 'QTimer', stubs.InstaTimer) config_stub.val.completion.show = 'auto' - return completer.Completer(status_command_stub, completion_widget_stub) + return completer.Completer(cmd=status_command_stub, win_id=0, + parent=completion_widget_stub) @pytest.fixture(autouse=True) @@ -159,7 +160,7 @@ def _set_cmd_prompt(cmd, txt): (':set general editor |', 'value', '', ['general', 'editor']), (':set general editor gv|', 'value', 'gv', ['general', 'editor']), (':set general editor "gvim -f"|', 'value', 'gvim -f', - ['general', 'editor']), + ['general', 'editor']), (':set general editor "gvim |', 'value', 'gvim', ['general', 'editor']), (':set general huh |', 'value', '', ['general', 'huh']), (':help |', 'helptopic', '', []), @@ -189,6 +190,7 @@ def _set_cmd_prompt(cmd, txt): (':gibberish nonesense |', None, '', []), ('/:help|', None, '', []), ('::bind|', 'command', ':bind', []), + (':-w open |', None, '', []), ]) def test_update_completion(txt, kind, pattern, pos_args, status_command_stub, completer_obj, completion_widget_stub, config_stub, diff --git a/tests/unit/completion/test_completionmodel.py b/tests/unit/completion/test_completionmodel.py index 1e88838f3..b0775428f 100644 --- a/tests/unit/completion/test_completionmodel.py +++ b/tests/unit/completion/test_completionmodel.py @@ -31,7 +31,8 @@ from qutebrowser.utils import qtutils from qutebrowser.commands import cmdexc -@hypothesis.given(strategies.lists(min_size=0, max_size=3, +@hypothesis.given(strategies.lists( + min_size=0, max_size=3, elements=strategies.integers(min_value=0, max_value=2**31))) def test_first_last_item(counts): """Test that first() and last() index to the first and last items.""" diff --git a/tests/unit/completion/test_completionwidget.py b/tests/unit/completion/test_completionwidget.py index a639daf95..ba6e40e4a 100644 --- a/tests/unit/completion/test_completionwidget.py +++ b/tests/unit/completion/test_completionwidget.py @@ -82,9 +82,9 @@ def test_maybe_update_geometry(completionview, config_stub, qtbot): ('next', [['Aa'], ['Ba']], ['Aa', 'Ba', 'Aa']), ('prev', [['Aa'], ['Ba']], ['Ba', 'Aa', 'Ba']), ('next', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], - ['Aa', 'Ab', 'Ac', 'Ba', 'Bb', 'Ca', 'Aa']), + ['Aa', 'Ab', 'Ac', 'Ba', 'Bb', 'Ca', 'Aa']), ('prev', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], - ['Ca', 'Bb', 'Ba', 'Ac', 'Ab', 'Aa', 'Ca']), + ['Ca', 'Bb', 'Ba', 'Ac', 'Ab', 'Aa', 'Ca']), ('next', [[], ['Ba', 'Bb']], ['Ba', 'Bb', 'Ba']), ('prev', [[], ['Ba', 'Bb']], ['Bb', 'Ba', 'Bb']), ('next', [[], [], ['Ca', 'Cb']], ['Ca', 'Cb', 'Ca']), @@ -102,9 +102,9 @@ def test_maybe_update_geometry(completionview, config_stub, qtbot): ('next-category', [['Aa'], ['Ba']], ['Aa', 'Ba', 'Aa']), ('prev-category', [['Aa'], ['Ba']], ['Ba', 'Aa', 'Ba']), ('next-category', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], - ['Aa', 'Ba', 'Ca', 'Aa']), + ['Aa', 'Ba', 'Ca', 'Aa']), ('prev-category', [['Aa', 'Ab', 'Ac'], ['Ba', 'Bb'], ['Ca']], - ['Ca', 'Ba', 'Aa', 'Ca']), + ['Ca', 'Ba', 'Aa', 'Ca']), ('next-category', [[], ['Ba', 'Bb']], ['Ba', None, None]), ('prev-category', [[], ['Ba', 'Bb']], ['Ba', None, None]), ('next-category', [[], [], ['Ca', 'Cb']], ['Ca', None, None]), @@ -170,8 +170,9 @@ def test_completion_item_focus_fetch(completionview, qtbot): emitted. """ model = completionmodel.CompletionModel() - cat = mock.Mock(spec=['layoutChanged', 'layoutAboutToBeChanged', - 'canFetchMore', 'fetchMore', 'rowCount', 'index', 'data']) + cat = mock.Mock(spec=[ + 'layoutChanged', 'layoutAboutToBeChanged', 'canFetchMore', + 'fetchMore', 'rowCount', 'index', 'data']) cat.canFetchMore = lambda *_: True cat.rowCount = lambda *_: 2 cat.fetchMore = mock.Mock() diff --git a/tests/unit/completion/test_histcategory.py b/tests/unit/completion/test_histcategory.py index 834b3a5a3..e42f9b91f 100644 --- a/tests/unit/completion/test_histcategory.py +++ b/tests/unit/completion/test_histcategory.py @@ -61,7 +61,7 @@ def hist(init_sql, config_stub): ('foo bar', [('foo', ''), ('bar foo', ''), ('xfooyybarz', '')], - [('xfooyybarz', '')]), + [('bar foo', ''), ('xfooyybarz', '')]), ('foo%bar', [('foo%bar', ''), ('foo bar', ''), ('foobar', '')], @@ -78,6 +78,10 @@ def hist(init_sql, config_stub): ("can't", [("can't touch this", ''), ('a', '')], [("can't touch this", '')]), + + ("ample itle", + [('example.com', 'title'), ('example.com', 'nope')], + [('example.com', 'title')]), ]) def test_set_pattern(pattern, before, after, model_validator, hist): """Validate the filtering and sorting results of set_pattern.""" @@ -89,6 +93,38 @@ def test_set_pattern(pattern, before, after, model_validator, hist): model_validator.validate(after) +def test_set_pattern_repeated(model_validator, hist): + """Validate multiple subsequent calls to set_pattern.""" + hist.insert({'url': 'example.com/foo', 'title': 'title1', 'last_atime': 1}) + hist.insert({'url': 'example.com/bar', 'title': 'title2', 'last_atime': 1}) + hist.insert({'url': 'example.com/baz', 'title': 'title3', 'last_atime': 1}) + cat = histcategory.HistoryCategory() + model_validator.set_model(cat) + + cat.set_pattern('b') + model_validator.validate([ + ('example.com/bar', 'title2'), + ('example.com/baz', 'title3'), + ]) + + cat.set_pattern('ba') + model_validator.validate([ + ('example.com/bar', 'title2'), + ('example.com/baz', 'title3'), + ]) + + cat.set_pattern('ba ') + model_validator.validate([ + ('example.com/bar', 'title2'), + ('example.com/baz', 'title3'), + ]) + + cat.set_pattern('ba z') + model_validator.validate([ + ('example.com/baz', 'title3'), + ]) + + @pytest.mark.parametrize('max_items, before, after', [ (-1, [ ('a', 'a', '2017-04-16'), diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py index 9c767c102..1a0fe57b0 100644 --- a/tests/unit/completion/test_models.py +++ b/tests/unit/completion/test_models.py @@ -191,7 +191,8 @@ def web_history_populated(web_history): @pytest.fixture def info(config_stub, key_config_stub): return completer.CompletionInfo(config=config_stub, - keyconf=key_config_stub) + keyconf=key_config_stub, + win_id=0) def test_command_completion(qtmodeltester, cmdutils_stub, configdata_stub, @@ -413,12 +414,12 @@ def test_url_completion_no_bookmarks(qtmodeltester, web_history_populated, ('example.com', 'Site Title', 'am', 1), ('example.com', 'Site Title', 'com', 1), ('example.com', 'Site Title', 'ex com', 1), - ('example.com', 'Site Title', 'com ex', 0), + ('example.com', 'Site Title', 'com ex', 1), ('example.com', 'Site Title', 'ex foo', 0), ('example.com', 'Site Title', 'foo com', 0), ('example.com', 'Site Title', 'exm', 0), ('example.com', 'Site Title', 'Si Ti', 1), - ('example.com', 'Site Title', 'Ti Si', 0), + ('example.com', 'Site Title', 'Ti Si', 1), ('example.com', '', 'foo', 0), ('foo_bar', '', '_', 1), ('foobar', '', '_', 0), @@ -581,7 +582,33 @@ def test_tab_completion_delete(qtmodeltester, fake_web_tab, app_stub, QUrl('https://duckduckgo.com')] -def test_window_completion(qtmodeltester, fake_web_tab, tabbed_browser_stubs): +def test_other_buffer_completion(qtmodeltester, fake_web_tab, app_stub, + win_registry, tabbed_browser_stubs, info): + tabbed_browser_stubs[0].tabs = [ + fake_web_tab(QUrl('https://github.com'), 'GitHub', 0), + fake_web_tab(QUrl('https://wikipedia.org'), 'Wikipedia', 1), + fake_web_tab(QUrl('https://duckduckgo.com'), 'DuckDuckGo', 2), + ] + tabbed_browser_stubs[1].tabs = [ + fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0), + ] + info.win_id = 1 + model = miscmodels.other_buffer(info=info) + model.set_pattern('') + qtmodeltester.data_display_may_return_none = True + qtmodeltester.check(model) + + _check_completions(model, { + '0': [ + ('0/1', 'https://github.com', 'GitHub'), + ('0/2', 'https://wikipedia.org', 'Wikipedia'), + ('0/3', 'https://duckduckgo.com', 'DuckDuckGo') + ], + }) + + +def test_window_completion(qtmodeltester, fake_web_tab, tabbed_browser_stubs, + info): tabbed_browser_stubs[0].tabs = [ fake_web_tab(QUrl('https://github.com'), 'GitHub', 0), fake_web_tab(QUrl('https://wikipedia.org'), 'Wikipedia', 1), @@ -591,7 +618,8 @@ def test_window_completion(qtmodeltester, fake_web_tab, tabbed_browser_stubs): fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0) ] - model = miscmodels.window() + info.win_id = 1 + model = miscmodels.window(info=info) model.set_pattern('') qtmodeltester.data_display_may_return_none = True qtmodeltester.check(model) @@ -599,8 +627,7 @@ def test_window_completion(qtmodeltester, fake_web_tab, tabbed_browser_stubs): _check_completions(model, { 'Windows': [ ('0', 'window title - qutebrowser', - 'GitHub, Wikipedia, DuckDuckGo'), - ('1', 'window title - qutebrowser', 'ArchWiki') + 'GitHub, Wikipedia, DuckDuckGo'), ] }) @@ -615,11 +642,11 @@ def test_setting_option_completion(qtmodeltester, config_stub, _check_completions(model, { "Options": [ ('aliases', 'Aliases for commands.', '{"q": "quit"}'), - ('bindings.commands', 'Default keybindings', + ('bindings.commands', 'Default keybindings', ( '{"normal": {"": "quit", "ZQ": "quit", ' - '"I": "invalid", "d": "scroll down"}}'), + '"I": "invalid", "d": "scroll down"}}')), ('bindings.default', 'Default keybindings', - '{"normal": {"": "quit", "d": "tab-close"}}'), + '{"normal": {"": "quit", "d": "tab-close"}}'), ] }) diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index 32a7a8119..d8bf73700 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -385,7 +385,7 @@ class TestConfig: def test_get(self, conf): """Test conf.get() with a QColor (where get/get_obj is different).""" - assert conf.get('colors.completion.fg') == QColor('white') + assert conf.get('colors.completion.category.fg') == QColor('white') @pytest.mark.parametrize('value', [{}, {'normal': {'a': 'nop'}}]) def test_get_bindings(self, config_stub, conf, value): @@ -400,7 +400,7 @@ class TestConfig: assert not conf._mutables def test_get_obj_simple(self, conf): - assert conf.get_obj('colors.completion.fg') == 'white' + assert conf.get_obj('colors.completion.category.fg') == 'white' @pytest.mark.parametrize('option', ['content.headers.custom', 'keyhint.blacklist', diff --git a/tests/unit/config/test_configcommands.py b/tests/unit/config/test_configcommands.py index 7137f50db..4c0c833a1 100644 --- a/tests/unit/config/test_configcommands.py +++ b/tests/unit/config/test_configcommands.py @@ -281,7 +281,7 @@ class TestSource: def test_config_source(self, tmpdir, commands, config_stub, config_tmpdir, use_default_dir, clear): assert config_stub.val.content.javascript.enabled - config_stub.val.ignore_case = 'always' + config_stub.val.search.ignore_case = 'always' if use_default_dir: pyfile = config_tmpdir / 'config.py' @@ -295,7 +295,8 @@ class TestSource: commands.config_source(arg, clear=clear) assert not config_stub.val.content.javascript.enabled - assert config_stub.val.ignore_case == ('smart' if clear else 'always') + ignore_case = config_stub.val.search.ignore_case + assert ignore_case == ('smart' if clear else 'always') def test_errors(self, commands, config_tmpdir): pyfile = config_tmpdir / 'config.py' diff --git a/tests/unit/config/test_configdata.py b/tests/unit/config/test_configdata.py index 7edb1b0d6..b7e960ac6 100644 --- a/tests/unit/config/test_configdata.py +++ b/tests/unit/config/test_configdata.py @@ -34,7 +34,7 @@ def test_init(config_stub): # configdata.init() is called by config_stub config_stub.val.aliases = {} assert isinstance(configdata.DATA, dict) - assert 'ignore_case' in configdata.DATA + assert 'search.ignore_case' in configdata.DATA def test_data(config_stub): diff --git a/tests/unit/config/test_configfiles.py b/tests/unit/config/test_configfiles.py index 9efbc6a4e..e195f720c 100644 --- a/tests/unit/config/test_configfiles.py +++ b/tests/unit/config/test_configfiles.py @@ -24,13 +24,12 @@ import unittest.mock import textwrap import pytest +from PyQt5.QtCore import QSettings from qutebrowser.config import (config, configfiles, configexc, configdata, configtypes) from qutebrowser.utils import utils, usertypes -from PyQt5.QtCore import QSettings - @pytest.fixture(autouse=True) def configdata_init(): @@ -57,8 +56,7 @@ def test_state_config(fake_save_manager, data_tmpdir, if insert: state['general']['newval'] = '23' - # WORKAROUND for https://github.com/PyCQA/pylint/issues/574 - if 'foobar' in (old_data or ''): # pylint: disable=superfluous-parens + if 'foobar' in (old_data or ''): assert state['general']['foobar'] == '42' state._save() @@ -106,10 +104,7 @@ class TestYaml: print(lines) - # WORKAROUND for https://github.com/PyCQA/pylint/issues/574 - # pylint: disable=superfluous-parens if 'magenta' in (old_config or ''): - # pylint: enable=superfluous-parens assert ' colors.hints.fg: magenta' in lines if insert: assert ' tabs.show: never' in lines @@ -218,7 +213,7 @@ class TestYaml: ('%', 'While parsing', 'while scanning a directive'), ('global: 42', 'While loading data', "'global' object is not a dict"), ('foo: 42', 'While loading data', - "Toplevel object does not contain 'global' key"), + "Toplevel object does not contain 'global' key"), ('42', 'While loading data', "Toplevel object is not a dict"), ]) def test_invalid(self, yaml, config_tmpdir, line, text, exception): @@ -323,8 +318,9 @@ class TestConfigPyModules: sys.path = old_path def test_bind_in_module(self, confpy, qbmodulepy, tmpdir): - qbmodulepy.write('def run(config):', - ' config.bind(",a", "message-info foo", mode="normal")') + qbmodulepy.write( + 'def run(config):', + ' config.bind(",a", "message-info foo", mode="normal")') confpy.write_qbmodule() confpy.read() expected = {'normal': {',a': 'message-info foo'}} diff --git a/tests/unit/config/test_configinit.py b/tests/unit/config/test_configinit.py index 354920258..79ac43219 100644 --- a/tests/unit/config/test_configinit.py +++ b/tests/unit/config/test_configinit.py @@ -143,11 +143,11 @@ class TestEarlyInit: suffix = ' (autoconfig.yml)' if config_py else '' if invalid_yaml == '42': error = ("While loading data{}: Toplevel object is not a dict" - .format(suffix)) + .format(suffix)) expected_errors.append(error) elif invalid_yaml == 'wrong-type': error = ("Error{}: Invalid value 'True' - expected a value of " - "type str but got bool.".format(suffix)) + "type str but got bool.".format(suffix)) expected_errors.append(error) elif invalid_yaml == 'unknown': error = ("While loading options{}: Unknown option " diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index 1a4360479..556b3f9e2 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -340,13 +340,13 @@ class TestBaseType: @pytest.mark.parametrize('valid_values, completions', [ # Without description (['foo', 'bar'], - [('foo', ''), ('bar', '')]), + [('foo', ''), ('bar', '')]), # With description ([('foo', "foo desc"), ('bar', "bar desc")], - [('foo', "foo desc"), ('bar', "bar desc")]), + [('foo', "foo desc"), ('bar', "bar desc")]), # With mixed description ([('foo', "foo desc"), 'bar'], - [('foo', "foo desc"), ('bar', "")]), + [('foo', "foo desc"), ('bar', "")]), ]) def test_complete_without_desc(self, klass, valid_values, completions): """Test complete with valid_values set without description.""" @@ -489,9 +489,9 @@ class TestString: @pytest.mark.parametrize('valid_values, expected', [ (configtypes.ValidValues('one', 'two'), - [('one', ''), ('two', '')]), + [('one', ''), ('two', '')]), (configtypes.ValidValues(('1', 'one'), ('2', 'two')), - [('1', 'one'), ('2', 'two')]), + [('1', 'one'), ('2', 'two')]), ]) def test_complete_valid_values(self, klass, valid_values, expected): assert klass(valid_values=valid_values).complete() == expected @@ -1853,14 +1853,14 @@ class TestProxy: ('system', configtypes.SYSTEM_PROXY), ('none', QNetworkProxy(QNetworkProxy.NoProxy)), ('socks://example.com/', - QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com')), + QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com')), ('socks5://foo:bar@example.com:2323', - QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 2323, - 'foo', 'bar')), + QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 2323, + 'foo', 'bar')), ('pac+http://example.com/proxy.pac', - pac.PACFetcher(QUrl('pac+http://example.com/proxy.pac'))), + pac.PACFetcher(QUrl('pac+http://example.com/proxy.pac'))), ('pac+file:///tmp/proxy.pac', - pac.PACFetcher(QUrl('pac+file:///tmp/proxy.pac'))), + pac.PACFetcher(QUrl('pac+file:///tmp/proxy.pac'))), ]) def test_to_py_valid(self, klass, val, expected): actual = klass().to_py(val) diff --git a/tests/unit/javascript/conftest.py b/tests/unit/javascript/conftest.py index 8490a5362..c5c384c26 100644 --- a/tests/unit/javascript/conftest.py +++ b/tests/unit/javascript/conftest.py @@ -27,11 +27,6 @@ import pytest import jinja2 from PyQt5.QtCore import QUrl - -import helpers.utils -import qutebrowser.utils.debug -from qutebrowser.utils import utils - try: from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWebKitWidgets import QWebPage @@ -49,6 +44,10 @@ except ImportError: QWebEngineSettings = None QWebEngineScript = None +import helpers.utils +import qutebrowser.utils.debug +from qutebrowser.utils import utils + if QWebPage is None: TestWebPage = None diff --git a/tests/unit/javascript/test_greasemonkey.py b/tests/unit/javascript/test_greasemonkey.py new file mode 100644 index 000000000..0f5fe476c --- /dev/null +++ b/tests/unit/javascript/test_greasemonkey.py @@ -0,0 +1,104 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: +# Copyright 2017 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.browser.greasemonkey.""" + +import logging + +import pytest +import py.path # pylint: disable=no-name-in-module +from PyQt5.QtCore import QUrl + +from qutebrowser.browser import greasemonkey + +test_gm_script = """ +// ==UserScript== +// @name qutebrowser test userscript +// @namespace invalid.org +// @include http://localhost:*/data/title.html +// @match http://trolol* +// @exclude https://badhost.xxx/* +// @run-at document-start +// ==/UserScript== +console.log("Script is running."); +""" + +pytestmark = pytest.mark.usefixtures('data_tmpdir') + + +def _save_script(script_text, filename): + # pylint: disable=no-member + file_path = py.path.local(greasemonkey._scripts_dir()) / filename + # pylint: enable=no-member + file_path.write_text(script_text, encoding='utf-8', ensure=True) + + +def test_all(): + """Test that a script gets read from file, parsed and returned.""" + _save_script(test_gm_script, 'test.user.js') + + gm_manager = greasemonkey.GreasemonkeyManager() + assert (gm_manager.all_scripts()[0].name == + "qutebrowser test userscript") + + +@pytest.mark.parametrize("url, expected_matches", [ + # included + ('http://trololololololo.com/', 1), + # neither included nor excluded + ('http://aaaaaaaaaa.com/', 0), + # excluded + ('https://badhost.xxx/', 0), +]) +def test_get_scripts_by_url(url, expected_matches): + """Check Greasemonkey include/exclude rules work.""" + _save_script(test_gm_script, 'test.user.js') + gm_manager = greasemonkey.GreasemonkeyManager() + + scripts = gm_manager.scripts_for(QUrl(url)) + assert (len(scripts.start + scripts.end + scripts.idle) == + expected_matches) + + +def test_no_metadata(caplog): + """Run on all sites at document-end is the default.""" + _save_script("var nothing = true;\n", 'nothing.user.js') + + with caplog.at_level(logging.WARNING): + gm_manager = greasemonkey.GreasemonkeyManager() + + scripts = gm_manager.scripts_for(QUrl('http://notamatch.invalid/')) + assert len(scripts.start + scripts.end + scripts.idle) == 1 + assert len(scripts.end) == 1 + + +def test_bad_scheme(caplog): + """qute:// isn't in the list of allowed schemes.""" + _save_script("var nothing = true;\n", 'nothing.user.js') + + with caplog.at_level(logging.WARNING): + gm_manager = greasemonkey.GreasemonkeyManager() + + scripts = gm_manager.scripts_for(QUrl('qute://settings')) + assert len(scripts.start + scripts.end + scripts.idle) == 0 + + +def test_load_emits_signal(qtbot): + gm_manager = greasemonkey.GreasemonkeyManager() + with qtbot.wait_signal(gm_manager.scripts_reloaded): + gm_manager.load_scripts() diff --git a/tests/unit/keyinput/test_modeman.py b/tests/unit/keyinput/test_modeman.py index d00ce09f7..5185636a6 100644 --- a/tests/unit/keyinput/test_modeman.py +++ b/tests/unit/keyinput/test_modeman.py @@ -19,10 +19,10 @@ import pytest -from qutebrowser.utils import usertypes - from PyQt5.QtCore import Qt, QObject, pyqtSignal +from qutebrowser.utils import usertypes + class FakeKeyparser(QObject): diff --git a/tests/unit/misc/test_checkpyver.py b/tests/unit/misc/test_checkpyver.py index 6487eb263..b61fadc59 100644 --- a/tests/unit/misc/test_checkpyver.py +++ b/tests/unit/misc/test_checkpyver.py @@ -44,7 +44,7 @@ def test_python2(): pytest.skip("python2 not found") assert not proc.stdout stderr = proc.stderr.decode('utf-8') - assert re.match(TEXT, stderr), stderr + assert re.fullmatch(TEXT, stderr), stderr assert proc.returncode == 1 @@ -64,7 +64,7 @@ def test_patched_no_errwindow(capfd, monkeypatch): checkpyver.check_python_version() stdout, stderr = capfd.readouterr() assert not stdout - assert re.match(TEXT, stderr), stderr + assert re.fullmatch(TEXT, stderr), stderr def test_patched_errwindow(capfd, mocker, monkeypatch): diff --git a/tests/unit/misc/test_cmdhistory.py b/tests/unit/misc/test_cmdhistory.py index 44c6c2d82..0e6f9748e 100644 --- a/tests/unit/misc/test_cmdhistory.py +++ b/tests/unit/misc/test_cmdhistory.py @@ -84,8 +84,7 @@ def test_start_no_items(hist): def test_getitem(hist): """Test __getitem__.""" - for i in range(0, len(HISTORY)): - assert hist[i] == HISTORY[i] + assert hist[0] == HISTORY[0] def test_setitem(hist): diff --git a/tests/unit/misc/test_guiprocess.py b/tests/unit/misc/test_guiprocess.py index 674c250e5..78c55bf98 100644 --- a/tests/unit/misc/test_guiprocess.py +++ b/tests/unit/misc/test_guiprocess.py @@ -19,7 +19,6 @@ """Tests for qutebrowser.misc.guiprocess.""" -import json import logging import pytest @@ -27,6 +26,7 @@ from PyQt5.QtCore import QProcess, QIODevice from qutebrowser.misc import guiprocess from qutebrowser.utils import usertypes +from qutebrowser.browser import qutescheme @pytest.fixture() @@ -60,7 +60,7 @@ def test_start(proc, qtbot, message_mock, py_proc): proc.start(*argv) assert not message_mock.messages - assert bytes(proc._proc.readAll()).rstrip() == b'test' + assert qutescheme.spawn_output == proc._spawn_format(stdout="test") def test_start_verbose(proc, qtbot, message_mock, py_proc): @@ -77,7 +77,7 @@ def test_start_verbose(proc, qtbot, message_mock, py_proc): assert msgs[1].level == usertypes.MessageLevel.info assert msgs[0].text.startswith("Executing:") assert msgs[1].text == "Testprocess exited successfully." - assert bytes(proc._proc.readAll()).rstrip() == b'test' + assert qutescheme.spawn_output == proc._spawn_format(stdout="test") def test_start_env(monkeypatch, qtbot, py_proc): @@ -99,10 +99,9 @@ def test_start_env(monkeypatch, qtbot, py_proc): order='strict'): proc.start(*argv) - data = bytes(proc._proc.readAll()).decode('utf-8') - ret_env = json.loads(data) - assert 'QUTEBROWSER_TEST_1' in ret_env - assert 'QUTEBROWSER_TEST_2' in ret_env + data = qutescheme.spawn_output + assert 'QUTEBROWSER_TEST_1' in data + assert 'QUTEBROWSER_TEST_2' in data @pytest.mark.qt_log_ignore('QIODevice::read.*: WriteOnly device') @@ -225,3 +224,18 @@ def test_exit_successful_output(qtbot, proc, py_proc, stream): print("test", file=sys.{}) sys.exit(0) """.format(stream))) + + +def test_stdout_not_decodable(proc, qtbot, message_mock, py_proc): + """Test handling malformed utf-8 in stdout.""" + with qtbot.waitSignals([proc.started, proc.finished], timeout=10000, + order='strict'): + argv = py_proc(r""" + import sys + # Using \x81 because it's invalid in UTF-8 and CP1252 + sys.stdout.buffer.write(b"A\x81B") + sys.exit(0) + """) + proc.start(*argv) + assert not message_mock.messages + assert qutescheme.spawn_output == proc._spawn_format(stdout="A\ufffdB") diff --git a/tests/unit/misc/test_ipc.py b/tests/unit/misc/test_ipc.py index 281bd4ac4..fdb03cfd9 100644 --- a/tests/unit/misc/test_ipc.py +++ b/tests/unit/misc/test_ipc.py @@ -461,11 +461,11 @@ NEW_VERSION = str(ipc.PROTOCOL_VERSION + 1).encode('utf-8') (b'{"valid json without args": true}\n', 'Missing args'), (b'{"args": []}\n', 'Missing target_arg'), (b'{"args": [], "target_arg": null, "protocol_version": ' + OLD_VERSION + - b'}\n', 'incompatible version'), + b'}\n', 'incompatible version'), (b'{"args": [], "target_arg": null, "protocol_version": ' + NEW_VERSION + - b'}\n', 'incompatible version'), + b'}\n', 'incompatible version'), (b'{"args": [], "target_arg": null, "protocol_version": "foo"}\n', - 'invalid version'), + 'invalid version'), (b'{"args": [], "target_arg": null}\n', 'invalid version'), ]) def test_invalid_data(qtbot, ipc_server, connected_socket, caplog, data, msg): @@ -672,9 +672,9 @@ class TestSendOrListen: @pytest.mark.parametrize('has_error, exc_name, exc_msg', [ (True, 'SocketError', - 'Error while writing to running instance: Error string (error 0)'), + 'Error while writing to running instance: Error string (error 0)'), (False, 'AddressInUseError', - 'Error while listening to IPC server: Error string (error 8)'), + 'Error while listening to IPC server: Error string (error 8)'), ]) def test_address_in_use_error(self, qlocalserver_mock, qlocalsocket_mock, stubs, caplog, args, has_error, exc_name, @@ -737,7 +737,7 @@ class TestSendOrListen: 'pre_text: ', 'post_text: Maybe another instance is running but frozen?', ('exception text: Error while listening to IPC server: Error ' - 'string (error 4)'), + 'string (error 4)'), ] assert caplog.records[-1].msg == '\n'.join(error_msgs) diff --git a/tests/unit/misc/test_miscwidgets.py b/tests/unit/misc/test_miscwidgets.py index 3e2261cc0..1ee351a81 100644 --- a/tests/unit/misc/test_miscwidgets.py +++ b/tests/unit/misc/test_miscwidgets.py @@ -106,7 +106,7 @@ class TestFullscreenNotification: @pytest.mark.parametrize('bindings, text', [ ({'': 'fullscreen --leave'}, - "Press Escape to exit fullscreen."), + "Press Escape to exit fullscreen."), ({'': 'fullscreen'}, "Page is now fullscreen."), ({'a': 'fullscreen --leave'}, "Press a to exit fullscreen."), ({}, "Page is now fullscreen."), diff --git a/tests/unit/misc/test_msgbox.py b/tests/unit/misc/test_msgbox.py index cab72c251..783816011 100644 --- a/tests/unit/misc/test_msgbox.py +++ b/tests/unit/misc/test_msgbox.py @@ -20,12 +20,12 @@ import pytest -from qutebrowser.misc import msgbox -from qutebrowser.utils import utils - from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QMessageBox, QWidget +from qutebrowser.misc import msgbox +from qutebrowser.utils import utils + @pytest.fixture(autouse=True) def patch_args(fake_args): diff --git a/tests/unit/misc/test_sessions.py b/tests/unit/misc/test_sessions.py index 771430d5b..b2cb8a3dd 100644 --- a/tests/unit/misc/test_sessions.py +++ b/tests/unit/misc/test_sessions.py @@ -170,7 +170,7 @@ class TestSaveAll: ]) def test_get_session_name(config_stub, sess_man, arg, config, current, expected): - config_stub.val.session_default_name = config + config_stub.val.session.default_name = config sess_man._current = current assert sess_man._get_session_name(arg) == expected diff --git a/tests/unit/misc/test_sql.py b/tests/unit/misc/test_sql.py index 89c442b2b..048826513 100644 --- a/tests/unit/misc/test_sql.py +++ b/tests/unit/misc/test_sql.py @@ -20,10 +20,11 @@ """Test the SQL API.""" import pytest -from qutebrowser.misc import sql from PyQt5.QtSql import QSqlError +from qutebrowser.misc import sql + pytestmark = pytest.mark.usefixtures('init_sql') @@ -156,13 +157,13 @@ def test_iter(): @pytest.mark.parametrize('rows, sort_by, sort_order, limit, result', [ ([{"a": 2, "b": 5}, {"a": 1, "b": 6}, {"a": 3, "b": 4}], 'a', 'asc', 5, - [(1, 6), (2, 5), (3, 4)]), + [(1, 6), (2, 5), (3, 4)]), ([{"a": 2, "b": 5}, {"a": 1, "b": 6}, {"a": 3, "b": 4}], 'a', 'desc', 3, - [(3, 4), (2, 5), (1, 6)]), + [(3, 4), (2, 5), (1, 6)]), ([{"a": 2, "b": 5}, {"a": 1, "b": 6}, {"a": 3, "b": 4}], 'b', 'desc', 2, - [(1, 6), (2, 5)]), + [(1, 6), (2, 5)]), ([{"a": 2, "b": 5}, {"a": 1, "b": 6}, {"a": 3, "b": 4}], 'a', 'asc', -1, - [(1, 6), (2, 5), (3, 4)]), + [(1, 6), (2, 5), (3, 4)]), ]) def test_select(rows, sort_by, sort_order, limit, result): table = sql.SqlTable('Foo', ['a', 'b']) diff --git a/tests/unit/scripts/test_check_coverage.py b/tests/unit/scripts/test_check_coverage.py index 6b18568c5..d7183111f 100644 --- a/tests/unit/scripts/test_check_coverage.py +++ b/tests/unit/scripts/test_check_coverage.py @@ -50,6 +50,7 @@ class CovtestHelper: def run(self): """Run pytest with coverage for the given module.py.""" coveragerc = str(self._testdir.tmpdir / 'coveragerc') + self._monkeypatch.delenv('PYTEST_ADDOPTS', raising=False) return self._testdir.runpytest('--cov=module', '--cov-config={}'.format(coveragerc), '--cov-report=xml', diff --git a/tests/unit/scripts/test_dictcli.py b/tests/unit/scripts/test_dictcli.py index e5408ef68..3471251ba 100644 --- a/tests/unit/scripts/test_dictcli.py +++ b/tests/unit/scripts/test_dictcli.py @@ -22,8 +22,8 @@ import py.path # pylint: disable=no-name-in-module import pytest from qutebrowser.browser.webengine import spell -from scripts import dictcli from qutebrowser.config import configdata +from scripts import dictcli def afrikaans(): diff --git a/tests/unit/scripts/test_importer.py b/tests/unit/scripts/test_importer.py index 9edab7600..7c9ada1c4 100644 --- a/tests/unit/scripts/test_importer.py +++ b/tests/unit/scripts/test_importer.py @@ -27,28 +27,22 @@ _samples = '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: + with open(os.path.join(_samples, input_format, 'quickmarks'), + 'r', encoding='utf-8') as f: return f.read() 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: + with open(os.path.join(_samples, input_format, 'bookmarks'), + 'r', encoding='utf-8') as f: return f.read() 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: + with open(os.path.join(_samples, input_format, 'config_py'), + 'r', encoding='utf-8') as f: return f.read() diff --git a/tests/unit/utils/test_debug.py b/tests/unit/utils/test_debug.py index e32f564c8..b7b5cf3ca 100644 --- a/tests/unit/utils/test_debug.py +++ b/tests/unit/utils/test_debug.py @@ -90,8 +90,8 @@ class TestLogTime: assert len(caplog.records) == 1 - pattern = re.compile(r'^Foobar took ([\d.]*) seconds\.$') - match = pattern.match(caplog.records[0].msg) + pattern = re.compile(r'Foobar took ([\d.]*) seconds\.') + match = pattern.fullmatch(caplog.records[0].msg) assert match duration = float(match.group(1)) diff --git a/tests/unit/utils/test_error.py b/tests/unit/utils/test_error.py index 47a1c52d9..43ebc01b7 100644 --- a/tests/unit/utils/test_error.py +++ b/tests/unit/utils/test_error.py @@ -21,13 +21,12 @@ import logging import pytest +from PyQt5.QtCore import QTimer +from PyQt5.QtWidgets import QMessageBox from qutebrowser.utils import error, utils from qutebrowser.misc import ipc -from PyQt5.QtCore import QTimer -from PyQt5.QtWidgets import QMessageBox - class Error(Exception): diff --git a/tests/unit/utils/test_log.py b/tests/unit/utils/test_log.py index 30dd5d634..fb2723cc6 100644 --- a/tests/unit/utils/test_log.py +++ b/tests/unit/utils/test_log.py @@ -27,13 +27,12 @@ import warnings import attr import pytest -import pytest_catchlog +import _pytest.logging +from PyQt5 import QtCore from qutebrowser.utils import log from qutebrowser.misc import utilcmds -from PyQt5 import QtCore - @pytest.fixture(autouse=True) def restore_loggers(): @@ -66,11 +65,11 @@ def restore_loggers(): while root_logger.handlers: h = root_logger.handlers[0] root_logger.removeHandler(h) - if not isinstance(h, pytest_catchlog.LogCaptureHandler): + if not isinstance(h, _pytest.logging.LogCaptureHandler): h.close() root_logger.setLevel(original_logging_level) for h in root_handlers: - if not isinstance(h, pytest_catchlog.LogCaptureHandler): + if not isinstance(h, _pytest.logging.LogCaptureHandler): # https://github.com/qutebrowser/qutebrowser/issues/856 root_logger.addHandler(h) logging._acquireLock() diff --git a/tests/unit/utils/test_qtutils.py b/tests/unit/utils/test_qtutils.py index d580d677a..b817eeed0 100644 --- a/tests/unit/utils/test_qtutils.py +++ b/tests/unit/utils/test_qtutils.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . + """Tests for qutebrowser.utils.qtutils.""" import io @@ -40,6 +41,7 @@ from qutebrowser.utils import qtutils, utils import overflow_test_cases +# pylint: disable=bad-continuation @pytest.mark.parametrize(['qversion', 'compiled', 'pyqt', 'version', 'exact', 'expected'], [ # equal versions @@ -61,6 +63,7 @@ import overflow_test_cases # all up-to-date ('5.4.0', '5.4.0', '5.4.0', '5.4.0', False, True), ]) +# pylint: enable=bad-continuation def test_version_check(monkeypatch, qversion, compiled, pyqt, version, exact, expected): """Test for version_check(). diff --git a/tests/unit/utils/test_standarddir.py b/tests/unit/utils/test_standarddir.py index 5a3f74a66..e13b5e917 100644 --- a/tests/unit/utils/test_standarddir.py +++ b/tests/unit/utils/test_standarddir.py @@ -20,9 +20,9 @@ """Tests for qutebrowser.utils.standarddir.""" import os +import os.path import sys import json -import os.path import types import textwrap import logging diff --git a/tests/unit/utils/test_urlutils.py b/tests/unit/utils/test_urlutils.py index cbfc31055..4d9b3ba0a 100644 --- a/tests/unit/utils/test_urlutils.py +++ b/tests/unit/utils/test_urlutils.py @@ -492,18 +492,14 @@ def test_filename_from_url(qurl, output): (QUrl('qute://'), None), (QUrl('qute://foobar'), None), (QUrl('mailto:nobody'), None), - (QUrl('ftp://example.com/'), - ('ftp', 'example.com', 21)), - (QUrl('ftp://example.com:2121/'), - ('ftp', 'example.com', 2121)), + (QUrl('ftp://example.com/'), ('ftp', 'example.com', 21)), + (QUrl('ftp://example.com:2121/'), ('ftp', 'example.com', 2121)), (QUrl('http://qutebrowser.org:8010/waterfall'), - ('http', 'qutebrowser.org', 8010)), - (QUrl('https://example.com/'), - ('https', 'example.com', 443)), - (QUrl('https://example.com:4343/'), - ('https', 'example.com', 4343)), + ('http', 'qutebrowser.org', 8010)), + (QUrl('https://example.com/'), ('https', 'example.com', 443)), + (QUrl('https://example.com:4343/'), ('https', 'example.com', 4343)), (QUrl('http://user:password@qutebrowser.org/foo?bar=baz#fish'), - ('http', 'qutebrowser.org', 80)), + ('http', 'qutebrowser.org', 80)), ]) def test_host_tuple(qurl, tpl): """Test host_tuple(). @@ -752,7 +748,7 @@ def test_data_url(): (QUrl('http://www.example.com/ä'), 'http://www.example.com/ä'), # Unicode only in TLD (looks like Qt shows Punycode with рф...) (QUrl('http://www.example.xn--p1ai'), - '(www.example.xn--p1ai) http://www.example.рф'), + '(www.example.xn--p1ai) http://www.example.рф'), # https://bugreports.qt.io/browse/QTBUG-60364 pytest.param(QUrl('http://www.xn--80ak6aa92e.com'), '(unparseable URL!) http://www.аррӏе.com', @@ -779,19 +775,19 @@ class TestProxyFromUrl: @pytest.mark.parametrize('url, expected', [ ('socks://example.com/', - QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com')), + QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com')), ('socks5://example.com', - QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com')), + QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com')), ('socks5://example.com:2342', - QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 2342)), + QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 2342)), ('socks5://foo@example.com', - QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 0, 'foo')), + QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 0, 'foo')), ('socks5://foo:bar@example.com', - QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 0, 'foo', - 'bar')), + QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 0, 'foo', + 'bar')), ('socks5://foo:bar@example.com:2323', - QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 2323, - 'foo', 'bar')), + QNetworkProxy(QNetworkProxy.Socks5Proxy, 'example.com', 2323, + 'foo', 'bar')), ('direct://', QNetworkProxy(QNetworkProxy.NoProxy)), ]) def test_proxy_from_url_valid(self, url, expected): diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index f6eef7f4f..28837e93c 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -376,7 +376,7 @@ class TestKeyEventToString: ('', utils.KeyInfo(Qt.Key_X, Qt.ControlModifier, '')), ('', utils.KeyInfo(Qt.Key_X, Qt.MetaModifier, '')), ('', - utils.KeyInfo(Qt.Key_Y, Qt.ControlModifier | Qt.AltModifier, '')), + utils.KeyInfo(Qt.Key_Y, Qt.ControlModifier | Qt.AltModifier, '')), ('x', utils.KeyInfo(Qt.Key_X, Qt.NoModifier, 'x')), ('X', utils.KeyInfo(Qt.Key_X, Qt.ShiftModifier, 'X')), ('', utils.KeyInfo(Qt.Key_Escape, Qt.NoModifier, '')), @@ -863,7 +863,7 @@ class TestOpenFile: cmdline = '{} -c pass'.format(executable) utils.open_file('/foo/bar', cmdline) result = caplog.records[0].message - assert re.match( + assert re.fullmatch( r'Opening /foo/bar with \[.*python.*/foo/bar.*\]', result) @pytest.mark.not_frozen @@ -872,7 +872,7 @@ class TestOpenFile: cmdline = '{} -c pass {{}} raboof'.format(executable) utils.open_file('/foo/bar', cmdline) result = caplog.records[0].message - assert re.match( + assert re.fullmatch( r"Opening /foo/bar with \[.*python.*/foo/bar.*'raboof'\]", result) @pytest.mark.not_frozen @@ -882,7 +882,7 @@ class TestOpenFile: config_stub.val.downloads.open_dispatcher = cmdline utils.open_file('/foo/bar') result = caplog.records[1].message - assert re.match( + assert re.fullmatch( r"Opening /foo/bar with \[.*python.*/foo/bar.*\]", result) def test_system_default_application(self, caplog, config_stub, mocker): @@ -890,7 +890,7 @@ class TestOpenFile: new_callable=mocker.Mock) utils.open_file('/foo/bar') result = caplog.records[0].message - assert re.match( + assert re.fullmatch( r"Opening /foo/bar with the system application", result) m.assert_called_with(QUrl('file:///foo/bar')) diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py index 3d01fcfb5..aa9df5419 100644 --- a/tests/unit/utils/test_version.py +++ b/tests/unit/utils/test_version.py @@ -467,12 +467,12 @@ def test_path_info(monkeypatch, equal): equal: Whether system data / data and system config / config are equal. """ patches = { - 'config': lambda auto=False: + 'config': lambda auto=False: ( 'AUTO CONFIG PATH' if auto and not equal - else 'CONFIG PATH', - 'data': lambda system=False: + else 'CONFIG PATH'), + 'data': lambda system=False: ( 'SYSTEM DATA PATH' if system and not equal - else 'DATA PATH', + else 'DATA PATH'), 'cache': lambda: 'CACHE PATH', 'runtime': lambda: 'RUNTIME PATH', } @@ -795,7 +795,7 @@ class FakeQSslSocket: def sslLibraryVersionString(self): """Fake for QSslSocket::sslLibraryVersionString().""" if self._version is None: - raise AssertionError("Got called with version None!") + raise utils.Unreachable("Got called with version None!") return self._version diff --git a/tests/unit/utils/usertypes/test_neighborlist.py b/tests/unit/utils/usertypes/test_neighborlist.py index 751f940a3..804ed51ed 100644 --- a/tests/unit/utils/usertypes/test_neighborlist.py +++ b/tests/unit/utils/usertypes/test_neighborlist.py @@ -19,10 +19,10 @@ """Tests for the NeighborList class.""" -from qutebrowser.utils import usertypes - import pytest +from qutebrowser.utils import usertypes + class TestInit: diff --git a/tests/unit/utils/usertypes/test_timer.py b/tests/unit/utils/usertypes/test_timer.py index d120d82e6..81ea6feae 100644 --- a/tests/unit/utils/usertypes/test_timer.py +++ b/tests/unit/utils/usertypes/test_timer.py @@ -19,11 +19,11 @@ """Tests for Timer.""" -from qutebrowser.utils import usertypes - import pytest from PyQt5.QtCore import QObject +from qutebrowser.utils import usertypes + class Parent(QObject): diff --git a/tox.ini b/tox.ini index c0395a621..5b8bc05b5 100644 --- a/tox.ini +++ b/tox.ini @@ -13,134 +13,29 @@ skipsdist = true setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/platforms PYTEST_QT_API=pyqt5 + pyqt{,56,571,58,59}: LINK_PYQT_SKIP=true + pyqt{,56,571,58,59}: 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 +basepython = + py35: python3.5 + py36: python3.6 deps = -r{toxinidir}/requirements.txt -r{toxinidir}/misc/requirements/requirements-tests.txt + pyqt: -r{toxinidir}/misc/requirements/requirements-pyqt.txt + pyqt571: PyQt5==5.7.1 + pyqt58: PyQt5==5.8.2 + pyqt59: PyQt5==5.9.2 commands = {envpython} scripts/link_pyqt.py --tox {envdir} {envpython} -bb -m pytest {posargs:tests} - -# test envs with PyQt5 from PyPI - -[testenv:py35-pyqt56] -basepython = python3.5 -setenv = - {[testenv]setenv} - QUTE_BDD_WEBENGINE=true -passenv = {[testenv]passenv} -deps = - {[testenv]deps} - PyQt5==5.6 -commands = {envpython} -bb -m pytest {posargs:tests} - -[testenv:py35-pyqt571] -basepython = python3.5 -setenv = - {[testenv]setenv} - QUTE_BDD_WEBENGINE=true -passenv = {[testenv]passenv} -deps = - {[testenv]deps} - PyQt5==5.7.1 -commands = {envpython} -bb -m pytest {posargs:tests} - -[testenv:py36-pyqt571] -basepython = python3.6 -setenv = - {[testenv]setenv} - QUTE_BDD_WEBENGINE=true -passenv = {[testenv]passenv} -deps = - {[testenv]deps} - PyQt5==5.7.1 -commands = {envpython} -bb -m pytest {posargs:tests} - -[testenv:py35-pyqt58] -basepython = python3.5 -setenv = - {[testenv]setenv} - QUTE_BDD_WEBENGINE=true -passenv = {[testenv]passenv} -deps = - {[testenv]deps} - PyQt5==5.8.2 -commands = {envpython} -bb -m pytest {posargs:tests} - -[testenv:py36-pyqt58] -basepython = {env:PYTHON:python3.6} -setenv = - {[testenv]setenv} - QUTE_BDD_WEBENGINE=true -passenv = {[testenv]passenv} -deps = - {[testenv]deps} - PyQt5==5.8.2 -commands = {envpython} -bb -m pytest {posargs:tests} - -[testenv:py35-pyqt59] -basepython = python3.5 -setenv = - {[testenv]setenv} - QUTE_BDD_WEBENGINE=true -passenv = {[testenv]passenv} -deps = - {[testenv]deps} - PyQt5==5.9.2 -commands = {envpython} -bb -m pytest {posargs:tests} - -[testenv:py36-pyqt59] -basepython = {env:PYTHON:python3.6} -setenv = - {[testenv]setenv} - QUTE_BDD_WEBENGINE=true -passenv = {[testenv]passenv} -deps = - {[testenv]deps} - PyQt5==5.9.2 -commands = {envpython} -bb -m pytest {posargs:tests} - -# test envs with coverage - -[testenv:py35-pyqt59-cov] -basepython = python3.5 -setenv = - {[testenv]setenv} - QUTE_BDD_WEBENGINE=true -passenv = {[testenv]passenv} -deps = - {[testenv]deps} - PyQt5==5.9.2 -commands = - {envpython} -bb -m pytest --cov --cov-report xml --cov-report=html --cov-report= {posargs:tests} - {envpython} scripts/dev/check_coverage.py {posargs} - -[testenv:py36-pyqt59-cov] -basepython = python3.6 -setenv = - {[testenv]setenv} - QUTE_BDD_WEBENGINE=true -passenv = {[testenv]passenv} -deps = - {[testenv]deps} - PyQt5==5.9.2 -commands = - {envpython} -bb -m pytest --cov --cov-report xml --cov-report=html --cov-report= {posargs:tests} - {envpython} scripts/dev/check_coverage.py {posargs} + cov: {envpython} scripts/dev/check_coverage.py {posargs} # other envs [testenv:mkvenv] -basepython = python3 -commands = {envpython} scripts/link_pyqt.py --tox {envdir} -envdir = {toxinidir}/.venv -usedevelop = true -deps = - -r{toxinidir}/requirements.txt - -# This is used for Windows, since binary name is different -[testenv:mkvenv-win] -basepython = python.exe +basepython = {env:PYTHON:python3} commands = {envpython} scripts/link_pyqt.py --tox {envdir} envdir = {toxinidir}/.venv usedevelop = true @@ -157,7 +52,7 @@ deps = {[testenv:mkvenv]deps} # Virtualenv with PyQt5 from PyPI [testenv:mkvenv-pypi] -basepython = python3 +basepython = {env:PYTHON:python3} envdir = {toxinidir}/.venv commands = {envpython} -c "" usedevelop = true @@ -165,19 +60,9 @@ deps = -r{toxinidir}/requirements.txt -r{toxinidir}/misc/requirements/requirements-pyqt.txt -# This is used for Windows, since binary name is different -[testenv:mkvenv-win-pypi] -basepython = python.exe -commands = {envpython} -c "" -envdir = {toxinidir}/.venv -usedevelop = true -deps = - -r{toxinidir}/requirements.txt - -r{toxinidir}/misc/requirements/requirements-pyqt.txt - [testenv:misc] ignore_errors = true -basepython = python3 +basepython = {env:PYTHON:python3} # For global .gitignore files passenv = HOME deps = @@ -187,7 +72,7 @@ commands = {envpython} scripts/dev/misc_checks.py spelling [testenv:vulture] -basepython = python3 +basepython = {env:PYTHON:python3} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/misc/requirements/requirements-vulture.txt @@ -196,23 +81,48 @@ setenv = PYTHONPATH={toxinidir} commands = {envpython} scripts/dev/run_vulture.py +[testenv:vulture-pyqtlink] +basepython = {env:PYTHON:python3} +deps = + -r{toxinidir}/requirements.txt + -r{toxinidir}/misc/requirements/requirements-vulture.txt +setenv = PYTHONPATH={toxinidir} +commands = + {envpython} scripts/link_pyqt.py --tox {envdir} + {[testenv:vulture]commands} + [testenv:pylint] basepython = {env:PYTHON:python3} ignore_errors = true passenv = deps = - {[testenv]deps} + -r{toxinidir}/requirements.txt + -r{toxinidir}/misc/requirements/requirements-tests.txt -r{toxinidir}/misc/requirements/requirements-pylint.txt -r{toxinidir}/misc/requirements/requirements-pyqt.txt commands = {envpython} -m pylint scripts qutebrowser --output-format=colorized --reports=no {posargs} {envpython} scripts/dev/run_pylint_on_tests.py {toxinidir} --output-format=colorized --reports=no {posargs} +[testenv:pylint-pyqtlink] +basepython = {env:PYTHON:python3} +ignore_errors = true +passenv = +deps = + -r{toxinidir}/requirements.txt + -r{toxinidir}/misc/requirements/requirements-tests.txt + -r{toxinidir}/misc/requirements/requirements-pylint.txt +commands = + {envpython} scripts/link_pyqt.py --tox {envdir} + {envpython} -m pylint scripts qutebrowser --output-format=colorized --reports=no {posargs} + {envpython} scripts/dev/run_pylint_on_tests.py {toxinidir} --output-format=colorized --reports=no {posargs} + [testenv:pylint-master] -basepython = python3 +basepython = {env:PYTHON:python3} passenv = {[testenv:pylint]passenv} deps = - {[testenv]deps} + -r{toxinidir}/requirements.txt + -r{toxinidir}/misc/requirements/requirements-tests.txt -r{toxinidir}/misc/requirements/requirements-pylint-master.txt commands = {envpython} scripts/link_pyqt.py --tox {envdir} @@ -220,7 +130,7 @@ commands = {envpython} scripts/dev/run_pylint_on_tests.py --output-format=colorized --reports=no {posargs} [testenv:flake8] -basepython = python3 +basepython = {env:PYTHON:python3} passenv = deps = -r{toxinidir}/requirements.txt @@ -229,7 +139,7 @@ commands = {envpython} -m flake8 {posargs:qutebrowser tests scripts} [testenv:pyroma] -basepython = python3 +basepython = {env:PYTHON:python3} passenv = deps = -r{toxinidir}/misc/requirements/requirements-pyroma.txt @@ -237,7 +147,7 @@ commands = {envdir}/bin/pyroma . [testenv:check-manifest] -basepython = python3 +basepython = {env:PYTHON:python3} passenv = deps = -r{toxinidir}/misc/requirements/requirements-check-manifest.txt @@ -245,7 +155,7 @@ commands = {envdir}/bin/check-manifest --ignore 'qutebrowser/git-commit-id,qutebrowser/html/doc,qutebrowser/html/doc/*,*/__pycache__' [testenv:docs] -basepython = python3 +basepython = {env:PYTHON:python3} whitelist_externals = git passenv = TRAVIS TRAVIS_PULL_REQUEST deps = @@ -271,6 +181,7 @@ commands = [testenv:eslint] # This is duplicated in travis_run.sh for Travis CI because we can't get tox in # the JavaScript environment easily. +basepython = python3 deps = whitelist_externals = eslint changedir = {toxinidir}/qutebrowser/javascript