diff options
author | Árni Dagur <arni@dagur.eu> | 2020-12-19 20:29:38 +0000 |
---|---|---|
committer | Árni Dagur <arni@dagur.eu> | 2020-12-19 20:29:38 +0000 |
commit | 4c5e3d9ea084024f0b88a43963aeb45ce37bfd40 (patch) | |
tree | 7ac785e913c775a5b43f43502db86ed95836c188 | |
parent | 918d4c5e19a27bad3a1c132d3ce2f3b304ee4365 (diff) | |
parent | 8303a99ddb3f2dc6465fa324a181081cdd7787c0 (diff) | |
download | qutebrowser-4c5e3d9ea084024f0b88a43963aeb45ce37bfd40.tar.gz qutebrowser-4c5e3d9ea084024f0b88a43963aeb45ce37bfd40.zip |
Merge branch 'master' into more-sophisticated-adblock
182 files changed, 2437 insertions, 1750 deletions
diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 2628059be..fa5e2a66e 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.13.1 +current_version = 1.14.0 commit = True message = Release v{new_version} tag = True diff --git a/.coveragerc b/.coveragerc index 9d43917a3..cb0619b80 100644 --- a/.coveragerc +++ b/.coveragerc @@ -18,6 +18,7 @@ exclude_lines = raise utils\.Unreachable if __name__ == ["']__main__["']: if typing.TYPE_CHECKING: + if TYPE_CHECKING: \.\.\. [xml] @@ -39,6 +39,7 @@ exclude = .*,__pycache__,resources.py # A003: Builtin name for class attribute (needed for overridden methods) # W503: like break before binary operator # W504: line break after binary operator +# FI15: __future__ import "generator_stop" missing ignore = B001,B008,B305, E128,E226,E265,E501,E402,E266,E722,E731, @@ -48,7 +49,8 @@ ignore = D102,D103,D106,D107,D104,D105,D209,D211,D401,D402,D403,D412,D413, A003, W503, W504 -min-version = 3.4.0 + FI15 +min-version = 3.6.0 max-complexity = 12 per-file-ignores = qutebrowser/api/hook.py : N801 diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 123014908..000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,6 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36423aab8..182f935be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,10 +39,10 @@ jobs: .tox ~/.cache/pip key: "${{ matrix.testenv }}-${{ hashFiles('misc/requirements/requirements-*.txt') }}-${{ hashFiles('requirements.txt') }}" - - uses: actions/setup-python@v2.1.2 + - uses: actions/setup-python@v2 with: python-version: '3.8' - - uses: actions/setup-node@v2.1.1 + - uses: actions/setup-node@v2-beta with: node-version: '12.x' if: "matrix.testenv == 'eslint'" @@ -57,7 +57,7 @@ jobs: bindir="$HOME/.local/bin" mkdir -p "$bindir" wget -qO- "https://github.com/koalaman/shellcheck/releases/download/$scversion/shellcheck-$scversion.linux.x86_64.tar.xz" | tar -xJv --strip-components 1 -C "$bindir" shellcheck-$scversion/shellcheck - echo "::add-path::$bindir" + echo "$bindir" >> "$GITHUB_PATH" fi python -m pip install -U pip python -m pip install -U -r misc/requirements/requirements-tox.txt @@ -100,11 +100,6 @@ jobs: fail-fast: false matrix: include: - ### PyQt 5.7.1 (Python 3.5) - # - testenv: py35-pyqt57 - # os: ubuntu-16.04 - # python: 3.5 - # experimental: true ### PyQt 5.9 (Python 3.6) - testenv: py36-pyqt59 os: ubuntu-18.04 @@ -132,7 +127,7 @@ jobs: ### PyQt 5.15 (Python 3.9) - testenv: py39-pyqt515 os: ubuntu-20.04 - python: 3.9-dev + python: 3.9 ### PyQt 5.15 (Python 3.8, with coverage) - testenv: py38-pyqt515-cov os: ubuntu-20.04 @@ -157,7 +152,7 @@ jobs: ~/.cache/pip key: "${{ matrix.testenv }}-${{ matrix.os }}-${{ matrix.python }}-${{ hashFiles('misc/requirements/requirements-*.txt') }}-${{ hashFiles('requirements.txt') }}" - name: Set up Python - uses: actions/setup-python@v2.1.2 + uses: actions/setup-python@v2 with: python-version: "${{ matrix.python }}" - name: Set up problem matchers @@ -178,7 +173,7 @@ jobs: if: "failure()" - name: Upload coverage if: "endsWith(matrix.testenv, '-cov')" - uses: codecov/codecov-action@v1.0.13 + uses: codecov/codecov-action@v1 with: name: "${{ matrix.testenv }}" @@ -212,7 +207,7 @@ jobs: if: "always() && github.repository_owner == 'qutebrowser'" steps: - name: Send success IRC notification - uses: Gottox/irc-message-action@v1 + uses: Gottox/irc-message-action@v1.1 if: "needs.linters.result == 'success' && needs.tests.result == 'success' && needs.tests-docker.result == 'success' && needs.codeql.result == 'success'" with: server: chat.freenode.net @@ -220,7 +215,7 @@ jobs: nickname: qutebrowser-bot message: "[${{ github.workflow }}] \u00033Success:\u0003 ${{ github.ref }} https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} (@${{ github.actor }})" - name: Send failure IRC notification - uses: Gottox/irc-message-action@v1 + uses: Gottox/irc-message-action@v1.1 if: "needs.linters.result == 'failure' || needs.tests.result == 'failure' || needs.tests-docker.result == 'failure' || needs.codeql.result == 'failure'" with: server: chat.freenode.net @@ -229,7 +224,7 @@ jobs: message: "[${{ github.workflow }}] \u00034FAIL:\u0003 ${{ github.ref }} https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} (@${{ github.actor }})\n linters: ${{ needs.linters.result }}, tests: ${{ needs.tests.result }}, tests-docker: ${{ needs.tests-docker.result }}, codeql: ${{ needs.codeql.result }}" - name: Send skipped IRC notification - uses: Gottox/irc-message-action@v1 + uses: Gottox/irc-message-action@v1.1 if: "needs.linters.result == 'skipped' || needs.tests.result == 'skipped' || needs.tests-docker.result == 'skipped' || needs.codeql.result == 'skipped'" with: server: chat.freenode.net @@ -237,7 +232,7 @@ jobs: nickname: qutebrowser-bot message: "[${{ github.workflow }}] \u00038Skipped:\u0003 ${{ github.ref }} https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} (@${{ github.actor }})" - name: Send cancelled IRC notification - uses: Gottox/irc-message-action@v1 + uses: Gottox/irc-message-action@v1.1 if: "needs.linters.result == 'cancelled' || needs.tests.result == 'cancelled' || needs.tests-docker.result == 'cancelled' || needs.codeql.result == 'cancelled'" with: server: chat.freenode.net diff --git a/.github/workflows/recompile-requirements.yml b/.github/workflows/recompile-requirements.yml index 045f2ee1e..c41f67810 100644 --- a/.github/workflows/recompile-requirements.yml +++ b/.github/workflows/recompile-requirements.yml @@ -20,11 +20,11 @@ jobs: steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 - uses: actions/setup-python@v2.1.2 + uses: actions/setup-python@v2 with: python-version: '3.7' - name: Set up Python 3.8 - uses: actions/setup-python@v2.1.2 + uses: actions/setup-python@v2 with: python-version: '3.8' - name: Recompile requirements @@ -60,7 +60,7 @@ jobs: if: "always() && github.repository == 'qutebrowser/qutebrowser'" steps: - name: Send success IRC notification - uses: Gottox/irc-message-action@v1 + uses: Gottox/irc-message-action@v1.1 if: "needs.update.result == 'success'" with: server: chat.freenode.net @@ -68,7 +68,7 @@ jobs: nickname: qutebrowser-bot message: "[${{ github.workflow }}] \u00033Success:\u0003 ${{ github.ref }} https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} (@${{ github.actor }})" - name: Send non-success IRC notification - uses: Gottox/irc-message-action@v1 + uses: Gottox/irc-message-action@v1.1 if: "needs.update.result != 'success'" with: server: chat.freenode.net diff --git a/.gitignore b/.gitignore index aa5b853f7..50c67dee4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,15 @@ __pycache__ *.py~ *.pyc -*.swp +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] +[._]*.un~ +Session.vim +Sessionx.vim +*~ /build /dist /qutebrowser.egg-info @@ -1,6 +1,4 @@ [mypy] -# We also need to support 3.5, but if we'd chose that here, we'd need to deal -# with conditional imports (like secrets.py). python_version = 3.6 # --strict @@ -115,6 +113,9 @@ disallow_untyped_defs = True [mypy-qutebrowser.browser.webengine.webengineelem] disallow_untyped_defs = True +[mypy-qutebrowser.browser.webengine.darkmode] +disallow_untyped_defs = True + [mypy-qutebrowser.keyinput.*] disallow_untyped_defs = True diff --git a/.travis.yml b/.travis.yml index 9a56a756c..b75081477 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ dist: xenial language: python -python: 3.5 +python: 3.6 os: linux -env: TESTENV=py35-pyqt57 +env: TESTENV=py36-pyqt57 install: - python -m pip install -U pip diff --git a/README.asciidoc b/README.asciidoc index 4da2c0edc..903c1415e 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -109,8 +109,7 @@ Requirements The following software and libraries are required to run qutebrowser: -* https://www.python.org/[Python] 3.5.2 or newer (3.6 - 3.8 recommended; - support for 3.5 will be dropped with qutebrowser v2.0.0) +* https://www.python.org/[Python] 3.6 or newer * https://www.qt.io/[Qt] 5.7.1 or newer (5.14 recommended; support for < 5.11 will be dropped with qutebrowser v2.0.0) with the following modules: - QtCore / qtbase @@ -219,11 +218,11 @@ Active * https://fanglingsu.github.io/vimb/[vimb] (C, GTK+ with WebKit2) * https://luakit.github.io/luakit/[luakit] (C/Lua, GTK+ with WebKit2) -* https://surf.suckless.org/[surf] (C, GTK+ with WebKit1/WebKit2) * https://nyxt.atlas.engineer/[Nyxt browser] (formerly "Next browser", Lisp, Emacs-like but also offers Vim bindings, QtWebKit or GTK+/WebKit2 - note there was a http://jgkamat.gitlab.io/blog/next-rce.html[critical remote code execution] which was handled quite badly) * https://vieb.dev/[Vieb] (JavaScript, Electron) * Chrome/Chromium addons: https://vimium.github.io/[Vimium], + https://github.com/dcchambers/vb4c[vb4c] (fork of cVim) * Firefox addons (based on WebExtensions): https://github.com/tridactyl/tridactyl[Tridactyl], https://addons.mozilla.org/en-GB/firefox/addon/vimium-ff/[Vimium-FF] (experimental), @@ -231,7 +230,6 @@ Active https://github.com/amedama41/vvimpulation[VVimpulation] * Addons for Firefox and Chrome: https://github.com/brookhong/Surfingkeys[Surfingkeys], - https://github.com/lusakasa/saka-key[Saka Key], https://krabby.netlify.com/[Krabby], https://lydell.github.io/LinkHints/[Link Hints] (hinting only) * Addons for Safari: @@ -253,6 +251,7 @@ original site is gone but the Arch Linux wiki has some data) * https://www.uzbl.org/[uzbl] (C, GTK+ with WebKit1/WebKit2) * https://github.com/conformal/xombrero[xombrero] (C, GTK+ with WebKit1) * https://github.com/linkdd/cream-browser[Cream Browser] (C, GTK+ with WebKit1) +* https://surf.suckless.org/[surf] (C, GTK+ with WebKit1/WebKit2) * Firefox addons (not based on WebExtensions or no recent activity): http://www.vimperator.org/[Vimperator], http://bug.5digits.org/pentadactyl/index[Pentadactyl], @@ -262,7 +261,7 @@ original site is gone but the Arch Linux wiki has some data) * Chrome/Chromium addons: https://github.com/k2nr/ViChrome/[ViChrome], https://github.com/jinzhu/vrome[Vrome], - https://github.com/lusakasa/saka-key[Saka Key], + https://github.com/lusakasa/saka-key[Saka Key] (https://github.com/lusakasa/saka-key/issues/171[unmaintained]), https://github.com/1995eaton/chromium-vim[cVim], https://glee.github.io/[GleeBox] diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index c29595d9e..129378b07 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -15,15 +15,54 @@ 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.14.0 (unreleased) +v2.0.0 (unreleased) +------------------- + +Major changes +~~~~~~~~~~~~~ + +- At least Python 3.6 is now required to run qutebrowser, support for Python + 3.5 is dropped. Note that Python 3.5 is + https://www.python.org/downloads/release/python-3510/[no longer supported + upstream] since September 2020. + +Changed +~~~~~~~ + +- `config.py` files now are required to have either + `config.load_autoconfig(False)` (don't load `autoconfig.yml`) or + `config.load_autoconfig()` (do load `autoconfig.yml`) in them. +- The `colors.webpage.darkmode.*` settings are now also supported with older Qt + versions (Qt 5.10 to 5.13) rather than just with Qt 5.14 and above. +- For regexes in the config (`hints.{prev,next}_regexes`), certain patterns + which will change meanings in future Python versions are now disallowed. This is + the case for character sets starting with a literal `[` or containing literal + character sequences `--`, `&&`, `~~`, or `||`. To avoid a warning, remove the + duplicate characters or escape them with a backslash. + +v1.14.0 (2020-10-15) -------------------- +Note: The QtWebEngine version bundled with the Windows/macOS +releases is still based on Qt 5.15.0 (like with qutebrowser v1.12.0 and +v1.13.0) rather than Qt 5.15.1 because of a +https://bugreports.qt.io/browse/QTBUG-86752[Qt bug] causing +frequent renderer process crashes. When Qt 5.15.2 is released +(planned for November 3rd, 2020), a qutebrowser v1.14.x patch +release with an updated QtWebEngine will be released. + +Furthermore, this release still only contains partial session support for QtWebEngine +5.15. It's still recommended to run against Qt 5.15 due to the security patches +contained in it -- for most users, the added workarounds seem to work out fine. A +rewritten session support will be part of qutebrowser v2.0.0, tentatively planned for the +end of the year or early 2021. + Changed ~~~~~~~ - The `content.media_capture` setting got split up into three more fine-grained settings, `content.media.audio_capture`, `.video_capture` and - `.audio_video_capture`. Before this change, anwering "always" to a prompt + `.audio_video_capture`. Before this change, answering "always" to a prompt about e.g. audio capturing would set the `content.media_capture` setting, which would also allow the same website to capture video on a future visit. Now every prompt will set the appropriate setting, though existing @@ -44,8 +83,6 @@ Changed - `:back` and `:forward` now take an optional index which is completed using the current tab's history. - The time a website in a tab was visited is now saved/restored in sessions. -- New argument `strip` for `:navigate` which removes queries and - fragments from the current URL. - When attempting to download a file to a location for which there's already a still-running download, a confirmation prompt is now displayed. - `:completion-item-focus` now understands `next-page` and `prev-page` with @@ -66,12 +103,27 @@ Changed `--asciidoc-python path/to/python --asciidoc path/to/asciidoc.py` instead of the former `--asciidoc path/to/python path/to/asciidoc.py`. -- The `readability-js` userscript now adds some CSS to better deal - with images, similarly to what Firefox' reader mode does. +- Dark mode (`colors.webpage.darkmode.*`) is now supported with Qt 5.15.2 (which + is not released yet). +- The default for the darkmode `policy.images` setting is now set to `smart` + which fixes issues with e.g. formulas on Wikipedia. +- The `readability-js` userscript now adds some CSS to improve the reader mode + styling in various scenarios: + * Images are now shrinked to the page width, similarly to what Firefox' reader + mode does. + * Some images ore now displayed as block (rather than inline) which is what + Firefox' reader mode does as well. + * Blockquotes are now styled more distinctively, again based on the Firefox + reader mode. + * Code blocks are now easier to distinguish from text and tables have visible + cell margins. +- The `readability-js` userscript now supports hint userscript mode. Added ~~~~~ +- New argument `strip` for `:navigate` which removes queries and + fragments from the current URL. - `:undo` now has a new `-w` / `--window` argument, which can be used to restore closed windows (rather than tabs). This is bound to `U` by default. - `:jseval` can now take `javascript:...` URLs via a new `--url` flag. @@ -88,6 +140,11 @@ Added open the directory containing the downloaded file. An entry to do the same was also added to the context menu. - Messages are now wrapped when they are too long to be displayed on a single line. +- New possible `--debug-flag` values: + * `wait-renderer-process` waits for a `SIGUSR1` in the renderer process so a + debugger can be attached. + * `avoid-chromium-init` allows using `--version` without needing a working + QtWebEngine/Chromium. Fixed ~~~~~ @@ -125,18 +182,21 @@ Fixed could end up in a confusing state. This is now fixed. - When qutebrowser quits, running downloads are now cancelled properly. - The site-specific quirk for `web.whatsapp.com` has been updated to work after recent - WhatsApp-changes. + changes in WhatsApp. - Highlighting in the completion now works properly when UTF-16 surrogate pairs (such as emoji) are involved. - When a windowed inspector is clicked, insert mode now isn't entered anymore. -- When `:undo` to re-open a tab but `tabs.tabs_are_windows` was set between +- When `:undo` is used to re-open a tab, but `tabs.tabs_are_windows` was set between closing and undoing the close, qutebrowser crashed. This is now fixed. +- With QtWebEngine 5.15.0, setting the darkmode image policy to `smart` leads to + renderer process crashes. The offending setting value is now ignored with a + warning. - Fixes for the `qute-pass` userscript: * With newer `gopass` versions, a deprecation notice was copied as password due to `qute-pass` using it in a deprecated way. * The `--password-store` argument didn't actually set `PASSWORD_STORE_DIR` for `pass`, resulting in `qute-pass` finding matches but the - underlying `pass` not finding matching passwords. This is now fixed. + underlying `pass` not finding matching passwords. v1.13.1 (2020-07-17) -------------------- diff --git a/doc/contributing.asciidoc b/doc/contributing.asciidoc index dbf1e5cc5..b8c9b9010 100644 --- a/doc/contributing.asciidoc +++ b/doc/contributing.asciidoc @@ -9,7 +9,7 @@ IMPORTANT: Bandwidth for pull request review is currently quite limited. If you want to contribute where it's most needed, please consider reviewing or testing open pull requests. -I `<3` footnote:[Of course, that says `<3` in HTML.] contributors! +I `<3` footnote:[`<3` in HTML] contributors! This document contains guidelines for contributing to qutebrowser, as well as useful hints when doing so. @@ -111,7 +111,7 @@ unittests and several linters/checkers. Currently, the following tox environments are available: * Tests using https://www.pytest.org[pytest]: - - `py35`, `py36`: Run pytest for python 3.5/3.6 with the system-wide PyQt. + - `py36`, `py37`, ...: Run pytest for python 3.6/3.7/... with the system-wide PyQt. - `py36-pyqt57`, ..., `py36-pyqt59`: Run pytest with the given PyQt version (`py35-*` also works). - `py36-pyqt59-cov`: Run with coverage support (other Python/PyQt versions work too). * `flake8`: Run various linting checks via https://pypi.python.org/pypi/flake8[flake8]. diff --git a/doc/faq.asciidoc b/doc/faq.asciidoc index be946fb83..275f82df2 100644 --- a/doc/faq.asciidoc +++ b/doc/faq.asciidoc @@ -322,6 +322,25 @@ certutil -d "sql:${HOME}/.pki/nssdb" -D -n "My Fancy Certificate Nickname" And then import the new and valid certificates using the procedure described above. +Is there a dark mode? How can I filter websites to be darker?:: +There is a total of four possible approaches to get dark websites: ++ +- The `colors.webpage.prefers_color_scheme_dark` setting tells websites that you prefer + a dark theme. However, this requires websites to ship an appropriate dark style sheet. + The setting requires a restart and QtWebEngine with at least Qt 5.14. +- The `colors.webpage.darkmode.*` settings enable the dark mode of the underlying + Chromium. Those setting require a restart and QtWebEngine with at least Qt 5.14. It's + unfortunately not possible (due to limitations in Chromium and/or QtWebEngine) to + change them dynamically or to specify a list of excluded websites. +- The `content.user_stylesheets` setting allows specifying a custom CSS such as + https://github.com/alphapapa/solarized-everything-css/[Solarized Everything]. Despite + the name, the repository also offers themes other than just Solarized. This approach + often yields worse results compared to the above ones, but it's possible to toggle it + dynamically using a binding like `:bind ,d 'config-cycle content.user_stylesheets + ~/path/to/solarized-everything-css/css/gruvbox/gruvbox-all-sites.css ""'` +- Finally, qutebrowser's Greasemonkey support should allow for running a + https://github.com/darkreader/darkreader/issues/926#issuecomment-575893299[stripped down version] + of the Dark Reader extension. This is mostly untested, though. == Troubleshooting diff --git a/doc/help/configuring.asciidoc b/doc/help/configuring.asciidoc index 4f1d872f7..aa1dfc12e 100644 --- a/doc/help/configuring.asciidoc +++ b/doc/help/configuring.asciidoc @@ -395,10 +395,10 @@ Pre-built colorschemes - A collection of https://github.com/chriskempson/base16[base16] color-schemes can be found in https://github.com/theova/base16-qutebrowser[base16-qutebrowser] and used with https://github.com/AuditeMarlow/base16-manager[base16-manager]. - https://gitlab.com/jjzmajic/qutewal[Pywal integration] -- Two implementations of the https://github.com/arcticicestudio/nord[Nord] colorscheme for qutebrowser exist: https://github.com/Linuus/nord-qutebrowser[Linuus], https://github.com/KnownAsDon/QuteBrowser-Nord-Theme[KnownAsDon] +- https://github.com/arcticicestudio/nord[Nord]: https://github.com/Linuus/nord-qutebrowser[Linuus], https://github.com/KnownAsDon/QuteBrowser-Nord-Theme[KnownAsDon] - https://github.com/dracula/qutebrowser-dracula-theme[Dracula] - https://gitlab.com/lovetocode999/selenized-qutebrowser[Selenized] -- https://github.com/The-Compiler/dotfiles/blob/master/qutebrowser/gruvbox.py[gruvbox] +- https://github.com/morhetz/gruvbox[gruvbox]: https://github.com/The-Compiler/dotfiles/blob/master/qutebrowser/gruvbox.py[The-Compiler], https://gitlab.com/shaneyost/dots-popos-september-2020/-/blob/master/qutebrowser/config.py[Shane Yost] Avoiding flake8 errors ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 035c7881d..94a884db9 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -1572,6 +1572,7 @@ Default: +pass:[white]+ [[colors.webpage.darkmode.algorithm]] === colors.webpage.darkmode.algorithm Which algorithm to use for modifying how colors are rendered with darkmode. +The `lightness-cielab` value was added with QtWebEngine 5.14 and is treated like `lightness-hsl` with older QtWebEngine versions. This setting requires a restart. @@ -1579,13 +1580,13 @@ Type: <<types,String>> Valid values: - * +lightness-cielab+: Modify colors by converting them to CIELAB color space and inverting the L value. + * +lightness-cielab+: Modify colors by converting them to CIELAB color space and inverting the L value. Not available with Qt < 5.14. * +lightness-hsl+: Modify colors by converting them to the HSL color space and inverting the lightness (i.e. the "L" in HSL). * +brightness-rgb+: Modify colors by subtracting each of r, g, and b from their maximum value. Default: +pass:[lightness-cielab]+ -On QtWebEngine, this setting requires Qt 5.14 or newer. +On QtWebEngine, this setting requires Qt 5.10 or newer. On QtWebKit, this setting is unavailable. @@ -1600,7 +1601,7 @@ Type: <<types,Float>> Default: +pass:[0.0]+ -On QtWebEngine, this setting requires Qt 5.14 or newer. +On QtWebEngine, this setting requires Qt 5.10 or newer. On QtWebKit, this setting is unavailable. @@ -1628,7 +1629,7 @@ Type: <<types,Bool>> Default: +pass:[false]+ -On QtWebEngine, this setting requires Qt 5.14 or newer. +On QtWebEngine, this setting requires Qt 5.10 or newer. On QtWebKit, this setting is unavailable. @@ -1643,7 +1644,7 @@ Type: <<types,Bool>> Default: +pass:[false]+ -On QtWebEngine, this setting requires Qt 5.14 or newer. +On QtWebEngine, this setting requires Qt 5.10 or newer. On QtWebKit, this setting is unavailable. @@ -1665,7 +1666,7 @@ On QtWebKit, this setting is unavailable. [[colors.webpage.darkmode.policy.images]] === colors.webpage.darkmode.policy.images Which images to apply dark mode to. -WARNING: On Qt 5.15.0, this setting can cause frequent renderer process crashes due to a https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/304211[bug in Qt]. +With QtWebEngine 5.15.0, this setting can cause frequent renderer process crashes due to a https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/304211[bug in Qt]. With QtWebEngine 5.10, this is not available at all. In those cases, the 'smart' setting is ignored and treated like 'never'. This setting requires a restart. @@ -1675,11 +1676,11 @@ Valid values: * +always+: Apply dark mode filter to all images. * +never+: Never apply dark mode filter to any images. - * +smart+: Apply dark mode based on image content. + * +smart+: Apply dark mode based on image content. Not available with Qt 5.10 / 5.15.0. -Default: +pass:[never]+ +Default: +pass:[smart]+ -On QtWebEngine, this setting requires Qt 5.14 or newer. +On QtWebEngine, this setting requires Qt 5.10 or newer. On QtWebKit, this setting is unavailable. @@ -4081,14 +4082,14 @@ The following placeholders are defined: * `{perc}`: Percentage as a string like `[10%]`. * `{perc_raw}`: Raw percentage, e.g. `10`. * `{current_title}`: Title of the current web page. -* `{title_sep}`: The string ` - ` if a title is set, empty otherwise. +* `{title_sep}`: The string `" - "` if a title is set, empty otherwise. * `{index}`: Index of this tab. * `{aligned_index}`: Index of this tab padded with spaces to have the same width. * `{id}`: Internal tab ID of this tab. * `{scroll_pos}`: Page scroll position. * `{host}`: Host of the current web page. -* `{backend}`: Either ''webkit'' or ''webengine'' +* `{backend}`: Either `webkit` or `webengine` * `{private}`: Indicates when private mode is enabled. * `{current_url}`: URL of the current web page. * `{protocol}`: Protocol (http/https/...) of the current web page. diff --git a/doc/install.asciidoc b/doc/install.asciidoc index 0f9a4c399..9c71bf2b5 100644 --- a/doc/install.asciidoc +++ b/doc/install.asciidoc @@ -27,22 +27,41 @@ On Debian / Ubuntu How to install qutebrowser depends a lot on the version of Debian/Ubuntu you're running. +[[ubuntu1604]] Ubuntu 16.04 LTS / Linux Mint 18 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 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 -<<tox,install qutebrowser in a virtualenv>>. +QtWebEngine). It also comes with Python 3.5 which is not supported anymore since +qutebrowser v2.0.0. -You'll need some basic libraries to use the virtualenv-installed PyQt: +You should be able to install a newer Python (3.6+) using the +https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa[deadsnakes PPA] or +https://github.com/pyenv/pyenv[pyenv], and then proceed to +<<tox,install qutebrowser in a virtualenv>>. However, this is currently untested. If you +got this setup to work successfully, please submit a pull request to adjust these +instructions! + +Note you'll need some basic libraries to use the virtualenv-installed PyQt: ---- -# apt install libglib2.0-0 libgl1 libfontconfig1 libx11-xcb1 libxi6 libxrender1 libdbus-1-3 +# apt install --no-install-recommends git ca-certificates python3 python3-venv asciidoc libglib2.0-0 libgl1 libfontconfig1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxcb-xfixes0 libxcb-xinerama0 libxcb-xkb1 libxkbcommon-x11-0 libdbus-1-3 libyaml-dev gcc python3-dev ---- +// FIXME not needed anymore? +// libxi6 libxrender1 libegl1-mesa + Debian Stretch ~~~~~~~~~~~~~~ +WARNING: Debian Stretch packages Qt 5.7 which is very old (based on a Chromium +from March 2016 with security fixes from November 2016) and insecure. It is also +https://www.debian.org/releases/stretch/amd64/release-notes/ch-information.en.html#browser-security[not covered] +by Debian's security patches. Support for it will be dropped in qutebrowser +v2.0.0, preliminarily planned for December 2020. It is recommended to +<<tox,install qutebrowser in a virtualenv>> with a newer PyQt/Qt binary +instead. + Debian Stretch comes with QtWebEngine in the repositories. This makes it possible to install qutebrowser via the Debian package. @@ -70,6 +89,14 @@ qutebrowser package. Debian Buster / Ubuntu 18.04 LTS / Linux Mint 19 (or newer) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +WARNING: Debian Buster packages Qt 5.11 which is very old (based on a Chromium +from March 2018 with security fixes from November 2018) and insecure. It is also +https://www.debian.org/releases/buster/amd64/release-notes/ch-information.en.html#browser-security[not covered] +by Debian's security patches. Support for it will be dropped in qutebrowser +v2.0.0, preliminarily planned for December 2020. It is recommended to +<<tox,install qutebrowser in a virtualenv>> with a newer PyQt/Qt binary +instead. + With those distributions, qutebrowser is in the official repositories, and you can install it with apt: @@ -80,18 +107,18 @@ can install it with apt: Additional hints ~~~~~~~~~~~~~~~~ -- Alternatively, you can <<tox,install qutebrowser in a virtualenv>> to get a newer - QtWebEngine version. - If running from git, run the following to generate the documentation for the `:help` command: + ---- -# apt install --no-install-recommends asciidoc source-highlight +# apt install --no-install-recommends asciidoc $ python3 scripts/asciidoc2html.py ---- -- If you prefer using QtWebKit, there's an up-to-date version available in - https://packages.debian.org/buster/libqt5webkit5[Debian Testing]. +- If you prefer using QtWebKit, there's QtWebKit 5.212 available in + https://packages.debian.org/buster/libqt5webkit5[Debian Testing]. Note + however that it is based on an upstream WebKit from September 2016 with known + security issues and no sandboxing or process isolation. - If video or sound don't work with QtWebKit, try installing the gstreamer plugins: + ---- @@ -141,7 +168,8 @@ $ cd .. $ rm -r qutebrowser-git ---- -or you could use an AUR helper, e.g. `yaourt -S qutebrowser-git`. +or you could use an AUR helper like https://github.com/Jguer/yay/[yay], e.g. +`yay -S qutebrowser-git`. If video or sound don't work with QtWebKit, try installing the gstreamer plugins: @@ -181,12 +209,6 @@ with: # xbps-install qutebrowser ---- -It's currently recommended to install `python3-PyQt5-webengine` and -`python3-PyQt5-opengl`, then start with `--backend webengine` to use the new -backend. - -Since the v1.0 release, qutebrowser uses QtWebEngine by default. - On NixOS -------- @@ -197,18 +219,11 @@ it with: $ nix-env -i qutebrowser ---- -It's recommended to install `qt5.qtwebengine` and start with -`--backend webengine` to use the new backend. - -Since the v1.0 release, qutebrowser uses QtWebEngine by default. - On openSUSE ----------- There are prebuilt RPMs available at https://software.opensuse.org/download.html?project=network&package=qutebrowser[OBS]. -To use the QtWebEngine backend, install `libqt5-qtwebengine`. - On Slackware ------------ @@ -248,7 +263,7 @@ qutebrowser is available https://flathub.org/apps/details/org.qutebrowser.qutebrowser[on Flathub] as `org.qutebrowser.qutebrowser`. -WARNING: As of July 2020, the Flatpak package is severely outdated (qutebrowser +WARNING: As of October 2020, the Flatpak package is severely outdated (qutebrowser v1.7.0 from July 2019) and, among other issues, misses fixes for a (low-severity) https://github.com/qutebrowser/qutebrowser/security/advisories/GHSA-4rcq-jv2f-898j[security issue]. It's recommended to <<tox,install qutebrowser in a virtualenv>> instead, which @@ -350,10 +365,14 @@ qutebrowser from source. ==== Homebrew ---- -$ brew install qt5 +$ brew install qt +(build PyQt and PyQtWebEngine from source) $ pip3 install qutebrowser ---- +NOTE: Homebrew does not package PyQtWebEngine (Python wrappers for +QtWebEngine), so you will need to build that from sources manually. + Since the v1.0 release, qutebrowser uses QtWebEngine by default. Homebrew's builds of Qt and PyQt don't come with QtWebKit (and `--with-qtwebkit` @@ -364,12 +383,11 @@ https://github.com/annulen/webkit/wiki/Building-QtWebKit-on-OS-X[manually]. Packagers --------- -There are example .desktop and icon files provided. They would go in the -standard location for your distro (`/usr/share/applications` and -`/usr/share/pixmaps` for example). - -The normal `setup.py install` doesn't install these files, so you'll have to do -it as part of the packaging process. +qutebrowser ships with a +https://github.com/qutebrowser/qutebrowser/blob/master/misc/Makefile[Makefile] +intended for packagers. This installs system-wide files in a proper locations, +so it should be preferred to the usual `setup.py install` or `pip install` +invocation. // The tox anchor is so that old links remain compatible. // When switching to Sphinx, that should be changed. @@ -405,6 +423,10 @@ $ cd qutebrowser Installing dependencies (including Qt) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Using a Qt installed via virtualenv needs a couple of system-wide libraries. +See the <<ubuntu1604,Ubuntu 16.04 section>> for details about which libraries +are required. + Then run the install script: ---- @@ -418,9 +440,8 @@ This installs all needed Python dependencies in a `.venv` subfolder This comes with an up-to-date Qt/PyQt including a pre-compiled QtWebEngine binary, but has a few caveats: -- Make sure your `python3` is Python 3.5 or newer, otherwise you'll get a "No - matching distribution found" error. Note that qutebrowser itself also requires - this. +- Make sure your `python3` is Python 3.6 or newer, otherwise you'll get a "No + matching distribution found" error and/or qutebrowser will not run. - It only works on 64-bit x86 systems, with other architectures you'll get the same error. - It comes with a QtWebEngine compiled without proprietary codec support (such @@ -433,6 +454,12 @@ You can specify a Qt/PyQt version with the `--pyqt-version` flag, see `mkenv.py --help` for a list of available versions. By default, the latest version which plays well with qutebrowser is used. +NOTE: If qutebrowser fails to start with a _"This application failed to start +because no Qt platform plugin could be initialized."_ message, most likely a +system-wide library is missing. Run qutebrowser again after +`export QT_DEBUG_PLUGINS=1` and keep attention to a +_QLibraryPrivate::loadPlugin failed on ..._ line for details. + Installing dependencies (system-wide Qt) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/stacktrace.asciidoc b/doc/stacktrace.asciidoc index f295a5305..3d95aa25e 100644 --- a/doc/stacktrace.asciidoc +++ b/doc/stacktrace.asciidoc @@ -35,7 +35,7 @@ https://www.linuxmint.com/rel_tessa_mate_whatsnew.php[Linux Mint]) and install the debug packages: ---- -# apt install python3-dbg python3-pyqt5-dbg python3-pyqt5.qtwebengine-dbg libqt5webengine5-dbgsym +# apt install python3-dbg python3-pyqt5-dbg python3-pyqt5.qtwebengine-dbg libqt5webengine5-dbgsym libqt5webenginecore5-dbgsym ---- or with the QtWebKit backend: diff --git a/misc/apparmor/usr.bin.qutebrowser b/misc/apparmor/usr.bin.qutebrowser index b993e0058..3d27be697 100644 --- a/misc/apparmor/usr.bin.qutebrowser +++ b/misc/apparmor/usr.bin.qutebrowser @@ -1,41 +1,79 @@ -# AppArmor profile for qutebrowser -# Tested on Debian jessie - #include <tunables/global> profile qutebrowser /usr/{local/,}bin/qutebrowser { - #include <abstractions/base> + #include <abstractions/python> + #include <abstractions/audio> + #include <abstractions/dri-common> + #include <abstractions/mesa> + #include <abstractions/X> + #include <abstractions/wayland> + #include <abstractions/qt5> + #include <abstractions/fonts> + + #include <abstractions/dbus-session-strict> #include <abstractions/nameservice> #include <abstractions/openssl> #include <abstractions/ssl_certs> - #include <abstractions/audio> - #include <abstractions/fonts> - #include <abstractions/kde> + + #include <abstractions/freedesktop.org> #include <abstractions/user-download> - #include <abstractions/X> + #include <abstractions/user-tmp> - capability dac_override, - /usr/{local/,}bin/ r, - /usr/{local/,}bin/qutebrowser rix, - /usr/bin/python3.? r, + # not nice but required for chromium sandbox + capability sys_admin, + capability sys_chroot, + capability sys_ptrace, - /usr/lib/python3/ mr, - /usr/lib/python3/** mr, - /usr/lib/python3.?/ r, - /usr/lib/python3.?/** mr, - /usr/local/lib/python3.?/** r, + /dev/ r, + /dev/video* r, + /etc/mime.types r, + /usr/bin/ r, + /usr/bin/ldconfig ix, + /usr/bin/uname ix, + /usr/bin/qutebrowser rix, + /usr/lib/qt/libexec/QtWebEngineProcess mrix, + /usr/share/pdf.js/** r, + /usr/share/qt/translations/qtwebengine_locales/* r, + /usr/share/qt/qtwebengine_dictionaries r, + /usr/share/qt/qtwebengine_dictionaries/* r, + /usr/share/qt/resources/* r, - /proc/*/mounts r, - owner /tmp/** rwkl, - owner /run/user/*/ rw, - owner /run/user/*/** krw, + owner @{HOME}/ r, + owner /dev/shm/.org.chromium* rw, + owner @{HOME}/.cache/{qtshadercache,qutebrowser}/** rwlk, + owner @{HOME}/.cache/qtshadercache** rwl, + owner @{HOME}/.config/qutebrowser/** rwlk, + owner @{HOME}/.local/share/.org.chromium.Chromium* rw, + owner @{HOME}/.local/share/mime/generic-icons r, + owner @{HOME}/.local/share/qutebrowser/ r, + owner @{HOME}/.local/share/qutebrowser/** rwkl, + owner @{HOME}/.pki/nssdb/* rwk, + owner @{HOME}/#[0-9]* rwm, + owner /run/user/*/qutebrowser/ rw, + owner /run/user/*/qutebrowser/* rw, + owner /run/user/*/qutebrowser*slave-socket rwl, + owner /run/user/*/#* rw, - @{HOME}/.config/qutebrowser/** krw, - @{HOME}/.local/share/qutebrowser/** krw, - @{HOME}/.cache/qutebrowser/** krw, - @{HOME}/.gstreamer-0.10/* r, + # qt/kde + @{PROC} r, + @{PROC}/sys/fs/inotify/max_user_watches r, + @{PROC}/sys/kernel/random/boot_id r, + @{PROC}/sys/kernel/core_pattern r, + @{PROC}/sys/kernel/yama/ptrace_scope r, + /sys/{class,bus}/ r, + /sys/bus/pci/devices/ r, + /sys/devices/**/{class,config,device,resource,revision,removable,uevent} r, + /sys/devices/**/{vendor,subsystem_device,subsystem_vendor} r, -} + owner @{PROC}/@{pid}/{fd,stat,task,mounts}/ r, + owner @{PROC}/@{pid}/stat r, + owner @{PROC}/@{pid}/task/@{pid}/status r, + owner @{PROC}/@{pid}/{setgroups,gid_map,oom_score_adj,uid_map} rw, + owner @{PROC}/@{pid}/{oom_score_adj,uid_map} rw, + + # allow execution of userscripts + /usr/share/qutebrowser/userscripts/* Ux, +} diff --git a/misc/org.qutebrowser.qutebrowser.appdata.xml b/misc/org.qutebrowser.qutebrowser.appdata.xml index e246ea4d7..f0885b8ed 100644 --- a/misc/org.qutebrowser.qutebrowser.appdata.xml +++ b/misc/org.qutebrowser.qutebrowser.appdata.xml @@ -44,6 +44,7 @@ </content_rating> <releases> <!-- Add new releases here --> +<release version="1.14.0" date="2020-10-15"/> <release version="1.13.1" date="2020-07-17"/> <release version="1.13.0" date="2020-06-26"/> <release version="1.12.0" date="2020-06-01"/> diff --git a/misc/requirements/requirements-check-manifest.txt b/misc/requirements/requirements-check-manifest.txt index 4cc00982d..9993cf4dd 100644 --- a/misc/requirements/requirements-check-manifest.txt +++ b/misc/requirements/requirements-check-manifest.txt @@ -1,5 +1,5 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -check-manifest==0.42 -pep517==0.8.2 +check-manifest==0.44 +pep517==0.9.1 toml==0.10.1 diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt index 798fecad6..e885b3335 100644 --- a/misc/requirements/requirements-dev.txt +++ b/misc/requirements/requirements-dev.txt @@ -1,20 +1,20 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -bump2version==1.0.0 +bump2version==1.0.1 certifi==2020.6.20 -cffi==1.14.2 +cffi==1.14.3 chardet==3.0.4 -colorama==0.4.3 -cryptography==3.1 +colorama==0.4.4 +cryptography==3.2 cssutils==1.0.2 github3.py==1.3.0 -hunter==3.2.2 +hunter==3.3.1 idna==2.10 jwcrypto==0.8 manhole==1.6.0 packaging==20.4 pycparser==2.20 -Pympler==0.8 +Pympler==0.9 pyparsing==2.4.7 PyQt-builder==1.5.0 python-dateutil==2.8.1 @@ -23,4 +23,4 @@ sip==5.4.0 six==1.15.0 toml==0.10.1 uritemplate==3.0.1 -# urllib3==1.25.10 +# urllib3==1.25.11 diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt index afd4f0bd6..d020c02a2 100644 --- a/misc/requirements/requirements-flake8.txt +++ b/misc/requirements/requirements-flake8.txt @@ -1,10 +1,10 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py attrs==20.2.0 -flake8==3.8.3 +flake8==3.8.4 flake8-bugbear==20.1.4 flake8-builtins==1.5.3 -flake8-comprehensions==3.2.3 +flake8-comprehensions==3.3.0 flake8-copyright==0.2.2 flake8-debugger==3.2.1 flake8-deprecated==1.3 diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt index 863f48c6f..ff3d430e8 100644 --- a/misc/requirements/requirements-mypy.txt +++ b/misc/requirements/requirements-mypy.txt @@ -1,15 +1,15 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -diff-cover==4.0.0 +diff-cover==4.0.1 inflect==4.1.0 Jinja2==2.11.2 jinja2-pluralize==0.3.0 -lxml==4.5.2 +lxml==4.6.1 MarkupSafe==1.1.1 -mypy==0.782 +mypy==0.790 mypy-extensions==0.4.3 pluggy==0.13.1 -Pygments==2.7.1 --e git+https://github.com/stlehmann/PyQt5-stubs.git@master#egg=PyQt5_stubs +Pygments==2.7.2 +-e git+https://github.com/stlehmann/PyQt5-stubs.git@811462b34ee151b898289ae8f1de8af30c690c55#egg=PyQt5_stubs typed-ast==1.4.1 typing-extensions==3.7.4.3 diff --git a/misc/requirements/requirements-mypy.txt-raw b/misc/requirements/requirements-mypy.txt-raw index d3b0dc4ca..7c888ed1e 100644 --- a/misc/requirements/requirements-mypy.txt-raw +++ b/misc/requirements/requirements-mypy.txt-raw @@ -2,6 +2,3 @@ mypy lxml # For HTML reports diff-cover -e git+https://github.com/stlehmann/PyQt5-stubs.git@master#egg=PyQt5-stubs - -# remove @commit-id for scm installs -#@ replace: @.*# @master# diff --git a/misc/requirements/requirements-pyinstaller.txt b/misc/requirements/requirements-pyinstaller.txt index 06405d96a..7941d2772 100644 --- a/misc/requirements/requirements-pyinstaller.txt +++ b/misc/requirements/requirements-pyinstaller.txt @@ -2,4 +2,4 @@ altgraph==0.17 pyinstaller==4.0 -pyinstaller-hooks-contrib==2020.8 +pyinstaller-hooks-contrib==2020.9 diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index 08c1d2c10..75a0e8e0f 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -2,9 +2,9 @@ astroid==2.3.3 # rq.filter: < 2.4 certifi==2020.6.20 -cffi==1.14.2 +cffi==1.14.3 chardet==3.0.4 -cryptography==3.1 +cryptography==3.2 github3.py==1.3.0 idna==2.10 isort==4.3.21 @@ -19,5 +19,5 @@ requests==2.24.0 six==1.15.0 typed-ast==1.4.1 ; python_version<"3.8" uritemplate==3.0.1 -# urllib3==1.25.10 +# urllib3==1.25.11 wrapt==1.11.2 diff --git a/misc/requirements/requirements-pyqt.txt b/misc/requirements/requirements-pyqt.txt index 88a04230d..148e8d8bb 100644 --- a/misc/requirements/requirements-pyqt.txt +++ b/misc/requirements/requirements-pyqt.txt @@ -2,4 +2,4 @@ PyQt5==5.15.1 PyQt5-sip==12.8.1 -PyQtWebEngine==5.15.1 +PyQtWebEngine==5.15.0 diff --git a/misc/requirements/requirements-pyqt.txt-raw b/misc/requirements/requirements-pyqt.txt-raw index 9c6afbf16..83ebc7671 100644 --- a/misc/requirements/requirements-pyqt.txt-raw +++ b/misc/requirements/requirements-pyqt.txt-raw @@ -1,2 +1,2 @@ PyQt5 -PyQtWebEngine +PyQtWebEngine!=5.15.1 diff --git a/misc/requirements/requirements-pyroma.txt b/misc/requirements/requirements-pyroma.txt index c6e751e19..1a2dbde7f 100644 --- a/misc/requirements/requirements-pyroma.txt +++ b/misc/requirements/requirements-pyroma.txt @@ -1,5 +1,5 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py docutils==0.16 -Pygments==2.7.1 +Pygments==2.7.2 pyroma==2.6 diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt index 7b43a72a1..baeea4d40 100644 --- a/misc/requirements/requirements-sphinx.txt +++ b/misc/requirements/requirements-sphinx.txt @@ -10,7 +10,7 @@ imagesize==1.2.0 Jinja2==2.11.2 MarkupSafe==1.1.1 packaging==20.4 -Pygments==2.7.1 +Pygments==2.7.2 pyparsing==2.4.7 pytz==2020.1 requests==2.24.0 @@ -23,4 +23,4 @@ sphinxcontrib-htmlhelp==1.0.3 sphinxcontrib-jsmath==1.0.1 sphinxcontrib-qthelp==1.0.3 sphinxcontrib-serializinghtml==1.1.4 -urllib3==1.25.10 +urllib3==1.25.11 diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 789c176e6..066f4c4db 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -2,23 +2,25 @@ apipkg==1.5 attrs==20.2.0 -beautifulsoup4==4.9.1 +beautifulsoup4==4.9.3 certifi==2020.6.20 chardet==3.0.4 cheroot==8.4.5 click==7.1.2 -# colorama==0.4.3 +# colorama==0.4.4 coverage==5.3 EasyProcess==0.3 execnet==1.7.1 +filelock==3.0.12 Flask==1.1.2 glob2==0.7 -hunter==3.2.2 -hypothesis==5.35.3 ; python_version>="3.6" +hunter==3.3.1 +hypothesis==5.38.0 +icdiff==1.9.1 idna==2.10 -iniconfig==1.0.1 +iniconfig==1.1.1 itsdangerous==1.1.0 -jaraco.functools==3.0.1 ; python_version>="3.6" +jaraco.functools==3.0.1 # Jinja2==2.11.2 Mako==1.1.3 manhole==1.6.0 @@ -28,20 +30,23 @@ packaging==20.4 parse==1.18.0 parse-type==0.5.2 pluggy==0.13.1 +pprintpp==0.4.0 py==1.9.0 py-cpuinfo==7.0.0 -Pygments==2.7.1 +Pygments==2.7.2 pyparsing==2.4.7 -pytest==6.0.2 +pytest==6.1.1 pytest-bdd==4.0.1 pytest-benchmark==3.2.3 +pytest-clarity==0.3.0a0 pytest-cov==2.10.1 pytest-forked==1.3.0 +pytest-icdiff==0.5 pytest-instafail==0.4.2 pytest-mock==3.3.1 pytest-qt==3.3.0 pytest-repeat==0.8.0 -pytest-rerunfailures==9.1 +pytest-rerunfailures==9.1.1 pytest-xdist==2.1.0 pytest-xvfb==2.0.0 PyVirtualDisplay==1.3.2 @@ -50,11 +55,9 @@ requests-file==1.5.1 six==1.15.0 sortedcontainers==2.2.2 soupsieve==2.0.1 -tldextract==2.2.3 +termcolor==1.1.0 +tldextract==3.0.2 toml==0.10.1 -urllib3==1.25.10 -vulture==2.1 ; python_version>="3.6" +urllib3==1.25.11 +vulture==2.1 Werkzeug==1.0.1 -jaraco.functools==2.0; python_version<"3.6" -vulture==1.6; python_version<"3.6" -hypothesis<5.34.0; python_version<"3.6" diff --git a/misc/requirements/requirements-tests.txt-raw b/misc/requirements/requirements-tests.txt-raw index 73f58461c..fd346d475 100644 --- a/misc/requirements/requirements-tests.txt-raw +++ b/misc/requirements/requirements-tests.txt-raw @@ -27,17 +27,11 @@ pytest-xvfb PyVirtualDisplay # To run on multiple cores with -n pytest-xdist +# For nicer output +pytest-icdiff +pytest-clarity # Needed to test misc/userscripts/qute-lastpass tldextract -#@ markers: jaraco.functools python_version>="3.6" -#@ add: jaraco.functools==2.0; python_version<"3.6" - -#@ markers: vulture python_version>="3.6" -#@ add: vulture==1.6; python_version<"3.6" - -#@ markers: hypothesis python_version>="3.6" -#@ add: hypothesis<5.34.0; python_version<"3.6" - #@ ignore: Jinja2, MarkupSafe, colorama diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt index c7f99a2da..a77333637 100644 --- a/misc/requirements/requirements-tox.txt +++ b/misc/requirements/requirements-tox.txt @@ -9,7 +9,7 @@ py==1.9.0 pyparsing==2.4.7 six==1.15.0 toml==0.10.1 -tox==3.20.0 +tox==3.20.1 tox-pip-version==0.0.7 tox-venv==0.4.0 -virtualenv==20.0.31 +virtualenv==20.1.0 diff --git a/misc/requirements/requirements-yamllint.txt b/misc/requirements/requirements-yamllint.txt index 0ea42bf7c..5f80fd5d5 100644 --- a/misc/requirements/requirements-yamllint.txt +++ b/misc/requirements/requirements-yamllint.txt @@ -2,4 +2,4 @@ pathspec==0.8.0 PyYAML==5.3.1 -yamllint==1.24.2 +yamllint==1.25.0 diff --git a/misc/userscripts/README.md b/misc/userscripts/README.md index d2519a672..0e3a5ffc3 100644 --- a/misc/userscripts/README.md +++ b/misc/userscripts/README.md @@ -63,6 +63,8 @@ The following userscripts can be found on their own repositories. snippets on web pages to the clipboard via hints. - [Qute-Translate](https://github.com/AckslD/Qute-Translate): Translate URLs or selections via Google Translate. +- [qute-snippets](https://github.com/Aledosim/qute-snippets): Bind text snippets to a keyword + and retrieve they when you want. [Zotero]: https://www.zotero.org/ [Pocket]: https://getpocket.com/ diff --git a/misc/userscripts/readability-js b/misc/userscripts/readability-js index 207b29b4a..e189e5ee4 100755 --- a/misc/userscripts/readability-js +++ b/misc/userscripts/readability-js @@ -54,6 +54,35 @@ const HEADER = ` figure img { display: block; } + table, + th, + td { + border: 1px solid currentColor; + border-collapse: collapse; + padding: 6px; + vertical-align: top; + } + table { + margin: 5px; + } + pre { + padding: 16px; + overflow: auto; + line-height: 1.45; + background-color: #dddddd; + } + code { + padding: .2em .4em; + margin: 0; + background-color: #dddddd; + } + blockquote { + border-inline-start: 2px solid #333333 !important; + padding: 0; + padding-inline-start: 16px; + margin-inline-start: 24px; + border-radius: 5px; + } </style> <!-- This icon is licensed under the Mozilla Public License 2.0 (available at: https://www.mozilla.org/en-US/MPL/2.0/). The original icon can be found here: https://dxr.mozilla.org/mozilla-central/source/browser/themes/shared/reader/readerMode.svg --> @@ -68,13 +97,24 @@ const HEADER = ` </head>`; const scriptsDir = path.join(process.env.QUTE_DATA_DIR, 'userscripts'); const tmpFile = path.join(scriptsDir, '/readability.html'); -const domOpts = {url: process.env.QUTE_URL, contentType: "text/html; charset=utf-8"}; -if (!fs.existsSync(scriptsDir)){ +if (!fs.existsSync(scriptsDir)) { fs.mkdirSync(scriptsDir); } -JSDOM.fromFile(process.env.QUTE_HTML, domOpts).then(dom => { +let getDOM, domOpts, target; +// When hinting, use the selected hint instead of the current page +if (process.env.QUTE_MODE === 'hints') { + getDOM = JSDOM.fromURL; + target = process.env.QUTE_URL; +} +else { + getDOM = JSDOM.fromFile; + domOpts = {url: process.env.QUTE_URL, contentType: "text/html; charset=utf-8"}; + target = process.env.QUTE_HTML; +} + +getDOM(target, domOpts).then(dom => { let reader = new Readability(dom.window.document); let article = reader.parse(); let content = util.format(HEADER, article.title) + article.content; diff --git a/pytest.ini b/pytest.ini index 51411e11e..0b4fecf37 100644 --- a/pytest.ini +++ b/pytest.ini @@ -76,6 +76,7 @@ qt_log_ignore = ^QPaintDevice: Cannot destroy paint device that is being painted ^DirectWrite: CreateFontFaceFromHDC\(\) failed .* ^Attribute Qt::AA_ShareOpenGLContexts must be set before QCoreApplication is created\. + ^QHttpNetworkConnectionPrivate::_q_hostLookupFinished could not de-queue request, failed to report HostNotFoundError xfail_strict = true filterwarnings = error diff --git a/qutebrowser/__init__.py b/qutebrowser/__init__.py index 34799df17..b5b4b8c7c 100644 --- a/qutebrowser/__init__.py +++ b/qutebrowser/__init__.py @@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2020 Florian Bruhin (The Compiler)" __license__ = "GPL" __maintainer__ = __author__ __email__ = "mail@qutebrowser.org" -__version__ = "1.13.1" +__version__ = "1.14.0" __version_info__ = tuple(int(part) for part in __version__.split('.')) __description__ = "A keyboard-driven, vim-like browser based on PyQt5." diff --git a/qutebrowser/api/cmdutils.py b/qutebrowser/api/cmdutils.py index 5d74991c1..d0d69a04c 100644 --- a/qutebrowser/api/cmdutils.py +++ b/qutebrowser/api/cmdutils.py @@ -45,12 +45,12 @@ Possible values: value. - A python enum type: All members of the enum are possible values. - A ``typing.Union`` of multiple types above: Any of these types are valid - values, e.g., ``typing.Union[str, int]``. + values, e.g., ``Union[str, int]``. """ import inspect -import typing +from typing import Any, Callable, Iterable from qutebrowser.utils import qtutils from qutebrowser.commands import command, cmdexc @@ -91,8 +91,7 @@ def check_overflow(arg: int, ctype: str) -> None: "representation.".format(ctype)) -def check_exclusive(flags: typing.Iterable[bool], - names: typing.Iterable[str]) -> None: +def check_exclusive(flags: Iterable[bool], names: Iterable[str]) -> None: """Check if only one flag is set with exclusive flags. Raise a CommandError if not. @@ -113,7 +112,7 @@ class register: # noqa: N801,N806 pylint: disable=invalid-name def __init__(self, *, instance: str = None, name: str = None, - **kwargs: typing.Any) -> None: + **kwargs: Any) -> None: """Save decorator arguments. Gets called on parse-time with the decorator arguments. @@ -128,7 +127,7 @@ class register: # noqa: N801,N806 pylint: disable=invalid-name # The arguments to pass to Command. self._kwargs = kwargs - def __call__(self, func: typing.Callable) -> typing.Callable: + def __call__(self, func: Callable) -> Callable: """Register the command before running the function. Gets called when a function should be decorated. @@ -175,7 +174,7 @@ class argument: # noqa: N801,N806 pylint: disable=invalid-name def foo(bar: str): ... - For ``typing.Union`` types, the given ``choices`` are only checked if other + For ``Union`` types, the given ``choices`` are only checked if other types (like ``int``) don't match. The following arguments are supported for ``@cmdutils.argument``: @@ -197,11 +196,11 @@ class argument: # noqa: N801,N806 pylint: disable=invalid-name trailing underscores stripped and underscores replaced by dashes. """ - def __init__(self, argname: str, **kwargs: typing.Any) -> None: + def __init__(self, argname: str, **kwargs: Any) -> None: self._argname = argname # The name of the argument to handle. self._kwargs = kwargs # Valid ArgInfo members. - def __call__(self, func: typing.Callable) -> typing.Callable: + def __call__(self, func: Callable) -> Callable: funcname = func.__name__ if self._argname not in inspect.signature(func).parameters: diff --git a/qutebrowser/api/config.py b/qutebrowser/api/config.py index 3b84a999c..fb363d858 100644 --- a/qutebrowser/api/config.py +++ b/qutebrowser/api/config.py @@ -19,7 +19,7 @@ """Access to the qutebrowser configuration.""" -import typing +from typing import cast, Any from PyQt5.QtCore import QUrl @@ -35,9 +35,9 @@ from qutebrowser.config import config #: This also supports setting configuration values:: #: #: config.val.content.javascript.enabled = False -val = typing.cast('config.ConfigContainer', None) +val = cast('config.ConfigContainer', None) -def get(name: str, url: QUrl = None) -> typing.Any: +def get(name: str, url: QUrl = None) -> Any: """Get a value from the config based on a string name.""" return config.instance.get(name, url) diff --git a/qutebrowser/api/hook.py b/qutebrowser/api/hook.py index 9bd14a8a1..4eadb2a99 100644 --- a/qutebrowser/api/hook.py +++ b/qutebrowser/api/hook.py @@ -22,13 +22,13 @@ """Hooks for extensions.""" import importlib -import typing +from typing import Callable from qutebrowser.extensions import loader -def _add_module_info(func: typing.Callable) -> loader.ModuleInfo: +def _add_module_info(func: Callable) -> loader.ModuleInfo: """Add module info to the given function.""" module = importlib.import_module(func.__module__) return loader.add_module_info(module) @@ -48,7 +48,7 @@ class init: message.info("Extension initialized.") """ - def __call__(self, func: typing.Callable) -> typing.Callable: + def __call__(self, func: Callable) -> Callable: info = _add_module_info(func) if info.init_hook is not None: raise ValueError("init hook is already registered!") @@ -86,7 +86,7 @@ class config_changed: def __init__(self, option_filter: str = None) -> None: self._filter = option_filter - def __call__(self, func: typing.Callable) -> typing.Callable: + def __call__(self, func: Callable) -> Callable: info = _add_module_info(func) info.config_changed_hooks.append((self._filter, func)) return func diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 20459b890..27f03fd54 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -82,6 +82,8 @@ def run(args): if args.temp_basedir: args.basedir = tempfile.mkdtemp(prefix='qutebrowser-basedir-') + log.init.debug("Main process PID: {}".format(os.getpid())) + log.init.debug("Initializing directories...") standarddir.init(args) utils.preload_resources() @@ -553,15 +555,15 @@ class Application(QApplication): def event(self, e): """Handle macOS FileOpen events.""" - if e.type() == QEvent.FileOpen: - url = e.url() - if url.isValid(): - open_url(url, no_raise=True) - else: - message.error("Invalid URL: {}".format(url.errorString())) - else: + if e.type() != QEvent.FileOpen: return super().event(e) + url = e.url() + if url.isValid(): + open_url(url, no_raise=True) + else: + message.error("Invalid URL: {}".format(url.errorString())) + return True def __repr__(self): diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index f7d951b33..58c434a42 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -22,7 +22,8 @@ import enum import itertools import functools -import typing +from typing import (cast, TYPE_CHECKING, Any, Callable, Iterable, List, Optional, + Sequence, Set, Type, Union) import attr from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF, Qt, @@ -32,9 +33,10 @@ from PyQt5.QtWidgets import QWidget, QApplication, QDialog from PyQt5.QtPrintSupport import QPrintDialog, QPrinter from PyQt5.QtNetwork import QNetworkAccessManager -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from PyQt5.QtWebKit import QWebHistory - from PyQt5.QtWebEngineWidgets import QWebEngineHistory + from PyQt5.QtWebKitWidgets import QWebPage + from PyQt5.QtWebEngineWidgets import QWebEngineHistory, QWebEnginePage import pygments import pygments.lexers @@ -48,7 +50,7 @@ from qutebrowser.misc import miscwidgets, objects, sessions from qutebrowser.browser import eventfilter, inspector from qutebrowser.qt import sip -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from qutebrowser.browser import webelem from qutebrowser.browser.inspector import AbstractWebInspector @@ -71,7 +73,7 @@ def create(win_id: int, mode_manager = modeman.instance(win_id) if objects.backend == usertypes.Backend.QtWebEngine: from qutebrowser.browser.webengine import webenginetab - tab_class = webenginetab.WebEngineTab # type: typing.Type[AbstractTab] + tab_class: Type[AbstractTab] = webenginetab.WebEngineTab elif objects.backend == usertypes.Backend.QtWebKit: from qutebrowser.browser.webkit import webkittab tab_class = webkittab.WebKitTab @@ -100,13 +102,23 @@ class UnsupportedOperationError(WebTabError): """Raised when an operation is not supported with the given backend.""" -TerminationStatus = enum.Enum('TerminationStatus', [ - 'normal', - 'abnormal', # non-zero exit status - 'crashed', # e.g. segfault - 'killed', - 'unknown', -]) +class TerminationStatus(enum.Enum): + + """How a QtWebEngine renderer process terminated. + + Also see QWebEnginePage::RenderProcessTerminationStatus + """ + + #: Unknown render process status value gotten from Qt. + unknown = -1 + #: The render process terminated normally. + normal = 0 + #: The render process terminated with with a non-zero exit status. + abnormal = 1 + #: The render process crashed, for example because of a segmentation fault. + crashed = 2 + #: The render process was killed, for example by SIGKILL or task manager kill. + killed = 3 @attr.s @@ -131,19 +143,17 @@ class TabData: splitter: InspectorSplitter used to show inspector inside the tab. """ - keep_icon = attr.ib(False) # type: bool - viewing_source = attr.ib(False) # type: bool - inspector = attr.ib(None) # type: typing.Optional[AbstractWebInspector] - open_target = attr.ib( - usertypes.ClickTarget.normal) # type: usertypes.ClickTarget - override_target = attr.ib( - None) # type: typing.Optional[usertypes.ClickTarget] - pinned = attr.ib(False) # type: bool - fullscreen = attr.ib(False) # type: bool - netrc_used = attr.ib(False) # type: bool - input_mode = attr.ib(usertypes.KeyMode.normal) # type: usertypes.KeyMode - last_navigation = attr.ib(None) # type: usertypes.NavigationRequest - splitter = attr.ib(None) # type: miscwidgets.InspectorSplitter + keep_icon: bool = attr.ib(False) + viewing_source: bool = attr.ib(False) + inspector: Optional['AbstractWebInspector'] = attr.ib(None) + open_target: usertypes.ClickTarget = attr.ib(usertypes.ClickTarget.normal) + override_target: Optional[usertypes.ClickTarget] = attr.ib(None) + pinned: bool = attr.ib(False) + fullscreen: bool = attr.ib(False) + netrc_used: bool = attr.ib(False) + input_mode: usertypes.KeyMode = attr.ib(usertypes.KeyMode.normal) + last_navigation: usertypes.NavigationRequest = attr.ib(None) + splitter: miscwidgets.InspectorSplitter = attr.ib(None) def should_show_icon(self) -> bool: return (config.val.tabs.favicons.show == 'always' or @@ -154,13 +164,11 @@ class AbstractAction: """Attribute ``action`` of AbstractTab for Qt WebActions.""" - # The class actions are defined on (QWeb{Engine,}Page) - action_class = None # type: type - # The type of the actions (QWeb{Engine,}Page.WebAction) - action_base = None # type: type + action_class: Type[Union['QWebPage', 'QWebEnginePage']] + action_base: Type[Union['QWebPage.WebAction', 'QWebEnginePage.WebAction']] def __init__(self, tab: 'AbstractTab') -> None: - self._widget = typing.cast(QWidget, None) + self._widget = cast(QWidget, None) self._tab = tab def exit_fullscreen(self) -> None: @@ -211,7 +219,7 @@ class AbstractPrinting: """Attribute ``printing`` of AbstractTab for printing the page.""" def __init__(self, tab: 'AbstractTab') -> None: - self._widget = typing.cast(QWidget, None) + self._widget = cast(QWidget, None) self._tab = tab def check_pdf_support(self) -> None: @@ -243,7 +251,7 @@ class AbstractPrinting: raise NotImplementedError def to_printer(self, printer: QPrinter, - callback: typing.Callable[[bool], None] = None) -> None: + callback: Callable[[bool], None] = None) -> None: """Print the tab. Args: @@ -295,13 +303,13 @@ class AbstractSearch(QObject): #: Signal emitted when an existing search was cleared. cleared = pyqtSignal() - _Callback = typing.Callable[[bool], None] + _Callback = Callable[[bool], None] def __init__(self, tab: 'AbstractTab', parent: QWidget = None): super().__init__(parent) self._tab = tab - self._widget = typing.cast(QWidget, None) - self.text = None # type: typing.Optional[str] + self._widget = cast(QWidget, None) + self.text: Optional[str] = None self.search_displayed = False def _is_case_sensitive(self, ignore_case: usertypes.IgnoreCase) -> bool: @@ -364,7 +372,7 @@ class AbstractZoom(QObject): def __init__(self, tab: 'AbstractTab', parent: QWidget = None) -> None: super().__init__(parent) self._tab = tab - self._widget = typing.cast(QWidget, None) + self._widget = cast(QWidget, None) # Whether zoom was changed from the default. self._default_zoom_changed = False self._init_neighborlist() @@ -384,9 +392,8 @@ class AbstractZoom(QObject): It is a NeighborList with the zoom levels.""" levels = config.val.zoom.levels - self._neighborlist = usertypes.NeighborList( - levels, mode=usertypes.NeighborList.Modes.edge - ) # type: usertypes.NeighborList[float] + self._neighborlist: usertypes.NeighborList[float] = usertypes.NeighborList( + levels, mode=usertypes.NeighborList.Modes.edge) self._neighborlist.fuzzyval = config.val.zoom.default def apply_offset(self, offset: int) -> float: @@ -440,9 +447,9 @@ class SelectionState(enum.Enum): NOTE: Names need to line up with SelectionState in caret.js! """ - none = 1 - normal = 2 - line = 3 + none = enum.auto() + normal = enum.auto() + line = enum.auto() class AbstractCaret(QObject): @@ -455,14 +462,15 @@ class AbstractCaret(QObject): follow_selected_done = pyqtSignal() def __init__(self, + tab: 'AbstractTab', mode_manager: modeman.ModeManager, parent: QWidget = None) -> None: super().__init__(parent) - self._widget = typing.cast(QWidget, None) + self._widget = cast(QWidget, None) self._mode_manager = mode_manager mode_manager.entered.connect(self._on_mode_entered) mode_manager.left.connect(self._on_mode_left) - # self._tab is set by subclasses so mypy knows its concrete type. + self._tab = tab def _on_mode_entered(self, mode: usertypes.KeyMode) -> None: raise NotImplementedError @@ -521,7 +529,7 @@ class AbstractCaret(QObject): def drop_selection(self) -> None: raise NotImplementedError - def selection(self, callback: typing.Callable[[str], None]) -> None: + def selection(self, callback: Callable[[str], None]) -> None: raise NotImplementedError def reverse_selection(self) -> None: @@ -551,7 +559,7 @@ class AbstractScroller(QObject): def __init__(self, tab: 'AbstractTab', parent: QWidget = None): super().__init__(parent) self._tab = tab - self._widget = typing.cast(QWidget, None) + self._widget = cast(QWidget, None) if 'log-scroll-pos' in objects.debug_flags: self.perc_changed.connect(self._log_scroll_pos_change) @@ -619,11 +627,6 @@ class AbstractHistoryPrivate: """Private API related to the history.""" - def __init__(self, tab: 'AbstractTab'): - self._tab = tab - self._history = typing.cast( - typing.Union['QWebHistory', 'QWebEngineHistory'], None) - def serialize(self) -> bytes: """Serialize into an opaque format understood by self.deserialize.""" raise NotImplementedError @@ -632,7 +635,7 @@ class AbstractHistoryPrivate: """Deserialize from a format produced by self.serialize.""" raise NotImplementedError - def load_items(self, items: typing.Sequence) -> None: + def load_items(self, items: Sequence) -> None: """Deserialize from a list of WebHistoryItems.""" raise NotImplementedError @@ -643,14 +646,13 @@ class AbstractHistory: def __init__(self, tab: 'AbstractTab') -> None: self._tab = tab - self._history = typing.cast( - typing.Union['QWebHistory', 'QWebEngineHistory'], None) - self.private_api = AbstractHistoryPrivate(tab) + self._history = cast(Union['QWebHistory', 'QWebEngineHistory'], None) + self.private_api = AbstractHistoryPrivate() def __len__(self) -> int: raise NotImplementedError - def __iter__(self) -> typing.Iterable: + def __iter__(self) -> Iterable: raise NotImplementedError def _check_count(self, count: int) -> None: @@ -687,16 +689,16 @@ class AbstractHistory: def can_go_forward(self) -> bool: raise NotImplementedError - def _item_at(self, i: int) -> typing.Any: + def _item_at(self, i: int) -> Any: raise NotImplementedError - def _go_to_item(self, item: typing.Any) -> None: + def _go_to_item(self, item: Any) -> None: raise NotImplementedError - def back_items(self) -> typing.List[typing.Any]: + def back_items(self) -> List[Any]: raise NotImplementedError - def forward_items(self) -> typing.List[typing.Any]: + def forward_items(self) -> List[Any]: raise NotImplementedError @@ -704,15 +706,13 @@ class AbstractElements: """Finding and handling of elements on the page.""" - _MultiCallback = typing.Callable[ - [typing.Sequence['webelem.AbstractWebElement']], None] - _SingleCallback = typing.Callable[ - [typing.Optional['webelem.AbstractWebElement']], None] - _ErrorCallback = typing.Callable[[Exception], None] + _MultiCallback = Callable[[Sequence['webelem.AbstractWebElement']], None] + _SingleCallback = Callable[[Optional['webelem.AbstractWebElement']], None] + _ErrorCallback = Callable[[Exception], None] - def __init__(self) -> None: - self._widget = typing.cast(QWidget, None) - # self._tab is set by subclasses so mypy knows its concrete type. + def __init__(self, tab: 'AbstractTab') -> None: + self._widget = cast(QWidget, None) + self._tab = tab def find_css(self, selector: str, callback: _MultiCallback, @@ -772,7 +772,7 @@ class AbstractAudio(QObject): def __init__(self, tab: 'AbstractTab', parent: QWidget = None) -> None: super().__init__(parent) - self._widget = typing.cast(QWidget, None) + self._widget = cast(QWidget, None) self._tab = tab def set_muted(self, muted: bool, override: bool = False) -> None: @@ -803,7 +803,7 @@ class AbstractTabPrivate: def __init__(self, mode_manager: modeman.ModeManager, tab: 'AbstractTab') -> None: - self._widget = typing.cast(QWidget, None) + self._widget = cast(QWidget, None) self._tab = tab self._mode_manager = mode_manager @@ -821,7 +821,7 @@ class AbstractTabPrivate: return def _auto_insert_mode_cb( - elem: typing.Optional['webelem.AbstractWebElement'] + elem: Optional['webelem.AbstractWebElement'] ) -> None: """Called from JS after finding the focused element.""" if elem is None: @@ -836,7 +836,7 @@ class AbstractTabPrivate: def clear_ssl_errors(self) -> None: raise NotImplementedError - def networkaccessmanager(self) -> typing.Optional[QNetworkAccessManager]: + def networkaccessmanager(self) -> Optional[QNetworkAccessManager]: """Get the QNetworkAccessManager for this tab. This is only implemented for QtWebKit. @@ -928,7 +928,7 @@ class AbstractTab(QWidget): # Note that we remember hosts here, without scheme/port: # QtWebEngine/Chromium also only remembers hostnames, and certificates are # for a given hostname anyways. - _insecure_hosts = set() # type: typing.Set[str] + _insecure_hosts: Set[str] = set() def __init__(self, *, win_id: int, mode_manager: modeman.ModeManager, @@ -948,12 +948,12 @@ class AbstractTab(QWidget): self.data = TabData() self._layout = miscwidgets.WrapperLayout(self) - self._widget = typing.cast(QWidget, None) + self._widget = cast(QWidget, None) self._progress = 0 self._load_status = usertypes.LoadStatus.none self._tab_event_filter = eventfilter.TabEventFilter( self, parent=self) - self.backend = None # type: typing.Optional[usertypes.Backend] + self.backend: Optional[usertypes.Backend] = None # If true, this tab has been requested to be removed (or is removed). self.pending_removal = False @@ -1156,7 +1156,7 @@ class AbstractTab(QWidget): self.send_event(release_evt) def dump_async(self, - callback: typing.Callable[[str], None], *, + callback: Callable[[str], None], *, plain: bool = False) -> None: """Dump the current page's html asynchronously. @@ -1168,8 +1168,8 @@ class AbstractTab(QWidget): def run_js_async( self, code: str, - callback: typing.Callable[[typing.Any], None] = None, *, - world: typing.Union[usertypes.JsWorld, int] = None + callback: Callable[[Any], None] = None, *, + world: Union[usertypes.JsWorld, int] = None ) -> None: """Run javascript async. diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 13f57e7dc..5a65384f3 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -22,7 +22,7 @@ import os.path import shlex import functools -import typing +from typing import cast, Callable, Dict, Union from PyQt5.QtWidgets import QApplication, QTabBar from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QEvent, QUrlQuery @@ -600,15 +600,14 @@ class CommandDispatcher: widget = self._current_widget() url = self._current_url() - handlers = { + handlers: Dict[str, Callable] = { 'prev': functools.partial(navigate.prevnext, prev=True), 'next': functools.partial(navigate.prevnext, prev=False), 'up': navigate.path_up, - 'decrement': functools.partial(navigate.incdec, - inc_or_dec='decrement'), - 'increment': functools.partial(navigate.incdec, - inc_or_dec='increment'), - } # type: typing.Dict[str, typing.Callable] + 'strip': navigate.strip, + 'decrement': functools.partial(navigate.incdec, inc_or_dec='decrement'), + 'increment': functools.partial(navigate.incdec, inc_or_dec='increment'), + } try: if where in ['prev', 'next']: @@ -949,7 +948,7 @@ class CommandDispatcher: @cmdutils.argument('index', choices=['last', 'stack-next', 'stack-prev'], completion=miscmodels.tab_focus) @cmdutils.argument('count', value=cmdutils.Value.count) - def tab_focus(self, index: typing.Union[str, int] = None, + def tab_focus(self, index: Union[str, int] = None, count: int = None, no_last: bool = False) -> None: """Select the tab given as argument/[count]. @@ -993,7 +992,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('index', choices=['+', '-']) @cmdutils.argument('count', value=cmdutils.Value.count) - def tab_move(self, index: typing.Union[str, int] = None, + def tab_move(self, index: Union[str, int] = None, count: int = None) -> None: """Move the current tab according to the argument and [count]. @@ -1431,7 +1430,7 @@ class CommandDispatcher: query = QUrlQuery() query.addQueryItem('level', level) if plain: - query.addQueryItem('plain', typing.cast(str, None)) + query.addQueryItem('plain', cast(str, None)) if logfilter: try: @@ -1652,7 +1651,7 @@ class CommandDispatcher: url: bool = False, quiet: bool = False, *, - world: typing.Union[usertypes.JsWorld, int] = None) -> None: + world: Union[usertypes.JsWorld, int] = None) -> None: """Evaluate a JavaScript string. Args: diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index 3c3932c5f..5430cde20 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -28,7 +28,7 @@ import functools import pathlib import tempfile import enum -import typing +from typing import Any, Dict, IO, List, MutableSequence, Optional, Union from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QObject, QModelIndex, QTimer, QAbstractListModel, QUrl) @@ -49,7 +49,7 @@ class ModelRole(enum.IntEnum): # Remember the last used directory -last_used_directory = None # type: typing.Optional[str] +last_used_directory: Optional[str] = None # All REFRESH_INTERVAL milliseconds, speeds will be recalculated and downloads # redrawn. @@ -228,7 +228,7 @@ def suggested_fn_from_title(url_path, title=None): ext_whitelist = [".html", ".htm", ".php", ""] _, ext = os.path.splitext(url_path) - suggested_fn = None # type: typing.Optional[str] + suggested_fn: Optional[str] = None if ext.lower() in ext_whitelist and title: suggested_fn = utils.sanitize_filename(title, shorten=True) if not suggested_fn.lower().endswith((".html", ".htm")): @@ -355,8 +355,7 @@ class DownloadItemStats(QObject): self.speed = 0 self._last_done = 0 samples = int(self.SPEED_AVG_WINDOW * (1000 / _REFRESH_INTERVAL)) - self._speed_avg = collections.deque( - maxlen=samples) # type: typing.MutableSequence[float] + self._speed_avg: MutableSequence[float] = collections.deque(maxlen=samples) def update_speed(self): """Recalculate the current download speed. @@ -459,12 +458,14 @@ class AbstractDownloadItem(QObject): self.basename = '???' self.successful = False - self.fileobj = UnsupportedAttribute( - ) # type: typing.Union[UnsupportedAttribute, typing.IO[bytes], None] - self.raw_headers = UnsupportedAttribute( - ) # type: typing.Union[UnsupportedAttribute, typing.Dict[bytes,bytes]] + self.fileobj: Union[ + UnsupportedAttribute, IO[bytes], None + ] = UnsupportedAttribute() + self.raw_headers: Union[ + UnsupportedAttribute, Dict[bytes, bytes] + ] = UnsupportedAttribute() - self._filename = None # type: typing.Optional[str] + self._filename: Optional[str] = None self._dead = False def __repr__(self): @@ -877,7 +878,7 @@ class AbstractDownloadManager(QObject): def __init__(self, parent=None): super().__init__(parent) - self.downloads = [] # type: typing.List[AbstractDownloadItem] + self.downloads: List[AbstractDownloadItem] = [] self._update_timer = usertypes.Timer(self, 'download-update') self._update_timer.timeout.connect(self._update_gui) self._update_timer.setInterval(_REFRESH_INTERVAL) @@ -1251,7 +1252,7 @@ class DownloadModel(QAbstractListModel): item = self[index.row()] if role == Qt.DisplayRole: - data = str(item) # type: typing.Any + data: Any = str(item) elif role == Qt.ForegroundRole: data = item.get_status_color('fg') elif role == Qt.BackgroundRole: @@ -1297,7 +1298,7 @@ class TempDownloadManager: """ def __init__(self): - self.files = [] # type: typing.MutableSequence[typing.IO[bytes]] + self.files: MutableSequence[IO[bytes]] = [] self._tmpdir = None def cleanup(self): diff --git a/qutebrowser/browser/downloadview.py b/qutebrowser/browser/downloadview.py index 178fb5357..bff4a8c93 100644 --- a/qutebrowser/browser/downloadview.py +++ b/qutebrowser/browser/downloadview.py @@ -20,7 +20,7 @@ """The ListView to display downloads in.""" import functools -import typing +from typing import Callable, MutableSequence, Tuple, Union from PyQt5.QtCore import pyqtSlot, QSize, Qt, QTimer from PyQt5.QtWidgets import QListView, QSizePolicy, QMenu, QStyleFactory @@ -54,10 +54,10 @@ def update_geometry(obj): QTimer.singleShot(0, _update_geometry) -_ActionListType = typing.MutableSequence[ - typing.Union[ - typing.Tuple[None, None], # separator - typing.Tuple[str, typing.Callable[[], None]], +_ActionListType = MutableSequence[ + Union[ + Tuple[None, None], # separator + Tuple[str, Callable[[], None]], ] ] @@ -142,7 +142,7 @@ class DownloadView(QListView): item: The DownloadItem to get the actions for, or None. """ model = self.model() - actions = [] # type: _ActionListType + actions: _ActionListType = [] if item is None: pass elif item.done: diff --git a/qutebrowser/browser/greasemonkey.py b/qutebrowser/browser/greasemonkey.py index 89d720682..5a5d55a6d 100644 --- a/qutebrowser/browser/greasemonkey.py +++ b/qutebrowser/browser/greasemonkey.py @@ -26,7 +26,7 @@ import fnmatch import functools import glob import textwrap -import typing +from typing import cast, List, Sequence import attr from PyQt5.QtCore import pyqtSignal, QObject, QUrl @@ -39,7 +39,7 @@ from qutebrowser.browser import downloads from qutebrowser.misc import objects -gm_manager = typing.cast('GreasemonkeyManager', None) +gm_manager = cast('GreasemonkeyManager', None) def _scripts_dir(): @@ -54,10 +54,10 @@ class GreasemonkeyScript: def __init__(self, properties, code, # noqa: C901 pragma: no mccabe filename=None): self._code = code - self.includes = [] # type: typing.Sequence[str] - self.matches = [] # type: typing.Sequence[str] - self.excludes = [] # type: typing.Sequence[str] - self.requires = [] # type: typing.Sequence[str] + self.includes: Sequence[str] = [] + self.matches: Sequence[str] = [] + self.excludes: Sequence[str] = [] + self.requires: Sequence[str] = [] self.description = None self.namespace = None self.run_at = None @@ -259,11 +259,10 @@ class GreasemonkeyManager(QObject): def __init__(self, parent=None): super().__init__(parent) - self._run_start = [] # type: typing.List[GreasemonkeyScript] - self._run_end = [] # type: typing.List[GreasemonkeyScript] - self._run_idle = [] # type: typing.List[GreasemonkeyScript] - self._in_progress_dls = [ - ] # type: typing.List[downloads.AbstractDownloadItem] + self._run_start: List[GreasemonkeyScript] = [] + self._run_end: List[GreasemonkeyScript] = [] + self._run_idle: List[GreasemonkeyScript] = [] + self._in_progress_dls: List[downloads.AbstractDownloadItem] = [] self.load_scripts() diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index daf38a755..f914f3085 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -20,16 +20,17 @@ """A HintManager to draw hints over links.""" import collections -import typing import functools import os import re import html import enum from string import ascii_lowercase +from typing import (TYPE_CHECKING, Callable, Dict, Iterable, Iterator, List, Mapping, + MutableSequence, Optional, Sequence, Set) import attr -from PyQt5.QtCore import pyqtSlot, QObject, Qt, QUrl +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, Qt, QUrl from PyQt5.QtWidgets import QLabel from qutebrowser.config import config, configexc @@ -38,14 +39,30 @@ from qutebrowser.browser import webelem, history from qutebrowser.commands import userscripts, runners from qutebrowser.api import cmdutils from qutebrowser.utils import usertypes, log, qtutils, message, objreg, utils -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from qutebrowser.browser import browsertab -Target = enum.Enum('Target', ['normal', 'current', 'tab', 'tab_fg', 'tab_bg', - 'window', 'yank', 'yank_primary', 'run', 'fill', - 'hover', 'download', 'userscript', 'spawn', - 'delete', 'right_click']) +class Target(enum.Enum): + + """What action to take on a hint.""" + + normal = enum.auto() + current = enum.auto() + tab = enum.auto() + tab_fg = enum.auto() + tab_bg = enum.auto() + window = enum.auto() + yank = enum.auto() + yank_primary = enum.auto() + run = enum.auto() + fill = enum.auto() + hover = enum.auto() + download = enum.auto() + userscript = enum.auto() + spawn = enum.auto() + delete = enum.auto() + right_click = enum.auto() class HintingError(Exception): @@ -164,22 +181,22 @@ class HintContext: group: The group of web elements to hint. """ - all_labels = attr.ib(attr.Factory(list)) # type: typing.List[HintLabel] - labels = attr.ib(attr.Factory(dict)) # type: typing.Dict[str, HintLabel] - target = attr.ib(None) # type: Target - baseurl = attr.ib(None) # type: QUrl - to_follow = attr.ib(None) # type: str - rapid = attr.ib(False) # type: bool - first_run = attr.ib(True) # type: bool - add_history = attr.ib(False) # type: bool - filterstr = attr.ib(None) # type: str - args = attr.ib(attr.Factory(list)) # type: typing.List[str] - tab = attr.ib(None) # type: browsertab.AbstractTab - group = attr.ib(None) # type: str - hint_mode = attr.ib(None) # type: str - first = attr.ib(False) # type: bool - - def get_args(self, urlstr: str) -> typing.Sequence[str]: + all_labels: List[HintLabel] = attr.ib(attr.Factory(list)) + labels: Dict[str, HintLabel] = attr.ib(attr.Factory(dict)) + target: Target = attr.ib(None) + baseurl: QUrl = attr.ib(None) + to_follow: str = attr.ib(None) + rapid: bool = attr.ib(False) + first_run: bool = attr.ib(True) + add_history: bool = attr.ib(False) + filterstr: str = attr.ib(None) + args: List[str] = attr.ib(attr.Factory(list)) + tab: 'browsertab.AbstractTab' = attr.ib(None) + group: str = attr.ib(None) + hint_mode: str = attr.ib(None) + first: bool = attr.ib(False) + + def get_args(self, urlstr: str) -> Sequence[str]: """Get the arguments, with {hint-url} replaced by the given URL.""" args = [] for arg in self.args: @@ -336,8 +353,8 @@ class HintActions: commandrunner.run_safely('spawn ' + ' '.join(args)) -_ElemsType = typing.Sequence[webelem.AbstractWebElement] -_HintStringsType = typing.MutableSequence[str] +_ElemsType = Sequence[webelem.AbstractWebElement] +_HintStringsType = MutableSequence[str] class HintManager(QObject): @@ -353,7 +370,7 @@ class HintManager(QObject): _tab_id: The tab ID this HintManager is associated with. Signals: - See HintActions + set_text: Request for the statusbar to change its text. """ HINT_TEXTS = { @@ -375,11 +392,13 @@ class HintManager(QObject): Target.delete: "Delete an element", } + set_text = pyqtSignal(str) + def __init__(self, win_id: int, parent: QObject = None) -> None: """Constructor.""" super().__init__(parent) self._win_id = win_id - self._context = None # type: typing.Optional[HintContext] + self._context: Optional[HintContext] = None self._word_hinter = WordHinter() self._actions = HintActions(win_id) @@ -402,10 +421,8 @@ class HintManager(QObject): for label in self._context.all_labels: label.cleanup() - text = self._get_text() - message_bridge = objreg.get('message-bridge', scope='window', - window=self._win_id) - message_bridge.maybe_reset_text(text) + self.set_text.emit('') + self._context = None def _hint_strings(self, elems: _ElemsType) -> _HintStringsType: @@ -511,12 +528,10 @@ class HintManager(QObject): Return: A list of shuffled hint strings. """ - buckets = [ - [] for i in range(length) - ] # type: typing.Sequence[_HintStringsType] + buckets: Sequence[_HintStringsType] = [[] for i in range(length)] for i, hint in enumerate(hints): buckets[i % len(buckets)].append(hint) - result = [] # type: _HintStringsType + result: _HintStringsType = [] for bucket in buckets: result += bucket return result @@ -541,7 +556,7 @@ class HintManager(QObject): A hint string. """ base = len(chars) - hintstr = [] # type: typing.MutableSequence[str] + hintstr: MutableSequence[str] = [] remainder = 0 while True: remainder = number % base @@ -636,9 +651,7 @@ class HintManager(QObject): modeman.enter(self._win_id, usertypes.KeyMode.hint, 'HintManager.start') - message_bridge = objreg.get('message-bridge', scope='window', - window=self._win_id) - message_bridge.set_text(self._get_text()) + self.set_text.emit(self._get_text()) if self._context.first: self._fire(strings[0]) @@ -771,7 +784,7 @@ class HintManager(QObject): error_cb=lambda err: message.error(str(err)), only_visible=True) - def _get_hint_mode(self, mode: typing.Optional[str]) -> str: + def _get_hint_mode(self, mode: Optional[str]) -> str: """Get the hinting mode to use based on a mode argument.""" if mode is None: return config.val.hints.mode @@ -783,7 +796,7 @@ class HintManager(QObject): raise cmdutils.CommandError("Invalid mode: {}".format(e)) return mode - def current_mode(self) -> typing.Optional[str]: + def current_mode(self) -> Optional[str]: """Return the currently active hinting mode (or None otherwise).""" if self._context is None: return None @@ -794,7 +807,7 @@ class HintManager(QObject): self, keystr: str = "", filterstr: str = "", - visible: typing.Mapping[str, HintLabel] = None + visible: Mapping[str, HintLabel] = None ) -> None: """Handle the auto_follow option.""" assert self._context is not None @@ -856,7 +869,7 @@ class HintManager(QObject): pass self._handle_auto_follow(keystr=keystr) - def filter_hints(self, filterstr: typing.Optional[str]) -> None: + def filter_hints(self, filterstr: Optional[str]) -> None: """Filter displayed hints according to a text. Args: @@ -1027,7 +1040,7 @@ class WordHinter: def __init__(self) -> None: # will be initialized on first use. - self.words = set() # type: typing.Set[str] + self.words: Set[str] = set() self.dictionary = None def ensure_initialized(self) -> None: @@ -1059,10 +1072,10 @@ class WordHinter: def extract_tag_words( self, elem: webelem.AbstractWebElement - ) -> typing.Iterator[str]: + ) -> Iterator[str]: """Extract tag words form the given element.""" - _extractor_type = typing.Callable[[webelem.AbstractWebElement], str] - attr_extractors = { + _extractor_type = Callable[[webelem.AbstractWebElement], str] + attr_extractors: Mapping[str, _extractor_type] = { "alt": lambda elem: elem["alt"], "name": lambda elem: elem["name"], "title": lambda elem: elem["title"], @@ -1070,7 +1083,7 @@ class WordHinter: "src": lambda elem: elem["src"].split('/')[-1], "href": lambda elem: elem["href"].split('/')[-1], "text": str, - } # type: typing.Mapping[str, _extractor_type] + } extractable_attrs = collections.defaultdict(list, { "img": ["alt", "title", "src"], @@ -1086,8 +1099,8 @@ class WordHinter: def tag_words_to_hints( self, - words: typing.Iterable[str] - ) -> typing.Iterator[str]: + words: Iterable[str] + ) -> Iterator[str]: """Take words and transform them to proper hints if possible.""" for candidate in words: if not candidate: @@ -1098,20 +1111,20 @@ class WordHinter: if 4 < match.end() - match.start() < 8: yield candidate[match.start():match.end()].lower() - def any_prefix(self, hint: str, existing: typing.Iterable[str]) -> bool: + def any_prefix(self, hint: str, existing: Iterable[str]) -> bool: return any(hint.startswith(e) or e.startswith(hint) for e in existing) def filter_prefixes( self, - hints: typing.Iterable[str], - existing: typing.Iterable[str] - ) -> typing.Iterator[str]: + hints: Iterable[str], + existing: Iterable[str] + ) -> Iterator[str]: """Filter hints which don't start with the given prefix.""" return (h for h in hints if not self.any_prefix(h, existing)) def new_hint_for(self, elem: webelem.AbstractWebElement, - existing: typing.Iterable[str], - fallback: typing.Iterable[str]) -> typing.Optional[str]: + existing: Iterable[str], + fallback: Iterable[str]) -> Optional[str]: """Return a hint for elem, not conflicting with the existing.""" new = self.tag_words_to_hints(self.extract_tag_words(elem)) new_no_prefixes = self.filter_prefixes(new, existing) @@ -1135,7 +1148,7 @@ class WordHinter: """ self.ensure_initialized() hints = [] - used_hints = set() # type: typing.Set[str] + used_hints: Set[str] = set() words = iter(self.words) for elem in elems: hint = self.new_hint_for(elem, used_hints, words) diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py index b7221dc15..89061cebf 100644 --- a/qutebrowser/browser/history.py +++ b/qutebrowser/browser/history.py @@ -22,7 +22,7 @@ import os import time import contextlib -import typing +from typing import cast, Mapping, MutableSequence from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal from PyQt5.QtWidgets import QProgressDialog, QApplication @@ -35,7 +35,7 @@ from qutebrowser.misc import objects, sql # increment to indicate that HistoryCompletion must be regenerated _USER_VERSION = 2 -web_history = typing.cast('WebHistory', None) +web_history = cast('WebHistory', None) class HistoryProgress: @@ -208,11 +208,11 @@ class WebHistory(sql.SqlTable): return any(pattern.matches(url) for pattern in patterns) def _rebuild_completion(self): - data = { + data: Mapping[str, MutableSequence[str]] = { 'url': [], 'title': [], 'last_atime': [] - } # type: typing.Mapping[str, typing.MutableSequence[str]] + } # select the latest entry for each url q = sql.Query('SELECT url, title, max(atime) AS atime FROM History ' 'WHERE NOT redirect and url NOT LIKE "qute://back%" ' diff --git a/qutebrowser/browser/inspector.py b/qutebrowser/browser/inspector.py index 390762ae0..d8fa1b6f0 100644 --- a/qutebrowser/browser/inspector.py +++ b/qutebrowser/browser/inspector.py @@ -21,8 +21,8 @@ import base64 import binascii -import typing import enum +from typing import cast, Optional from PyQt5.QtWidgets import QWidget from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QEvent @@ -65,11 +65,11 @@ class Position(enum.Enum): """Where the inspector is shown.""" - right = 1 - left = 2 - top = 3 - bottom = 4 - window = 5 + right = enum.auto() + left = enum.auto() + top = enum.auto() + bottom = enum.auto() + window = enum.auto() class Error(Exception): @@ -119,10 +119,10 @@ class AbstractWebInspector(QWidget): win_id: int, parent: QWidget = None) -> None: super().__init__(parent) - self._widget = typing.cast(QWidget, None) + self._widget = cast(QWidget, None) self._layout = miscwidgets.WrapperLayout(self) self._splitter = splitter - self._position = None # type: typing.Optional[Position] + self._position: Optional[Position] = None self._win_id = win_id self._event_filter = _EventFilter(parent=self) @@ -163,7 +163,7 @@ class AbstractWebInspector(QWidget): modeman.enter(self._win_id, usertypes.KeyMode.insert, reason='Inspector clicked', only_if_normal=True) - def set_position(self, position: typing.Optional[Position]) -> None: + def set_position(self, position: Optional[Position]) -> None: """Set the position of the inspector. If the position is None, the last known position is used. diff --git a/qutebrowser/browser/navigate.py b/qutebrowser/browser/navigate.py index ddc100d14..b852ab29e 100644 --- a/qutebrowser/browser/navigate.py +++ b/qutebrowser/browser/navigate.py @@ -21,7 +21,7 @@ import re import posixpath -import typing +from typing import Optional, Set from PyQt5.QtCore import QUrl @@ -97,9 +97,9 @@ def incdec(url, count, inc_or_dec): window: Open the link in a new window. """ urlutils.ensure_valid(url) - segments = ( + segments: Optional[Set[str]] = ( set(config.val.url.incdec_segments) - ) # type: typing.Optional[typing.Set[str]] + ) if segments is None: segments = {'path', 'query'} diff --git a/qutebrowser/browser/network/pac.py b/qutebrowser/browser/network/pac.py index 6ae01c7d8..3b5686a03 100644 --- a/qutebrowser/browser/network/pac.py +++ b/qutebrowser/browser/network/pac.py @@ -21,7 +21,7 @@ import sys import functools -import typing +from typing import Optional from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, QUrl from PyQt5.QtNetwork import (QNetworkProxy, QNetworkRequest, QHostInfo, @@ -66,7 +66,7 @@ def _js_slot(*args): return self._error_con.callAsConstructor([e]) # pylint: enable=protected-access - deco = pyqtSlot(*args, result=QJSValue) # type: ignore[arg-type] + deco = pyqtSlot(*args, result=QJSValue) return deco(new_method) return _decorator @@ -251,8 +251,7 @@ class PACFetcher(QObject): url.setScheme(url.scheme()[len(pac_prefix):]) self._pac_url = url - self._manager = QNetworkAccessManager( - ) # type: typing.Optional[QNetworkAccessManager] + self._manager: Optional[QNetworkAccessManager] = QNetworkAccessManager() self._manager.setProxy(QNetworkProxy(QNetworkProxy.NoProxy)) self._pac = None self._error_message = None diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py index 0bafeeaf9..44e6c45d5 100644 --- a/qutebrowser/browser/qtnetworkdownloads.py +++ b/qutebrowser/browser/qtnetworkdownloads.py @@ -23,7 +23,7 @@ import io import os.path import shutil import functools -import typing +from typing import Dict, IO, Optional import attr from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, QUrl @@ -92,8 +92,8 @@ class DownloadItem(downloads.AbstractDownloadItem): reply: The QNetworkReply to download. """ super().__init__(manager=manager, parent=manager) - self.fileobj = None # type: typing.Optional[typing.IO[bytes]] - self.raw_headers = {} # type: typing.Dict[bytes, bytes] + self.fileobj: Optional[IO[bytes]] = None + self.raw_headers: Dict[bytes, bytes] = {} self._autoclose = True self._retry_info = None diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index b661f533d..d36bb746a 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -31,15 +31,8 @@ import time import textwrap import urllib import collections -import base64 -import typing -from typing import TypeVar, Callable, Union, Tuple - -try: - import secrets -except ImportError: - # New in Python 3.6 - secrets = None # type: ignore[assignment] +import secrets +from typing import TypeVar, Callable, Dict, List, Optional, Union, Sequence, Tuple from PyQt5.QtCore import QUrlQuery, QUrl, qVersion @@ -112,7 +105,7 @@ class add_handler: # noqa: N801,N806 pylint: disable=invalid-name def __init__(self, name): self._name = name - self._function = None # type: typing.Optional[typing.Callable] + self._function: Optional[Callable] = None def __call__(self, function: _Handler) -> _Handler: self._function = function @@ -125,7 +118,7 @@ class add_handler: # noqa: N801,N806 pylint: disable=invalid-name return self._function(*args, **kwargs) -def data_for_url(url: QUrl) -> typing.Tuple[str, bytes]: +def data_for_url(url: QUrl) -> Tuple[str, bytes]: """Get the data to show for the given URL. Args: @@ -199,8 +192,7 @@ def qute_bookmarks(_url: QUrl) -> _HandlerRet: @add_handler('tabs') def qute_tabs(_url: QUrl) -> _HandlerRet: """Handler for qute://tabs. Display information about all open tabs.""" - tabs = collections.defaultdict( - list) # type: typing.Dict[str, typing.List[typing.Tuple[str, str]]] + tabs: Dict[str, List[Tuple[str, str]]] = collections.defaultdict(list) for win_id, window in objreg.window_registry.items(): if sip.isdeleted(window): continue @@ -221,7 +213,7 @@ def qute_tabs(_url: QUrl) -> _HandlerRet: def history_data( start_time: float, offset: int = None -) -> typing.Sequence[typing.Dict[str, typing.Union[str, int]]]: +) -> Sequence[Dict[str, Union[str, int]]]: """Return history data. Arguments: @@ -355,7 +347,7 @@ def qute_gpl(_url: QUrl) -> _HandlerRet: return 'text/html', utils.read_file('html/license.html') -def _asciidoc_fallback_path(html_path: str) -> typing.Optional[str]: +def _asciidoc_fallback_path(html_path: str) -> Optional[str]: """Fall back to plaintext asciidoc if the HTML is unavailable.""" path = html_path.replace('.html', '.asciidoc') try: @@ -449,12 +441,7 @@ def qute_settings(url: QUrl) -> _HandlerRet: # Requests to qute://settings/set should only be allowed from # qute://settings. As an additional security precaution, we generate a CSRF # token to use here. - if secrets: - csrf_token = secrets.token_urlsafe() - else: - # On Python < 3.6, from secrets.py - token = base64.urlsafe_b64encode(os.urandom(32)) - csrf_token = token.rstrip(b'=').decode('ascii') + csrf_token = secrets.token_urlsafe() src = jinja.render('settings.html', title='settings', configdata=configdata, diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py index 715487def..fb213ed62 100644 --- a/qutebrowser/browser/shared.py +++ b/qutebrowser/browser/shared.py @@ -22,7 +22,7 @@ import os import html import netrc -import typing +from typing import Callable, Mapping from PyQt5.QtCore import QUrl @@ -134,13 +134,13 @@ def javascript_alert(url, js_msg, abort_on, *, escape_msg=True): # Needs to line up with the values allowed for the # content.javascript.log setting. -_JS_LOGMAP = { +_JS_LOGMAP: Mapping[str, Callable[[str], None]] = { 'none': lambda arg: None, 'debug': log.js.debug, 'info': log.js.info, 'warning': log.js.warning, 'error': log.js.error, -} # type: typing.Mapping[str, typing.Callable[[str], None]] +} def javascript_log_message(level, source, line, msg): diff --git a/qutebrowser/browser/signalfilter.py b/qutebrowser/browser/signalfilter.py index b70deb165..348a7a2ff 100644 --- a/qutebrowser/browser/signalfilter.py +++ b/qutebrowser/browser/signalfilter.py @@ -50,7 +50,7 @@ class SignalFilter(QObject): """Factory for partial _filter_signals functions. Args: - signal: The pyqtSignal to filter. + signal: The pyqtBoundSignal to filter. tab: The WebView to create filters for. Return: diff --git a/qutebrowser/browser/urlmarks.py b/qutebrowser/browser/urlmarks.py index 18fd15771..46d9d450d 100644 --- a/qutebrowser/browser/urlmarks.py +++ b/qutebrowser/browser/urlmarks.py @@ -30,7 +30,7 @@ import os.path import html import functools import collections -import typing +from typing import MutableMapping from PyQt5.QtCore import pyqtSignal, QUrl, QObject @@ -78,8 +78,7 @@ class UrlMarkManager(QObject): """Initialize and read quickmarks.""" super().__init__(parent) - self.marks = collections.OrderedDict( - ) # type: typing.MutableMapping[str, str] + self.marks: MutableMapping[str, str] = collections.OrderedDict() self._init_lineparser() for line in self._lineparser: diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py index e79b5145e..7a888daeb 100644 --- a/qutebrowser/browser/webelem.py +++ b/qutebrowser/browser/webelem.py @@ -19,7 +19,7 @@ """Generic web element related code.""" -import typing +from typing import cast, TYPE_CHECKING, Iterator, Optional, Set, Union import collections.abc from PyQt5.QtCore import QUrl, Qt, QEvent, QTimer, QRect, QPoint @@ -29,11 +29,11 @@ from qutebrowser.config import config from qutebrowser.keyinput import modeman from qutebrowser.utils import log, usertypes, utils, qtutils, objreg -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from qutebrowser.browser import browsertab -JsValueType = typing.Union[int, float, str, None] +JsValueType = Union[int, float, str, None] class Error(Exception): @@ -80,7 +80,7 @@ class AbstractWebElement(collections.abc.MutableMapping): def __delitem__(self, key: str) -> None: raise NotImplementedError - def __iter__(self) -> typing.Iterator[str]: + def __iter__(self) -> Iterator[str]: raise NotImplementedError def __len__(self) -> int: @@ -88,8 +88,7 @@ class AbstractWebElement(collections.abc.MutableMapping): def __repr__(self) -> str: try: - html = utils.compact_text( - self.outer_xml(), 500) # type: typing.Optional[str] + html: Optional[str] = utils.compact_text(self.outer_xml(), 500) except Error: html = None return utils.get_repr(self, html=html) @@ -102,7 +101,7 @@ class AbstractWebElement(collections.abc.MutableMapping): """Get the geometry for this element.""" raise NotImplementedError - def classes(self) -> typing.Set[str]: + def classes(self) -> Set[str]: """Get a set of classes assigned to this element.""" raise NotImplementedError @@ -282,7 +281,7 @@ class AbstractWebElement(collections.abc.MutableMapping): """Remove target from link.""" raise NotImplementedError - def resolve_url(self, baseurl: QUrl) -> typing.Optional[QUrl]: + def resolve_url(self, baseurl: QUrl) -> Optional[QUrl]: """Resolve the URL in the element's src/href attribute. Args: @@ -357,16 +356,12 @@ class AbstractWebElement(collections.abc.MutableMapping): else: target_modifiers[usertypes.ClickTarget.tab_bg] |= Qt.ShiftModifier - modifiers = typing.cast(Qt.KeyboardModifiers, - target_modifiers[click_target]) + modifiers = cast(Qt.KeyboardModifiers, target_modifiers[click_target]) events = [ - QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton, - Qt.NoModifier), - QMouseEvent(QEvent.MouseButtonPress, pos, button, button, - modifiers), - QMouseEvent(QEvent.MouseButtonRelease, pos, button, Qt.NoButton, - modifiers), + QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton, Qt.NoModifier), + QMouseEvent(QEvent.MouseButtonPress, pos, button, button, modifiers), + QMouseEvent(QEvent.MouseButtonRelease, pos, button, Qt.NoButton, modifiers), ] for evt in events: diff --git a/qutebrowser/browser/webengine/darkmode.py b/qutebrowser/browser/webengine/darkmode.py new file mode 100644 index 000000000..bf0319702 --- /dev/null +++ b/qutebrowser/browser/webengine/darkmode.py @@ -0,0 +1,307 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2020 Florian Bruhin (The Compiler) <mail@qutebrowser.org> +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. + +"""Get darkmode arguments to pass to Qt. + +Overview of blink setting names based on the Qt version: + +Qt 5.10 +------- + +First implementation, called "high contrast mode". + +- highContrastMode (kOff/kSimpleInvertForTesting/kInvertBrightness/kInvertLightness) +- highContrastGrayscale (bool) +- highContrastContrast (float) +- highContractImagePolicy (kFilterAll/kFilterNone) + +Qt 5.11, 5.12, 5.13 +------------------- + +New "smart" image policy. + +- Mode/Grayscale/Contrast as above +- highContractImagePolicy (kFilterAll/kFilterNone/kFilterSmart [new!]) + +Qt 5.14 +------- + +Renamed to "darkMode". + +- darkMode (kOff/kSimpleInvertForTesting/kInvertBrightness/kInvertLightness/ + kInvertLightnessLAB [new!]) +- darkModeGrayscale (bool) +- darkModeContrast (float) +- darkModeImagePolicy (kFilterAll/kFilterNone/kFilterSmart) +- darkModePagePolicy (kFilterAll/kFilterByBackground) [new!] +- darkModeTextBrightnessThreshold (int) [new!] +- darkModeBackgroundBrightnessThreshold (int) [new!] +- darkModeImageGrayscale (float) [new!] + +Qt 5.15.0 and 5.15.1 +-------------------- + +"darkMode" split into "darkModeEnabled" and "darkModeInversionAlgorithm". + +- darkModeEnabled (bool) [new!] +- darkModeInversionAlgorithm (kSimpleInvertForTesting/kInvertBrightness/ + kInvertLightness/kInvertLightnessLAB) +- Rest (except darkMode) as above. +- NOTE: smart image policy is broken with Qt 5.15.0! + +Qt 5.15.2 +--------- + +Prefix changed to "forceDarkMode". + +- As with Qt 5.15.0 / .1, but with "forceDarkMode" as prefix. +""" + +import enum +from typing import Any, Iterable, Iterator, Mapping, Optional, Set, Tuple, Union + +try: + from PyQt5.QtWebEngine import PYQT_WEBENGINE_VERSION +except ImportError: # pragma: no cover + # Added in PyQt 5.13 + PYQT_WEBENGINE_VERSION = None # type: ignore[assignment] + +from qutebrowser.config import config +from qutebrowser.utils import usertypes, qtutils, utils, log + + +class Variant(enum.Enum): + + """A dark mode variant.""" + + unavailable = enum.auto() + qt_510 = enum.auto() + qt_511_to_513 = enum.auto() + qt_514 = enum.auto() + qt_515_0 = enum.auto() + qt_515_1 = enum.auto() + qt_515_2 = enum.auto() + + +# Mapping from a colors.webpage.darkmode.algorithm setting value to +# Chromium's DarkModeInversionAlgorithm enum values. +_ALGORITHMS = { + # 0: kOff (not exposed) + # 1: kSimpleInvertForTesting (not exposed) + 'brightness-rgb': 2, # kInvertBrightness + 'lightness-hsl': 3, # kInvertLightness + 'lightness-cielab': 4, # kInvertLightnessLAB +} +# kInvertLightnessLAB is not available with Qt < 5.14 +_ALGORITHMS_BEFORE_QT_514 = _ALGORITHMS.copy() +_ALGORITHMS_BEFORE_QT_514['lightness-cielab'] = _ALGORITHMS['lightness-hsl'] + +# Mapping from a colors.webpage.darkmode.policy.images setting value to +# Chromium's DarkModeImagePolicy enum values. +_IMAGE_POLICIES = { + 'always': 0, # kFilterAll + 'never': 1, # kFilterNone + 'smart': 2, # kFilterSmart +} +# Image policy smart is not available with Qt 5.10 +_IMAGE_POLICIES_QT_510 = _IMAGE_POLICIES.copy() +_IMAGE_POLICIES_QT_510['smart'] = _IMAGE_POLICIES['never'] + +# Mapping from a colors.webpage.darkmode.policy.page setting value to +# Chromium's DarkModePagePolicy enum values. +_PAGE_POLICIES = { + 'always': 0, # kFilterAll + 'smart': 1, # kFilterByBackground +} + +_BOOLS = { + True: 'true', + False: 'false', +} + +_DarkModeSettingsType = Iterable[ + Tuple[ + str, # qutebrowser option name + str, # darkmode setting name + # Mapping from the config value to a string (or something convertable + # to a string) which gets passed to Chromium. + Optional[Mapping[Any, Union[str, int]]], + ], +] + +_DarkModeDefinitionType = Tuple[_DarkModeSettingsType, Set[str]] + +_QT_514_SETTINGS = [ + ('policy.images', 'darkModeImagePolicy', _IMAGE_POLICIES), + ('contrast', 'darkModeContrast', None), + ('grayscale.all', 'darkModeGrayscale', _BOOLS), + + ('policy.page', 'darkModePagePolicy', _PAGE_POLICIES), + ('threshold.text', 'darkModeTextBrightnessThreshold', None), + ('threshold.background', 'darkModeBackgroundBrightnessThreshold', None), + ('grayscale.images', 'darkModeImageGrayscale', None), +] + +# Our defaults for policy.images are different from Chromium's, so we mark it as +# mandatory setting - except on Qt 5.15.0 where we don't, so we don't get the +# workaround warning below if the setting wasn't explicitly customized. + +_DARK_MODE_DEFINITIONS: Mapping[Variant, _DarkModeDefinitionType] = { + Variant.unavailable: ([], set()), + + Variant.qt_515_2: ([ + # 'darkMode' renamed to 'forceDarkMode' + ('enabled', 'forceDarkModeEnabled', _BOOLS), + ('algorithm', 'forceDarkModeInversionAlgorithm', _ALGORITHMS), + + ('policy.images', 'forceDarkModeImagePolicy', _IMAGE_POLICIES), + ('contrast', 'forceDarkModeContrast', None), + ('grayscale.all', 'forceDarkModeGrayscale', _BOOLS), + + ('policy.page', 'forceDarkModePagePolicy', _PAGE_POLICIES), + ('threshold.text', 'forceDarkModeTextBrightnessThreshold', None), + ( + 'threshold.background', + 'forceDarkModeBackgroundBrightnessThreshold', + None + ), + ('grayscale.images', 'forceDarkModeImageGrayscale', None), + ], {'enabled', 'policy.images'}), + + Variant.qt_515_1: ([ + # 'policy.images' mandatory again + ('enabled', 'darkModeEnabled', _BOOLS), + ('algorithm', 'darkModeInversionAlgorithm', _ALGORITHMS), + + ('policy.images', 'darkModeImagePolicy', _IMAGE_POLICIES), + ('contrast', 'darkModeContrast', None), + ('grayscale.all', 'darkModeGrayscale', _BOOLS), + + ('policy.page', 'darkModePagePolicy', _PAGE_POLICIES), + ('threshold.text', 'darkModeTextBrightnessThreshold', None), + ('threshold.background', 'darkModeBackgroundBrightnessThreshold', None), + ('grayscale.images', 'darkModeImageGrayscale', None), + ], {'enabled', 'policy.images'}), + + Variant.qt_515_0: ([ + # 'policy.images' not mandatory because it's broken + ('enabled', 'darkModeEnabled', _BOOLS), + ('algorithm', 'darkModeInversionAlgorithm', _ALGORITHMS), + + ('policy.images', 'darkModeImagePolicy', _IMAGE_POLICIES), + ('contrast', 'darkModeContrast', None), + ('grayscale.all', 'darkModeGrayscale', _BOOLS), + + ('policy.page', 'darkModePagePolicy', _PAGE_POLICIES), + ('threshold.text', 'darkModeTextBrightnessThreshold', None), + ('threshold.background', 'darkModeBackgroundBrightnessThreshold', None), + ('grayscale.images', 'darkModeImageGrayscale', None), + ], {'enabled'}), + + Variant.qt_514: ([ + ('algorithm', 'darkMode', _ALGORITHMS), # new: kInvertLightnessLAB + + ('policy.images', 'darkModeImagePolicy', _IMAGE_POLICIES), + ('contrast', 'darkModeContrast', None), + ('grayscale.all', 'darkModeGrayscale', _BOOLS), + + ('policy.page', 'darkModePagePolicy', _PAGE_POLICIES), + ('threshold.text', 'darkModeTextBrightnessThreshold', None), + ('threshold.background', 'darkModeBackgroundBrightnessThreshold', None), + ('grayscale.images', 'darkModeImageGrayscale', None), + ], {'algorithm', 'policy.images'}), + + Variant.qt_511_to_513: ([ + ('algorithm', 'highContrastMode', _ALGORITHMS_BEFORE_QT_514), + + ('policy.images', 'highContrastImagePolicy', _IMAGE_POLICIES), # new: smart + ('contrast', 'highContrastContrast', None), + ('grayscale.all', 'highContrastGrayscale', _BOOLS), + ], {'algorithm', 'policy.images'}), + + Variant.qt_510: ([ + ('algorithm', 'highContrastMode', _ALGORITHMS_BEFORE_QT_514), + + ('policy.images', 'highContrastImagePolicy', _IMAGE_POLICIES_QT_510), + ('contrast', 'highContrastContrast', None), + ('grayscale.all', 'highContrastGrayscale', _BOOLS), + ], {'algorithm'}), +} + + +def _variant() -> Variant: + """Get the dark mode variant based on the underlying Qt version.""" + if PYQT_WEBENGINE_VERSION is not None: + # Available with Qt >= 5.13 + if PYQT_WEBENGINE_VERSION >= 0x050f02: + return Variant.qt_515_2 + elif PYQT_WEBENGINE_VERSION == 0x050f01: + return Variant.qt_515_1 + elif PYQT_WEBENGINE_VERSION == 0x050f00: + return Variant.qt_515_0 + elif PYQT_WEBENGINE_VERSION >= 0x050e00: + return Variant.qt_514 + elif PYQT_WEBENGINE_VERSION >= 0x050d00: + return Variant.qt_511_to_513 + raise utils.Unreachable(hex(PYQT_WEBENGINE_VERSION)) + + # If we don't have PYQT_WEBENGINE_VERSION, we'll need to assume based on the Qt + # version. + assert not qtutils.version_check( # type: ignore[unreachable] + '5.13', compiled=False) + + if qtutils.version_check('5.11', compiled=False): + return Variant.qt_511_to_513 + elif qtutils.version_check('5.10', compiled=False): + return Variant.qt_510 + + return Variant.unavailable + + +def settings() -> Iterator[Tuple[str, str]]: + """Get necessary blink settings to configure dark mode for QtWebEngine.""" + if not config.val.colors.webpage.darkmode.enabled: + return + + variant = _variant() + setting_defs, mandatory_settings = _DARK_MODE_DEFINITIONS[variant] + + for setting, key, mapping in setting_defs: + # To avoid blowing up the commandline length, we only pass modified + # settings to Chromium, as our defaults line up with Chromium's. + # However, we always pass enabled/algorithm to make sure dark mode gets + # actually turned on. + value = config.instance.get( + 'colors.webpage.darkmode.' + setting, + fallback=setting in mandatory_settings) + if isinstance(value, usertypes.Unset): + continue + + if (setting == 'policy.images' and value == 'smart' and + variant == Variant.qt_515_0): + # WORKAROUND for + # https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/304211 + log.init.warning("Ignoring colors.webpage.darkmode.policy.images = smart " + "because of Qt 5.15.0 bug") + continue + + if mapping is not None: + value = mapping[value] + + yield key, str(value) diff --git a/qutebrowser/browser/webengine/webengineelem.py b/qutebrowser/browser/webengine/webengineelem.py index e524b36d2..649339c50 100644 --- a/qutebrowser/browser/webengine/webengineelem.py +++ b/qutebrowser/browser/webengine/webengineelem.py @@ -19,7 +19,8 @@ """QtWebEngine specific part of the web element API.""" -import typing +from typing import ( + TYPE_CHECKING, Any, Callable, Dict, Iterator, Optional, Set, Tuple, Union) from PyQt5.QtCore import QRect, Qt, QPoint, QEventLoop from PyQt5.QtGui import QMouseEvent @@ -29,7 +30,7 @@ from PyQt5.QtWebEngineWidgets import QWebEngineSettings from qutebrowser.utils import log, javascript, urlutils, usertypes, utils from qutebrowser.browser import webelem -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from qutebrowser.browser.webengine import webenginetab @@ -37,11 +38,11 @@ class WebEngineElement(webelem.AbstractWebElement): """A web element for QtWebEngine, using JS under the hood.""" - def __init__(self, js_dict: typing.Dict[str, typing.Any], + def __init__(self, js_dict: Dict[str, Any], tab: 'webenginetab.WebEngineTab') -> None: super().__init__(tab) # Do some sanity checks on the data we get from JS - js_dict_types = { + js_dict_types: Dict[str, Union[type, Tuple[type, ...]]] = { 'id': int, 'text': str, 'value': (str, int, float), @@ -52,7 +53,7 @@ class WebEngineElement(webelem.AbstractWebElement): 'attributes': dict, 'is_content_editable': bool, 'caret_position': (int, type(None)), - } # type: typing.Dict[str, typing.Union[type, typing.Tuple[type,...]]] + } assert set(js_dict.keys()).issubset(js_dict_types.keys()) for name, typ in js_dict_types.items(): if name in js_dict and not isinstance(js_dict[name], typ): @@ -97,14 +98,14 @@ class WebEngineElement(webelem.AbstractWebElement): utils.unused(key) log.stub() - def __iter__(self) -> typing.Iterator[str]: + def __iter__(self) -> Iterator[str]: return iter(self._js_dict['attributes']) def __len__(self) -> int: return len(self._js_dict['attributes']) def _js_call(self, name: str, *args: webelem.JsValueType, - callback: typing.Callable[[typing.Any], None] = None) -> None: + callback: Callable[[Any], None] = None) -> None: """Wrapper to run stuff from webelem.js.""" if self._tab.is_deleted(): raise webelem.OrphanedError("Tab containing element vanished") @@ -118,7 +119,7 @@ class WebEngineElement(webelem.AbstractWebElement): log.stub() return QRect() - def classes(self) -> typing.Set[str]: + def classes(self) -> Set[str]: """Get a list of classes assigned to this element.""" return set(self._js_dict['class_name'].split()) @@ -150,7 +151,7 @@ class WebEngineElement(webelem.AbstractWebElement): composed: bool = False) -> None: self._js_call('dispatch_event', event, bubbles, cancelable, composed) - def caret_position(self) -> typing.Optional[int]: + def caret_position(self) -> Optional[int]: """Get the text caret position for the current element. If the element is not a text element, None is returned. @@ -256,7 +257,7 @@ class WebEngineElement(webelem.AbstractWebElement): QEventLoop.ExcludeSocketNotifiers | QEventLoop.ExcludeUserInputEvents) - def reset_setting(_arg: typing.Any) -> None: + def reset_setting(_arg: Any) -> None: """Set the JavascriptCanOpenWindows setting to its old value.""" assert view is not None try: diff --git a/qutebrowser/browser/webengine/webengineinspector.py b/qutebrowser/browser/webengine/webengineinspector.py index f84415c65..afe0d2b48 100644 --- a/qutebrowser/browser/webengine/webengineinspector.py +++ b/qutebrowser/browser/webengine/webengineinspector.py @@ -20,7 +20,6 @@ """Customized QWebInspector for QtWebEngine.""" import os -import typing import pathlib from PyQt5.QtCore import QUrl, QLibraryInfo @@ -118,7 +117,7 @@ class WebEngineInspector(inspector.AbstractWebInspector): pak = data_path / 'resources' / 'qtwebengine_devtools_resources.pak' if not pak.exists(): raise inspector.Error("QtWebEngine devtools resources not found, " - "please install the qt5-webengine-devtools " + "please install the qt5-qtwebengine-devtools " "Fedora package.") def inspect(self, page: QWebEnginePage) -> None: # type: ignore[override] diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index 336540ba0..d93f72f56 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -26,7 +26,7 @@ Module attributes: import os import operator -import typing +from typing import cast, Any, List, Optional, Tuple, Union from PyQt5.QtGui import QFont from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile, @@ -39,11 +39,11 @@ from qutebrowser.utils import (utils, standarddir, qtutils, message, log, urlmatch, usertypes) # The default QWebEngineProfile -default_profile = typing.cast(QWebEngineProfile, None) +default_profile = cast(QWebEngineProfile, None) # The QWebEngineProfile used for private (off-the-record) windows -private_profile = None # type: typing.Optional[QWebEngineProfile] +private_profile: Optional[QWebEngineProfile] = None # The global WebEngineSettings object -global_settings = typing.cast('WebEngineSettings', None) +global_settings = cast('WebEngineSettings', None) parsed_user_agent = None @@ -183,7 +183,7 @@ class WebEngineSettings(websettings.AbstractSettings): } def set_unknown_url_scheme_policy( - self, policy: typing.Union[str, usertypes.Unset]) -> bool: + self, policy: Union[str, usertypes.Unset]) -> bool: """Set the UnknownUrlSchemePolicy to use. Return: @@ -448,10 +448,10 @@ def _init_site_specific_quirks(): def _init_devtools_settings(): """Make sure the devtools always get images/JS permissions.""" - settings = [ + settings: List[Tuple[str, Any]] = [ ('content.javascript.enabled', True), ('content.images', True) - ] # type: typing.List[typing.Tuple[str, typing.Any]] + ] if qtutils.version_check('5.11'): settings.append(('content.cookies.accept', 'all')) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index a139f3d2f..f105bf2f4 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -23,13 +23,13 @@ import math import functools import re import html as html_utils -import typing +from typing import cast, Optional, Union from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QUrl, QTimer, QObject) from PyQt5.QtNetwork import QAuthenticator from PyQt5.QtWidgets import QApplication, QWidget -from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript +from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript, QWebEngineHistory from qutebrowser.config import configdata, config from qutebrowser.browser import (browsertab, eventfilter, shared, webelem, @@ -41,7 +41,6 @@ from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory, from qutebrowser.misc import miscwidgets, objects, quitter from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils, message, objreg, jinja, debug) -from qutebrowser.keyinput import modeman from qutebrowser.qt import sip @@ -356,12 +355,7 @@ class WebEngineCaret(browsertab.AbstractCaret): """QtWebEngine implementations related to moving the cursor/selection.""" - def __init__(self, - tab: 'WebEngineTab', - mode_manager: modeman.ModeManager, - parent: QWidget = None) -> None: - super().__init__(mode_manager, parent) - self._tab = tab + _tab: 'WebEngineTab' def _flags(self): """Get flags to pass to JS.""" @@ -674,6 +668,10 @@ class WebEngineHistoryPrivate(browsertab.AbstractHistoryPrivate): """History-related methods which are not part of the extension API.""" + def __init__(self, tab: 'WebEngineTab') -> None: + self._tab = tab + self._history = cast(QWebEngineHistory, None) + def serialize(self): if not qtutils.version_check('5.9', compiled=False): # WORKAROUND for @@ -782,9 +780,7 @@ class WebEngineElements(browsertab.AbstractElements): """QtWebEngine implemementations related to elements on the page.""" - def __init__(self, tab: 'WebEngineTab') -> None: - super().__init__() - self._tab = tab + _tab: 'WebEngineTab' def _js_cb_multiple(self, callback, error_cb, js_elems): """Handle found elements coming from JS and call the real callback. @@ -931,7 +927,7 @@ class _WebEnginePermissions(QObject): def __init__(self, tab, parent=None): super().__init__(parent) self._tab = tab - self._widget = typing.cast(QWidget, None) + self._widget = cast(QWidget, None) try: self._options.update({ @@ -1087,7 +1083,7 @@ class _WebEngineScripts(QObject): def __init__(self, tab, parent=None): super().__init__(parent) self._tab = tab - self._widget = typing.cast(QWidget, None) + self._widget = cast(QWidget, None) self._greasemonkey = greasemonkey.gm_manager def connect_signals(self): @@ -1380,7 +1376,7 @@ class WebEngineTab(browsertab.AbstractTab): self.backend = usertypes.Backend.QtWebEngine self._child_event_filter = None self._saved_zoom = None - self._reload_url = None # type: typing.Optional[QUrl] + self._reload_url: Optional[QUrl] = None self._scripts.init() def _set_widget(self, widget): @@ -1447,9 +1443,9 @@ class WebEngineTab(browsertab.AbstractTab): self._widget.page().toHtml(callback) def run_js_async(self, code, callback=None, *, world=None): - world_id_type = typing.Union[QWebEngineScript.ScriptWorldId, int] + world_id_type = Union[QWebEngineScript.ScriptWorldId, int] if world is None: - world_id = QWebEngineScript.ApplicationWorld # type: world_id_type + world_id: world_id_type = QWebEngineScript.ApplicationWorld elif isinstance(world, int): world_id = world if not 0 <= world_id <= qtutils.MAX_WORLD_ID: @@ -1545,9 +1541,7 @@ class WebEngineTab(browsertab.AbstractTab): authenticator.setPassword(answer.password) else: try: - sip.assign( # type: ignore[attr-defined] - authenticator, - QAuthenticator()) + sip.assign(authenticator, QAuthenticator()) except AttributeError: self._show_error_page(url, "Proxy authentication required") @@ -1568,8 +1562,7 @@ class WebEngineTab(browsertab.AbstractTab): if not netrc_success and answer is None: log.network.debug("Aborting auth") try: - sip.assign( # type: ignore[attr-defined] - authenticator, QAuthenticator()) + sip.assign(authenticator, QAuthenticator()) except AttributeError: # WORKAROUND for # https://www.riverbankcomputing.com/pipermail/pyqt/2016-December/038400.html @@ -1585,6 +1578,11 @@ class WebEngineTab(browsertab.AbstractTab): super()._on_load_started() self.data.netrc_used = False + @pyqtSlot('qint64') + def _on_renderer_process_pid_changed(self, pid): + log.webview.debug("Renderer process PID for tab {}: {}" + .format(self.tab_id, pid)) + @pyqtSlot(QWebEnginePage.RenderProcessTerminationStatus, int) def _on_render_process_terminated(self, status, exitcode): """Show an error when the renderer process terminated.""" @@ -1857,11 +1855,15 @@ class WebEngineTab(browsertab.AbstractTab): page.loadFinished.connect(self._restore_zoom) page.loadFinished.connect(self._on_load_finished) + try: + page.renderProcessPidChanged.connect(self._on_renderer_process_pid_changed) + except AttributeError: + # Added in Qt 5.15.0 + pass + self.before_load_started.connect(self._on_before_load_started) - self.shutting_down.connect( - self.abort_questions) # type: ignore[arg-type] - self.load_started.connect( - self.abort_questions) # type: ignore[arg-type] + self.shutting_down.connect(self.abort_questions) + self.load_started.connect(self.abort_questions) # pylint: disable=protected-access self.audio._connect_signals() diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py index 40ac12f11..934fe2dee 100644 --- a/qutebrowser/browser/webengine/webview.py +++ b/qutebrowser/browser/webengine/webview.py @@ -19,7 +19,7 @@ """The main browser widget for QtWebEngine.""" -import typing +from typing import Optional from PyQt5.QtCore import pyqtSignal, QUrl, PYQT_VERSION from PyQt5.QtGui import QPalette @@ -70,7 +70,7 @@ class WebEngineView(QWebEngineView): The above bug got introduced in Qt 5.11.0 and fixed in 5.12.0. """ - proxy = self.focusProxy() # type: typing.Optional[QWidget] + proxy: Optional[QWidget] = self.focusProxy() if 'lost-focusproxy' in objects.debug_flags: proxy = None diff --git a/qutebrowser/browser/webkit/cache.py b/qutebrowser/browser/webkit/cache.py index 7202ebd23..dd774ea5a 100644 --- a/qutebrowser/browser/webkit/cache.py +++ b/qutebrowser/browser/webkit/cache.py @@ -19,7 +19,7 @@ """HTTP network cache.""" -import typing +from typing import cast import os.path from PyQt5.QtNetwork import QNetworkDiskCache @@ -28,7 +28,7 @@ from qutebrowser.config import config from qutebrowser.utils import utils, qtutils, standarddir -diskcache = typing.cast('DiskCache', None) +diskcache = cast('DiskCache', None) class DiskCache(QNetworkDiskCache): diff --git a/qutebrowser/browser/webkit/cookies.py b/qutebrowser/browser/webkit/cookies.py index 4b2070f1d..9cc28cf69 100644 --- a/qutebrowser/browser/webkit/cookies.py +++ b/qutebrowser/browser/webkit/cookies.py @@ -19,7 +19,7 @@ """Handling of HTTP cookies.""" -import typing +from typing import Sequence from PyQt5.QtNetwork import QNetworkCookie, QNetworkCookieJar from PyQt5.QtCore import pyqtSignal, QDateTime @@ -93,7 +93,7 @@ class CookieJar(RAMCookieJar): def parse_cookies(self): """Parse cookies from lineparser and store them.""" - cookies = [] # type: typing.Sequence[QNetworkCookie] + cookies: Sequence[QNetworkCookie] = [] for line in self._lineparser: line_cookies = QNetworkCookie.parseCookies(line) cookies += line_cookies # type: ignore[operator] diff --git a/qutebrowser/browser/webkit/mhtml.py b/qutebrowser/browser/webkit/mhtml.py index a045e10f2..6e2442575 100644 --- a/qutebrowser/browser/webkit/mhtml.py +++ b/qutebrowser/browser/webkit/mhtml.py @@ -33,7 +33,7 @@ import email.encoders import email.mime.multipart import email.message import quopri -import typing +from typing import MutableMapping, Set, Tuple import attr from PyQt5.QtCore import QUrl @@ -90,10 +90,7 @@ def _get_css_imports_cssutils(data, inline=False): """ try: import cssutils - except (ImportError, re.error): - # Catching re.error because cssutils in earlier releases (<= 1.0) is - # broken on Python 3.5 - # See https://bitbucket.org/cthedot/cssutils/issues/52 + except ImportError: return None # We don't care about invalid CSS data, this will only litter the log @@ -189,7 +186,7 @@ class MHTMLWriter: self.root_content = root_content self.content_location = content_location self.content_type = content_type - self._files = {} # type: typing.MutableMapping[QUrl, _File] + self._files: MutableMapping[QUrl, _File] = {} def add_file(self, location, content, content_type=None, transfer_encoding=E_QUOPRI): @@ -244,8 +241,7 @@ class MHTMLWriter: return msg -_PendingDownloadType = typing.Set[ - typing.Tuple[QUrl, downloads.AbstractDownloadItem]] +_PendingDownloadType = Set[Tuple[QUrl, downloads.AbstractDownloadItem]] class _Downloader: @@ -268,7 +264,7 @@ class _Downloader: self.target = target self.writer = None self.loaded_urls = {tab.url()} - self.pending_downloads = set() # type: _PendingDownloadType + self.pending_downloads: _PendingDownloadType = set() self._finished_file = False self._used = False diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index 1def7ad44..14c47b1f9 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -21,7 +21,7 @@ import collections import html -import typing +from typing import TYPE_CHECKING, Dict, MutableMapping, Optional, Sequence import attr from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QCoreApplication, QUrl, @@ -40,12 +40,12 @@ from qutebrowser.browser.webkit.network import (webkitqutescheme, networkreply, filescheme) from qutebrowser.misc import objects -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from qutebrowser.mainwindow import prompt HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%' -_proxy_auth_cache = {} # type: typing.Dict[ProxyId, prompt.AuthInfo] +_proxy_auth_cache: Dict['ProxyId', 'prompt.AuthInfo'] = {} @attr.s(frozen=True) @@ -123,8 +123,7 @@ def init(): QSslSocket.setDefaultCiphers(good_ciphers) -_SavedErrorsType = typing.MutableMapping[urlutils.HostTupleType, - typing.Sequence[QSslError]] +_SavedErrorsType = MutableMapping[urlutils.HostTupleType, Sequence[QSslError]] class NetworkManager(QNetworkAccessManager): @@ -173,10 +172,8 @@ class NetworkManager(QNetworkAccessManager): self._set_cache() self.sslErrors.connect( # type: ignore[attr-defined] self.on_ssl_errors) - self._rejected_ssl_errors = collections.defaultdict( - list) # type: _SavedErrorsType - self._accepted_ssl_errors = collections.defaultdict( - list) # type: _SavedErrorsType + self._rejected_ssl_errors: _SavedErrorsType = collections.defaultdict(list) + self._accepted_ssl_errors: _SavedErrorsType = collections.defaultdict(list) self.authenticationRequired.connect( # type: ignore[attr-defined] self.on_authentication_required) self.proxyAuthenticationRequired.connect( # type: ignore[attr-defined] @@ -241,8 +238,8 @@ class NetworkManager(QNetworkAccessManager): log.network.debug("Certificate errors: {!r}".format( ' / '.join(str(err) for err in errors))) try: - host_tpl = urlutils.host_tuple( - reply.url()) # type: typing.Optional[urlutils.HostTupleType] + host_tpl: Optional[urlutils.HostTupleType] = urlutils.host_tuple( + reply.url()) except ValueError: host_tpl = None is_accepted = False diff --git a/qutebrowser/browser/webkit/tabhistory.py b/qutebrowser/browser/webkit/tabhistory.py index f293edacd..f0673036c 100644 --- a/qutebrowser/browser/webkit/tabhistory.py +++ b/qutebrowser/browser/webkit/tabhistory.py @@ -19,7 +19,7 @@ """Utilities related to QWebHistory.""" -import typing +from typing import Any, List, Mapping from PyQt5.QtCore import QByteArray, QDataStream, QIODevice, QUrl @@ -81,7 +81,7 @@ def serialize(items): """ data = QByteArray() stream = QDataStream(data, QIODevice.ReadWrite) - user_data = [] # type: typing.List[typing.Mapping[str, typing.Any]] + user_data: List[Mapping[str, Any]] = [] current_idx = None diff --git a/qutebrowser/browser/webkit/webkitelem.py b/qutebrowser/browser/webkit/webkitelem.py index e73d6a9e8..2f3562b7f 100644 --- a/qutebrowser/browser/webkit/webkitelem.py +++ b/qutebrowser/browser/webkit/webkitelem.py @@ -19,7 +19,7 @@ """QtWebKit specific part of the web element API.""" -import typing +from typing import cast, TYPE_CHECKING, Iterator, List, Optional, Set from PyQt5.QtCore import QRect, Qt from PyQt5.QtWebKit import QWebElement, QWebSettings @@ -29,7 +29,7 @@ from qutebrowser.config import config from qutebrowser.utils import log, utils, javascript, usertypes from qutebrowser.browser import webelem -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from qutebrowser.browser.webkit import webkittab @@ -42,6 +42,8 @@ class WebKitElement(webelem.AbstractWebElement): """A wrapper around a QWebElement.""" + _tab: 'webkittab.WebKitTab' + def __init__(self, elem: QWebElement, tab: 'webkittab.WebKitTab') -> None: super().__init__(tab) if isinstance(elem, self.__class__): @@ -80,7 +82,7 @@ class WebKitElement(webelem.AbstractWebElement): self._check_vanished() return self._elem.hasAttribute(key) - def __iter__(self) -> typing.Iterator[str]: + def __iter__(self) -> Iterator[str]: self._check_vanished() yield from self._elem.attributeNames() @@ -101,7 +103,7 @@ class WebKitElement(webelem.AbstractWebElement): self._check_vanished() return self._elem.geometry() - def classes(self) -> typing.Set[str]: + def classes(self) -> Set[str]: self._check_vanished() return set(self._elem.classes()) @@ -174,21 +176,16 @@ class WebKitElement(webelem.AbstractWebElement): this.dispatchEvent(event); """.format(javascript.to_js(text))) - def _parent(self) -> typing.Optional['WebKitElement']: + def _parent(self) -> Optional['WebKitElement']: """Get the parent element of this element.""" self._check_vanished() - elem = typing.cast(typing.Optional[QWebElement], - self._elem.parent()) + elem = cast(Optional[QWebElement], self._elem.parent()) if elem is None or elem.isNull(): return None - if typing.TYPE_CHECKING: - # pylint: disable=used-before-assignment - assert isinstance(self._tab, webkittab.WebKitTab) - return WebKitElement(elem, tab=self._tab) - def _rect_on_view_js(self) -> typing.Optional[QRect]: + def _rect_on_view_js(self) -> Optional[QRect]: """Javascript implementation for rect_on_view.""" # FIXME:qtwebengine maybe we can reuse this? rects = self._elem.evaluateJavaScript("this.getClientRects()") @@ -217,29 +214,32 @@ class WebKitElement(webelem.AbstractWebElement): height *= zoom rect = QRect(int(rect["left"]), int(rect["top"]), int(width), int(height)) - frame = self._elem.webFrame() + + frame = cast(Optional[QWebFrame], self._elem.webFrame()) while frame is not None: # Translate to parent frames' position (scroll position # is taken care of inside getClientRects) rect.translate(frame.geometry().topLeft()) frame = frame.parentFrame() + return rect return None - def _rect_on_view_python(self, - elem_geometry: typing.Optional[QRect]) -> QRect: + def _rect_on_view_python(self, elem_geometry: Optional[QRect]) -> QRect: """Python implementation for rect_on_view.""" if elem_geometry is None: geometry = self._elem.geometry() else: geometry = elem_geometry - frame = self._elem.webFrame() rect = QRect(geometry) + + frame = cast(Optional[QWebFrame], self._elem.webFrame()) while frame is not None: rect.translate(frame.geometry().topLeft()) rect.translate(frame.scrollPosition() * -1) - frame = frame.parentFrame() + frame = cast(Optional[QWebFrame], frame.parentFrame()) + return rect def rect_on_view(self, *, elem_geometry: QRect = None, @@ -332,7 +332,7 @@ class WebKitElement(webelem.AbstractWebElement): return all([visible_on_screen, visible_in_frame]) def remove_blank_target(self) -> None: - elem = self # type: typing.Optional[WebKitElement] + elem: Optional[WebKitElement] = self for _ in range(5): if elem is None: break @@ -377,7 +377,7 @@ class WebKitElement(webelem.AbstractWebElement): super()._click_fake_event(click_target) -def get_child_frames(startframe: QWebFrame) -> typing.List[QWebFrame]: +def get_child_frames(startframe: QWebFrame) -> List[QWebFrame]: """Get all children recursively of a given QWebFrame. Loosely based on http://blog.nextgenetics.net/?e=64 @@ -391,7 +391,7 @@ def get_child_frames(startframe: QWebFrame) -> typing.List[QWebFrame]: results = [] frames = [startframe] while frames: - new_frames = [] # type: typing.List[QWebFrame] + new_frames: List[QWebFrame] = [] for frame in frames: results.append(frame) new_frames += frame.childFrames() diff --git a/qutebrowser/browser/webkit/webkitsettings.py b/qutebrowser/browser/webkit/webkitsettings.py index 0db1a738d..ec30267d4 100644 --- a/qutebrowser/browser/webkit/webkitsettings.py +++ b/qutebrowser/browser/webkit/webkitsettings.py @@ -24,7 +24,7 @@ Module attributes: constants. """ -import typing +from typing import cast import os.path from PyQt5.QtCore import QUrl @@ -39,7 +39,7 @@ from qutebrowser.browser import shared # The global WebKitSettings object -global_settings = typing.cast('WebKitSettings', None) +global_settings = cast('WebKitSettings', None) parsed_user_agent = None diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index cad9badee..1008169a0 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -22,12 +22,13 @@ import re import functools import xml.etree.ElementTree +from typing import cast, Iterable from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QPoint, QTimer, QSizeF, QSize from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QWidget from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame -from PyQt5.QtWebKit import QWebSettings +from PyQt5.QtWebKit import QWebSettings, QWebHistory, QWebElement from PyQt5.QtPrintSupport import QPrinter from qutebrowser.browser import browsertab, shared @@ -200,8 +201,7 @@ class WebKitCaret(browsertab.AbstractCaret): tab: 'WebKitTab', mode_manager: modeman.ModeManager, parent: QWidget = None) -> None: - super().__init__(mode_manager, parent) - self._tab = tab + super().__init__(tab, mode_manager, parent) self._selection_state = browsertab.SelectionState.none @pyqtSlot(usertypes.KeyMode) @@ -622,6 +622,10 @@ class WebKitHistoryPrivate(browsertab.AbstractHistoryPrivate): """History-related methods which are not part of the extension API.""" + def __init__(self, tab: 'WebKitTab') -> None: + self._tab = tab + self._history = cast(QWebHistory, None) + def serialize(self): return qtutils.serialize(self._history) @@ -636,6 +640,7 @@ class WebKitHistoryPrivate(browsertab.AbstractHistoryPrivate): qtutils.deserialize_stream(stream, self._history) for i, data in enumerate(user_data): self._history.itemAt(i).setUserData(data) + cur_data = self._history.currentItem().userData() if cur_data is not None: if 'zoom' in cur_data: @@ -687,9 +692,7 @@ class WebKitElements(browsertab.AbstractElements): """QtWebKit implemementations related to elements on the page.""" - def __init__(self, tab: 'WebKitTab') -> None: - super().__init__() - self._tab = tab + _tab: 'WebKitTab' def find_css(self, selector, callback, error_cb, *, only_visible=False): utils.unused(error_cb) @@ -700,7 +703,8 @@ class WebKitElements(browsertab.AbstractElements): elems = [] frames = webkitelem.get_child_frames(mainframe) for f in frames: - for elem in f.findAllElements(selector): + frame_elems = cast(Iterable[QWebElement], f.findAllElements(selector)) + for elem in frame_elems: elems.append(webkitelem.WebKitElement(elem, tab=self._tab)) if only_visible: diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index 9055bff24..956b9be9d 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -21,7 +21,7 @@ import html import functools -import typing +from typing import cast from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QUrl, QPoint from PyQt5.QtGui import QDesktopServices @@ -353,11 +353,11 @@ class BrowserPage(QWebPage): self.setFeaturePermission, frame, feature, QWebPage.PermissionDeniedByUser) - url = frame.url().adjusted(typing.cast(QUrl.FormattingOptions, - QUrl.RemoveUserInfo | - QUrl.RemovePath | - QUrl.RemoveQuery | - QUrl.RemoveFragment)) + url = frame.url().adjusted(cast(QUrl.FormattingOptions, + QUrl.RemoveUserInfo | + QUrl.RemovePath | + QUrl.RemoveQuery | + QUrl.RemoveFragment)) question = shared.feature_permission( url=url, option=options[feature], msg=messages[feature], diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 2672fcd68..61b44d555 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -23,6 +23,7 @@ import inspect import collections import traceback import typing +from typing import Any, MutableMapping, MutableSequence, Tuple, Union import attr @@ -116,13 +117,11 @@ class Command: self.parser.add_argument('-h', '--help', action=argparser.HelpAction, default=argparser.SUPPRESS, nargs=0, help=argparser.SUPPRESS) - self.opt_args = collections.OrderedDict( - ) # type: typing.MutableMapping[str, typing.Tuple[str, str]] + self.opt_args: MutableMapping[str, Tuple[str, str]] = collections.OrderedDict() self.namespace = None self._count = None - self.pos_args = [ - ] # type: typing.MutableSequence[typing.Tuple[str, str]] - self.flags_with_args = [] # type: typing.MutableSequence[str] + self.pos_args: MutableSequence[Tuple[str, str]] = [] + self.flags_with_args: MutableSequence[str] = [] self._has_vararg = False # This is checked by future @cmdutils.argument calls so they fail @@ -406,22 +405,19 @@ class Command: raise TypeError("{}: Legacy tuple type annotation!".format( self.name)) - if hasattr(typing, 'UnionMeta'): - # Python 3.5.2 - # pylint: disable=no-member,useless-suppression - is_union = isinstance( - typ, typing.UnionMeta) # type: ignore[attr-defined] - else: - is_union = getattr(typ, '__origin__', None) is typing.Union + try: + origin = typing.get_origin(typ) # type: ignore[attr-defined] + except AttributeError: + # typing.get_origin was added in Python 3.8 + origin = getattr(typ, '__origin__', None) - if is_union: - # this is... slightly evil, I know + if origin is Union: try: - types = list(typ.__args__) + types = list(typing.get_args(typ)) # type: ignore[attr-defined] except AttributeError: - # Python 3.5.2 - types = list(typ.__union_params__) - # pylint: enable=no-member,useless-suppression + # typing.get_args was added in Python 3.8 + types = list(typ.__args__) + if param.default is not inspect.Parameter.empty: types.append(type(param.default)) choices = self.get_arg_info(param).choices @@ -497,8 +493,8 @@ class Command: Return: An (args, kwargs) tuple. """ - args = [] # type: typing.Any - kwargs = {} # type: typing.MutableMapping[str, typing.Any] + args: Any = [] + kwargs: MutableMapping[str, Any] = {} signature = inspect.signature(self.handler) for i, param in enumerate(signature.parameters.values()): diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index 76ae1d64f..c195a8be9 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -21,8 +21,8 @@ import traceback import re -import typing import contextlib +from typing import TYPE_CHECKING, Callable, Dict, Iterator, Mapping, MutableMapping import attr from PyQt5.QtCore import pyqtSlot, QUrl, QObject @@ -34,9 +34,9 @@ from qutebrowser.utils import message, objreg, qtutils, usertypes, utils from qutebrowser.misc import split, objects from qutebrowser.keyinput import macros, modeman -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from qutebrowser.mainwindow import tabbedbrowser -_ReplacementFunction = typing.Callable[['tabbedbrowser.TabbedBrowser'], str] +_ReplacementFunction = Callable[['tabbedbrowser.TabbedBrowser'], str] last_command = {} @@ -64,9 +64,9 @@ def _url(tabbed_browser): raise cmdutils.CommandError(msg) -def _init_variable_replacements() -> typing.Mapping[str, _ReplacementFunction]: +def _init_variable_replacements() -> Mapping[str, _ReplacementFunction]: """Return a dict from variable replacements to fns processing them.""" - replacements = { + replacements: Dict[str, _ReplacementFunction] = { 'url': lambda tb: _url(tb).toString( QUrl.FullyEncoded | QUrl.RemovePassword), 'url:pretty': lambda tb: _url(tb).toString( @@ -88,7 +88,7 @@ def _init_variable_replacements() -> typing.Mapping[str, _ReplacementFunction]: 'title': lambda tb: tb.widget.page_title(tb.widget.currentIndex()), 'clipboard': lambda _: utils.get_clipboard(), 'primary': lambda _: utils.get_clipboard(selection=True), - } # type: typing.Dict[str, _ReplacementFunction] + } for key in list(replacements): modified_key = '{' + key + '}' @@ -108,7 +108,7 @@ def replace_variables(win_id, arglist): """Utility function to replace variables like {url} in a list of args.""" tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) - values = {} # type: typing.MutableMapping[str, str] + values: MutableMapping[str, str] = {} args = [] def repl_cb(matchobj): @@ -332,7 +332,7 @@ class CommandRunner(AbstractCommandRunner): self._win_id = win_id @contextlib.contextmanager - def _handle_error(self, safely: bool) -> typing.Iterator[None]: + def _handle_error(self, safely: bool) -> Iterator[None]: """Show exceptions as errors if safely=True is given.""" try: yield diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py index 485161600..ce25d7d28 100644 --- a/qutebrowser/commands/userscripts.py +++ b/qutebrowser/commands/userscripts.py @@ -22,7 +22,7 @@ import os import os.path import tempfile -import typing +from typing import cast, Any, MutableMapping, Tuple from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QSocketNotifier @@ -60,7 +60,7 @@ class _QtFIFOReader(QObject): fd = os.open(filepath, os.O_RDWR | os.O_NONBLOCK) # pylint: enable=no-member,useless-suppression self._fifo = os.fdopen(fd, 'r') - self._notifier = QSocketNotifier(typing.cast(sip.voidptr, fd), + self._notifier = QSocketNotifier(cast(sip.voidptr, fd), QSocketNotifier.Read, self) self._notifier.activated.connect( # type: ignore[attr-defined] self.read_line) @@ -117,10 +117,10 @@ class _BaseUserscriptRunner(QObject): self._cleaned_up = False self._filepath = None self._proc = None - self._env = {} # type: typing.MutableMapping[str, str] + self._env: MutableMapping[str, str] = {} self._text_stored = False self._html_stored = False - self._args = () # type: typing.Tuple[typing.Any, ...] + self._args: Tuple[Any, ...] = () self._kwargs = {} def store_text(self, text): @@ -267,7 +267,7 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner): return self._reader = _QtFIFOReader(self._filepath) - self._reader.got_line.connect(self.got_cmd) # type: ignore[arg-type] + self._reader.got_line.connect(self.got_cmd) @pyqtSlot() def on_proc_finished(self): @@ -426,7 +426,7 @@ def run_async(tab, cmd, *args, win_id, env, verbose=False, commandrunner = runners.CommandRunner(win_id, parent=tb) if utils.is_posix: - runner = _POSIXUserscriptRunner(tb) # type: _BaseUserscriptRunner + runner: _BaseUserscriptRunner = _POSIXUserscriptRunner(tb) elif utils.is_windows: # pragma: no cover runner = _WindowsUserscriptRunner(tb) else: # pragma: no cover diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index 50d5bdf62..4f51ecd4b 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. """ -import typing +from typing import TYPE_CHECKING, Optional from PyQt5.QtWidgets import QTreeView, QSizePolicy, QStyleFactory, QWidget from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, QSize @@ -32,7 +32,7 @@ from qutebrowser.config import config, stylesheet from qutebrowser.completion import completiondelegate from qutebrowser.utils import utils, usertypes, debug, log from qutebrowser.api import cmdutils -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from qutebrowser.mainwindow.statusbar import command @@ -115,7 +115,7 @@ class CompletionView(QTreeView): win_id: int, parent: QWidget = None) -> None: super().__init__(parent) - self.pattern = None # type: typing.Optional[str] + self.pattern: Optional[str] = None self._win_id = win_id self._cmd = cmd self._active = False diff --git a/qutebrowser/completion/models/completionmodel.py b/qutebrowser/completion/models/completionmodel.py index 1bd2a808f..7d65d4439 100644 --- a/qutebrowser/completion/models/completionmodel.py +++ b/qutebrowser/completion/models/completionmodel.py @@ -19,7 +19,7 @@ """A model that proxies access to one or more completion categories.""" -import typing +from typing import MutableSequence from PyQt5.QtCore import Qt, QModelIndex, QAbstractItemModel @@ -43,8 +43,7 @@ class CompletionModel(QAbstractItemModel): def __init__(self, *, column_widths=(30, 70, 0), parent=None): super().__init__(parent) self.column_widths = column_widths - self._categories = [ - ] # type: typing.MutableSequence[QAbstractItemModel] + self._categories: MutableSequence[QAbstractItemModel] = [] def _cat_from_idx(self, index): """Return the category pointed to by the given index. diff --git a/qutebrowser/completion/models/histcategory.py b/qutebrowser/completion/models/histcategory.py index 464caa19e..e7ccd3505 100644 --- a/qutebrowser/completion/models/histcategory.py +++ b/qutebrowser/completion/models/histcategory.py @@ -19,7 +19,7 @@ """A completion category that queries the SQL history store.""" -import typing +from typing import Optional from PyQt5.QtSql import QSqlQueryModel from PyQt5.QtWidgets import QWidget @@ -40,12 +40,12 @@ class HistoryCategory(QSqlQueryModel): """Create a new History completion category.""" super().__init__(parent=parent) self.name = "History" - self._query = None # type: typing.Optional[sql.Query] + self._query: Optional[sql.Query] = None # advertise that this model filters by URL and title self.columns_to_filter = [0, 1] self.delete_func = delete_func - self._empty_prefix = None # type: typing.Optional[str] + self._empty_prefix: Optional[str] = None def _atime_expr(self): """If max_items is set, return an expression to limit the query.""" diff --git a/qutebrowser/completion/models/listcategory.py b/qutebrowser/completion/models/listcategory.py index 6995071ed..f0cc21da0 100644 --- a/qutebrowser/completion/models/listcategory.py +++ b/qutebrowser/completion/models/listcategory.py @@ -20,7 +20,7 @@ """Completion category that uses a list of tuples as a data source.""" import re -import typing +from typing import Iterable, Tuple from PyQt5.QtCore import Qt, QSortFilterProxyModel, QRegExp from PyQt5.QtGui import QStandardItem, QStandardItemModel @@ -36,7 +36,7 @@ class ListCategory(QSortFilterProxyModel): def __init__(self, name: str, - items: typing.Iterable[typing.Tuple[str, ...]], + items: Iterable[Tuple[str, ...]], sort: bool = True, delete_func: util.DeleteFuncType = None, parent: QWidget = None): diff --git a/qutebrowser/completion/models/miscmodels.py b/qutebrowser/completion/models/miscmodels.py index 9cf2d5fd6..925f95bbb 100644 --- a/qutebrowser/completion/models/miscmodels.py +++ b/qutebrowser/completion/models/miscmodels.py @@ -20,7 +20,7 @@ """Functions that return miscellaneous completion models.""" import datetime -import typing +from typing import List, Sequence, Tuple from qutebrowser.config import config, configdata from qutebrowser.utils import objreg, log, utils @@ -53,7 +53,7 @@ def helptopic(*, info): def quickmark(*, info=None): """A CompletionModel filled with all quickmarks.""" - def delete(data: typing.Sequence[str]) -> None: + def delete(data: Sequence[str]) -> None: """Delete a quickmark from the completion menu.""" name = data[0] quickmark_manager = objreg.get('quickmark-manager') @@ -71,7 +71,7 @@ def quickmark(*, info=None): def bookmark(*, info=None): """A CompletionModel filled with all bookmarks.""" - def delete(data: typing.Sequence[str]) -> None: + def delete(data: Sequence[str]) -> None: """Delete a bookmark from the completion menu.""" urlstr = data[0] log.completion.debug('Deleting bookmark {}'.format(urlstr)) @@ -121,7 +121,7 @@ def _buffer(*, win_id_filter=lambda _win_id: True, add_win_id=True): tabs_are_windows = config.val.tabs.tabs_are_windows # list storing all single-tabbed windows when tabs_are_windows - windows = [] # type: typing.List[typing.Tuple[str, str, str]] + windows: List[Tuple[str, str, str]] = [] for win_id in objreg.window_registry: if not win_id_filter(win_id): @@ -131,7 +131,7 @@ def _buffer(*, win_id_filter=lambda _win_id: True, add_win_id=True): window=win_id) if tabbed_browser.is_shutting_down: continue - tabs = [] # type: typing.List[typing.Tuple[str, str, str]] + tabs: List[Tuple[str, str, str]] = [] for idx in range(tabbed_browser.widget.count()): tab = tabbed_browser.widget.widget(idx) tab_str = ("{}/{}".format(win_id, idx + 1) if add_win_id diff --git a/qutebrowser/completion/models/urlmodel.py b/qutebrowser/completion/models/urlmodel.py index ff83a598a..ba0857d4c 100644 --- a/qutebrowser/completion/models/urlmodel.py +++ b/qutebrowser/completion/models/urlmodel.py @@ -19,10 +19,9 @@ """Function to return the url completion model for the `open` command.""" -import typing +from typing import Dict, Sequence -if typing.TYPE_CHECKING: - from PyQt5.QtCore import QAbstractItemModel +from PyQt5.QtCore import QAbstractItemModel from qutebrowser.completion.models import (completionmodel, listcategory, histcategory) @@ -41,14 +40,14 @@ def _delete_history(data): history.web_history.delete_url(urlstr) -def _delete_bookmark(data: typing.Sequence[str]) -> None: +def _delete_bookmark(data: Sequence[str]) -> None: urlstr = data[_URLCOL] log.completion.debug('Deleting bookmark {}'.format(urlstr)) bookmark_manager = objreg.get('bookmark-manager') bookmark_manager.delete(urlstr) -def _delete_quickmark(data: typing.Sequence[str]) -> None: +def _delete_quickmark(data: Sequence[str]) -> None: name = data[_TEXTCOL] quickmark_manager = objreg.get('quickmark-manager') log.completion.debug('Deleting quickmark {}'.format(name)) @@ -77,7 +76,7 @@ def url(*, info): if k != 'DEFAULT'] # pylint: enable=bad-config-option categories = config.val.completion.open_categories - models = {} # type: typing.Dict[str, QAbstractItemModel] + models: Dict[str, QAbstractItemModel] = {} if searchengines and 'searchengines' in categories: models['searchengines'] = listcategory.ListCategory( diff --git a/qutebrowser/completion/models/util.py b/qutebrowser/completion/models/util.py index a0dda334a..7f4f6d19d 100644 --- a/qutebrowser/completion/models/util.py +++ b/qutebrowser/completion/models/util.py @@ -19,13 +19,13 @@ """Utility functions for completion models.""" -import typing +from typing import Callable, Sequence from qutebrowser.utils import usertypes from qutebrowser.misc import objects -DeleteFuncType = typing.Callable[[typing.Sequence[str]], None] +DeleteFuncType = Callable[[Sequence[str]], None] def get_cmd_completions(info, include_hidden, include_aliases, prefix=''): diff --git a/qutebrowser/components/adblock.py b/qutebrowser/components/adblock.py index e7e2c3573..d68a3feb0 100644 --- a/qutebrowser/components/adblock.py +++ b/qutebrowser/components/adblock.py @@ -23,8 +23,8 @@ import os.path import posixpath import zipfile import logging -import typing import pathlib +from typing import cast, IO, List, Set from PyQt5.QtCore import QUrl @@ -57,7 +57,7 @@ def _guess_zip_filename(zf: zipfile.ZipFile) -> str: raise FileNotFoundError("No hosts file found in zip") -def get_fileobj(byte_io: typing.IO[bytes]) -> typing.IO[bytes]: +def get_fileobj(byte_io: IO[bytes]) -> IO[bytes]: """Get a usable file object to read the hosts file from.""" byte_io.seek(0) # rewind downloaded file if zipfile.is_zipfile(byte_io): @@ -101,8 +101,8 @@ class HostBlocker: ) -> None: self.enabled = _should_be_used() self._has_basedir = has_basedir - self._blocked_hosts = set() # type: typing.Set[str] - self._config_blocked_hosts = set() # type: typing.Set[str] + self._blocked_hosts: Set[str] = set() + self._config_blocked_hosts: Set[str] = set() self._local_hosts_file = str(data_dir / "blocked-hosts") self.update_files() @@ -137,7 +137,7 @@ class HostBlocker: ) info.block() - def _read_hosts_line(self, raw_line: bytes) -> typing.Set[str]: + def _read_hosts_line(self, raw_line: bytes) -> Set[str]: """Read hosts from the given line. Args: @@ -173,7 +173,7 @@ class HostBlocker: return filtered_hosts - def _read_hosts_file(self, filename: str, target: typing.Set[str]) -> bool: + def _read_hosts_file(self, filename: str, target: Set[str]) -> bool: """Read hosts from the given filename. Args: @@ -225,7 +225,7 @@ class HostBlocker: dl.initiate() return dl - def _merge_file(self, byte_io: typing.IO[bytes]) -> None: + def _merge_file(self, byte_io: IO[bytes]) -> None: """Read and merge host files. Args: diff --git a/qutebrowser/components/misccommands.py b/qutebrowser/components/misccommands.py index ff9a21070..19ad126c1 100644 --- a/qutebrowser/components/misccommands.py +++ b/qutebrowser/components/misccommands.py @@ -23,7 +23,7 @@ import os import signal import functools import logging -import typing +from typing import Optional try: import hunter @@ -41,7 +41,7 @@ from qutebrowser.completion.models import miscmodels @cmdutils.register(name='reload') @cmdutils.argument('tab', value=cmdutils.Value.count_tab) -def reloadpage(tab: typing.Optional[apitypes.Tab], +def reloadpage(tab: Optional[apitypes.Tab], force: bool = False) -> None: """Reload the current/[count]th tab. @@ -55,7 +55,7 @@ def reloadpage(tab: typing.Optional[apitypes.Tab], @cmdutils.register() @cmdutils.argument('tab', value=cmdutils.Value.count_tab) -def stop(tab: typing.Optional[apitypes.Tab]) -> None: +def stop(tab: Optional[apitypes.Tab]) -> None: """Stop loading in the current/[count]th tab. Args: @@ -97,7 +97,7 @@ def _print_pdf(tab: apitypes.Tab, filename: str) -> None: @cmdutils.register(name='print') @cmdutils.argument('tab', value=cmdutils.Value.count_tab) @cmdutils.argument('pdf', flag='f', metavar='file') -def printpage(tab: typing.Optional[apitypes.Tab], +def printpage(tab: Optional[apitypes.Tab], preview: bool = False, *, pdf: str = None) -> None: """Print the current/[count]th tab. @@ -163,7 +163,7 @@ def insert_text(tab: apitypes.Tab, text: str) -> None: Args: text: The text to insert. """ - def _insert_text_cb(elem: typing.Optional[apitypes.WebElement]) -> None: + def _insert_text_cb(elem: Optional[apitypes.WebElement]) -> None: if elem is None: message.error("No element focused!") return @@ -195,7 +195,7 @@ def click_element(tab: apitypes.Tab, filter_: str, value: str, *, target: How to open the clicked element (normal/tab/tab-bg/window). force_event: Force generating a fake click event. """ - def single_cb(elem: typing.Optional[apitypes.WebElement]) -> None: + def single_cb(elem: Optional[apitypes.WebElement]) -> None: """Click a single element.""" if elem is None: message.error("No element found with id {}!".format(value)) @@ -236,7 +236,7 @@ def debug_webaction(tab: apitypes.Tab, action: str, count: int = 1) -> None: @cmdutils.register() @cmdutils.argument('tab', value=cmdutils.Value.count_tab) -def tab_mute(tab: typing.Optional[apitypes.Tab]) -> None: +def tab_mute(tab: Optional[apitypes.Tab]) -> None: """Mute/Unmute the current/[count]th tab. Args: diff --git a/qutebrowser/components/readlinecommands.py b/qutebrowser/components/readlinecommands.py index 076bb9055..ea8f12edf 100644 --- a/qutebrowser/components/readlinecommands.py +++ b/qutebrowser/components/readlinecommands.py @@ -19,7 +19,7 @@ """Bridge to provide readline-like shortcuts for QLineEdits.""" -import typing +from typing import Iterable, Optional, MutableMapping from PyQt5.QtWidgets import QApplication, QLineEdit @@ -35,9 +35,9 @@ class _ReadlineBridge: """ def __init__(self) -> None: - self._deleted = {} # type: typing.MutableMapping[QLineEdit, str] + self._deleted: MutableMapping[QLineEdit, str] = {} - def _widget(self) -> typing.Optional[QLineEdit]: + def _widget(self) -> Optional[QLineEdit]: """Get the currently active QLineEdit.""" w = QApplication.instance().focusWidget() if isinstance(w, QLineEdit): @@ -86,7 +86,7 @@ class _ReadlineBridge: def kill_line(self) -> None: self._dispatch('end', mark=True, delete=True) - def _rubout(self, delim: typing.Iterable[str]) -> None: + def _rubout(self, delim: Iterable[str]) -> None: """Delete backwards using the characters in delim as boundaries.""" widget = self._widget() if widget is None: diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 007b44404..8611e46ab 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -23,7 +23,7 @@ import copy import contextlib import functools import typing -from typing import Any +from typing import Any, Tuple, MutableMapping from PyQt5.QtCore import pyqtSignal, QObject, QUrl @@ -33,7 +33,6 @@ from qutebrowser.misc import objects from qutebrowser.keyinput import keyutils if typing.TYPE_CHECKING: - from typing import Tuple, MutableMapping from qutebrowser.config import configcache, configfiles from qutebrowser.misc import savemanager @@ -283,6 +282,7 @@ class Config(QObject): self._init_values() self.yaml_loaded = False self.config_py_loaded = False + self.warn_autoconfig = True def _init_values(self) -> None: """Populate the self._values dict.""" diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index c43847fe9..460f3bc41 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -575,12 +575,20 @@ content.headers.user_agent: - qutebrowser_version completions: # See https://techblog.willshouse.com/2012/01/03/most-common-user-agents/ - - - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, - like Gecko) Chrome/83.0.4103.61 Safari/537.36" - - Chrome 83 Win10 + # + # To update the following list of user agents, run the script + # 'ua_fetch.py' + # Vim-protip: Place your cursor below this comment and run + # :r!python scripts/dev/ua_fetch.py - - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like - Gecko) Chrome/83.0.4103.61 Safari/537.36" - - Chrome 83 Linux + Gecko) Chrome/86.0.4240.75 Safari/537.36" + - Chrome 86 Linux + - - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, + like Gecko) Chrome/86.0.4240.75 Safari/537.36" + - Chrome 86 Win10 + - - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 + (KHTML, like Gecko) Chrome/86.0.4240.80 Safari/537.36" + - Chrome 86 macOS supports_pattern: true desc: | User agent to send. @@ -1863,14 +1871,14 @@ tabs.title.format: * `{perc}`: Percentage as a string like `[10%]`. * `{perc_raw}`: Raw percentage, e.g. `10`. * `{current_title}`: Title of the current web page. - * `{title_sep}`: The string ` - ` if a title is set, empty otherwise. + * `{title_sep}`: The string `" - "` if a title is set, empty otherwise. * `{index}`: Index of this tab. * `{aligned_index}`: Index of this tab padded with spaces to have the same width. * `{id}`: Internal tab ID of this tab. * `{scroll_pos}`: Page scroll position. * `{host}`: Host of the current web page. - * `{backend}`: Either ''webkit'' or ''webengine'' + * `{backend}`: Either `webkit` or `webengine` * `{private}`: Indicates when private mode is enabled. * `{current_url}`: URL of the current web page. * `{protocol}`: Protocol (http/https/...) of the current web page. @@ -2740,18 +2748,21 @@ colors.webpage.darkmode.enabled: above. restart: true backend: - QtWebEngine: Qt 5.14 + QtWebEngine: Qt 5.10 QtWebKit: false colors.webpage.darkmode.algorithm: default: lightness-cielab - desc: "Which algorithm to use for modifying how colors are rendered with - darkmode." + desc: >- + Which algorithm to use for modifying how colors are rendered with darkmode. + + The `lightness-cielab` value was added with QtWebEngine 5.14 and is treated + like `lightness-hsl` with older QtWebEngine versions. type: name: String valid_values: - lightness-cielab: Modify colors by converting them to CIELAB color - space and inverting the L value. + space and inverting the L value. Not available with Qt < 5.14. - lightness-hsl: Modify colors by converting them to the HSL color space and inverting the lightness (i.e. the "L" in HSL). - brightness-rgb: Modify colors by subtracting each of r, g, and b from @@ -2761,7 +2772,7 @@ colors.webpage.darkmode.algorithm: # Chromium's automated tests restart: true backend: - QtWebEngine: Qt 5.14 + QtWebEngine: Qt 5.10 QtWebKit: false colors.webpage.darkmode.contrast: @@ -2777,27 +2788,29 @@ colors.webpage.darkmode.contrast: `lightness-hsl` or `brightness-rgb`. restart: true backend: - QtWebEngine: Qt 5.14 + QtWebEngine: Qt 5.10 QtWebKit: false colors.webpage.darkmode.policy.images: - default: never + default: smart type: name: String valid_values: - always: Apply dark mode filter to all images. - never: Never apply dark mode filter to any images. - - smart: Apply dark mode based on image content. + - smart: "Apply dark mode based on image content. Not available with Qt + 5.10 / 5.15.0." desc: >- Which images to apply dark mode to. - WARNING: On Qt 5.15.0, this setting can cause frequent renderer process + With QtWebEngine 5.15.0, this setting can cause frequent renderer process crashes due to a https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/304211[bug - in Qt]. + in Qt]. With QtWebEngine 5.10, this is not available at all. In those + cases, the 'smart' setting is ignored and treated like 'never'. restart: true backend: - QtWebEngine: Qt 5.14 + QtWebEngine: Qt 5.10 QtWebKit: false colors.webpage.darkmode.policy.page: @@ -2860,7 +2873,7 @@ colors.webpage.darkmode.grayscale.all: `lightness-hsl` or `brightness-rgb`. restart: true backend: - QtWebEngine: Qt 5.14 + QtWebEngine: Qt 5.10 QtWebKit: false colors.webpage.darkmode.grayscale.images: diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index a1b0e75bd..ae05a2861 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -566,12 +566,21 @@ class ConfigAPI: def finalize(self) -> None: """Do work which needs to be done after reading config.py.""" + if self._config.warn_autoconfig: + desc = configexc.ConfigErrorDesc( + "autoconfig loading not specified", + ("Your config.py should call either `config.load_autoconfig()`" + " (to load settings configured via the GUI) or " + "`config.load_autoconfig(False)` (to not do so)")) + self.errors.append(desc) self._config.update_mutables() - def load_autoconfig(self) -> None: + def load_autoconfig(self, load_config: bool = True) -> None: """Load the autoconfig.yml file which is used for :set/:bind/etc.""" - with self._handle_error('reading', 'autoconfig.yml'): - read_autoconfig() + self._config.warn_autoconfig = False + if load_config: + with self._handle_error('reading', 'autoconfig.yml'): + read_autoconfig() def get(self, name: str, pattern: str = None) -> typing.Any: """Get a setting value from the config, optionally with a pattern.""" @@ -689,12 +698,12 @@ class ConfigPyWriter: "still loaded.") yield self._line("# Remove it to not load settings done via the " "GUI.") - yield self._line("config.load_autoconfig()") + yield self._line("config.load_autoconfig(True)") yield '' else: - yield self._line("# Uncomment this to still load settings " + yield self._line("# Change the argument to True to still load settings " "configured via autoconfig.yml") - yield self._line("# config.load_autoconfig()") + yield self._line("config.load_autoconfig(False)") yield '' def _gen_options(self) -> typing.Iterator[str]: diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 75148947e..81c47590d 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -47,7 +47,6 @@ import html import codecs import os.path import itertools -import warnings import functools import operator import json @@ -1319,30 +1318,19 @@ class Regex(BaseType): def _compile_regex(self, pattern: str) -> typing.Pattern[str]: """Check if the given regex is valid. - This is more complicated than it could be since there's a warning on - invalid escapes with newer Python versions, and we want to catch that - case and treat it as invalid. + Some semi-invalid regexes can also raise warnings - we also treat them as + invalid. """ - with warnings.catch_warnings(record=True) as recorded_warnings: - warnings.simplefilter('always') - try: + try: + with log.py_warning_filter('error', category=FutureWarning): compiled = re.compile(pattern, self.flags) - except re.error as e: - raise configexc.ValidationError( - pattern, "must be a valid regex - " + str(e)) - except RuntimeError: # pragma: no cover - raise configexc.ValidationError( - pattern, "must be a valid regex - recursion depth " - "exceeded") - - assert recorded_warnings is not None - - for w in recorded_warnings: - if (issubclass(w.category, DeprecationWarning) and - str(w.message).startswith('bad escape')): - raise configexc.ValidationError( - pattern, "must be a valid regex - " + str(w.message)) - warnings.warn(w.message) + except (re.error, FutureWarning) as e: + raise configexc.ValidationError( + pattern, "must be a valid regex - " + str(e)) + except RuntimeError: # pragma: no cover + raise configexc.ValidationError( + pattern, "must be a valid regex - recursion depth " + "exceeded") return compiled diff --git a/qutebrowser/config/qtargs.py b/qutebrowser/config/qtargs.py index 0c517a14c..868f4d669 100644 --- a/qutebrowser/config/qtargs.py +++ b/qutebrowser/config/qtargs.py @@ -61,90 +61,6 @@ def qt_args(namespace: argparse.Namespace) -> typing.List[str]: return argv -def _darkmode_settings() -> typing.Iterator[typing.Tuple[str, str]]: - """Get necessary blink settings to configure dark mode for QtWebEngine.""" - if not config.val.colors.webpage.darkmode.enabled: - return - - # Mapping from a colors.webpage.darkmode.algorithm setting value to - # Chromium's DarkModeInversionAlgorithm enum values. - algorithms = { - # 0: kOff (not exposed) - # 1: kSimpleInvertForTesting (not exposed) - 'brightness-rgb': 2, # kInvertBrightness - 'lightness-hsl': 3, # kInvertLightness - 'lightness-cielab': 4, # kInvertLightnessLAB - } - - # Mapping from a colors.webpage.darkmode.policy.images setting value to - # Chromium's DarkModeImagePolicy enum values. - image_policies = { - 'always': 0, # kFilterAll - 'never': 1, # kFilterNone - 'smart': 2, # kFilterSmart - } - - # Mapping from a colors.webpage.darkmode.policy.page setting value to - # Chromium's DarkModePagePolicy enum values. - page_policies = { - 'always': 0, # kFilterAll - 'smart': 1, # kFilterByBackground - } - - bools = { - True: 'true', - False: 'false', - } - - _setting_description_type = typing.Tuple[ - str, # qutebrowser option name - str, # darkmode setting name - # Mapping from the config value to a string (or something convertable - # to a string) which gets passed to Chromium. - typing.Optional[typing.Mapping[typing.Any, typing.Union[str, int]]], - ] - if qtutils.version_check('5.15', compiled=False): - settings = [ - ('enabled', 'Enabled', bools), - ('algorithm', 'InversionAlgorithm', algorithms), - ] # type: typing.List[_setting_description_type] - mandatory_setting = 'enabled' - else: - settings = [ - ('algorithm', '', algorithms), - ] - mandatory_setting = 'algorithm' - - settings += [ - ('contrast', 'Contrast', None), - ('policy.images', 'ImagePolicy', image_policies), - ('policy.page', 'PagePolicy', page_policies), - ('threshold.text', 'TextBrightnessThreshold', None), - ('threshold.background', 'BackgroundBrightnessThreshold', None), - ('grayscale.all', 'Grayscale', bools), - ('grayscale.images', 'ImageGrayscale', None), - ] - - for setting, key, mapping in settings: - # To avoid blowing up the commandline length, we only pass modified - # settings to Chromium, as our defaults line up with Chromium's. - # However, we always pass enabled/algorithm to make sure dark mode gets - # actually turned on. - value = config.instance.get( - 'colors.webpage.darkmode.' + setting, - fallback=setting == mandatory_setting) - if isinstance(value, usertypes.Unset): - continue - - if mapping is not None: - value = mapping[value] - - # FIXME: This is "forceDarkMode" starting with Chromium 83 - prefix = 'darkMode' - - yield prefix + key, str(value) - - def _qtwebengine_enabled_features( feature_flags: typing.Sequence[str], ) -> typing.Iterator[str]: @@ -230,7 +146,11 @@ def _qtwebengine_args( yield '--enable-logging' yield '--v=1' - blink_settings = list(_darkmode_settings()) + if 'wait-renderer-process' in namespace.debug_flags: + yield '--renderer-startup-dialog' + + from qutebrowser.browser.webengine import darkmode + blink_settings = list(darkmode.settings()) if blink_settings: yield '--blink-settings=' + ','.join('{}={}'.format(k, v) for k, v in blink_settings) @@ -239,6 +159,10 @@ def _qtwebengine_args( if enabled_features: yield '--enable-features=' + ','.join(enabled_features) + yield from _qtwebengine_settings_args() + + +def _qtwebengine_settings_args() -> typing.Iterator[str]: settings = { 'qt.force_software_rendering': { 'software-opengl': None, diff --git a/qutebrowser/extensions/interceptors.py b/qutebrowser/extensions/interceptors.py index 6c5756016..fddeaabc9 100644 --- a/qutebrowser/extensions/interceptors.py +++ b/qutebrowser/extensions/interceptors.py @@ -19,8 +19,8 @@ """Infrastructure for intercepting requests.""" -import typing import enum +from typing import Callable, List, Optional import attr @@ -76,15 +76,15 @@ class Request: """A request which can be intercepted/blocked.""" #: The URL of the page being shown. - first_party_url = attr.ib() # type: typing.Optional[QUrl] + first_party_url: Optional[QUrl] = attr.ib() #: The URL of the file being requested. - request_url = attr.ib() # type: QUrl + request_url: QUrl = attr.ib() - is_blocked = attr.ib(False) # type: bool + is_blocked: bool = attr.ib(False) #: The resource type of the request. None if not supported on this backend. - resource_type = attr.ib(None) # type: typing.Optional[ResourceType] + resource_type: Optional[ResourceType] = attr.ib(None) def block(self) -> None: """Block this request.""" @@ -107,10 +107,10 @@ class Request: #: Type annotation for an interceptor function. -InterceptorType = typing.Callable[[Request], None] +InterceptorType = Callable[[Request], None] -_interceptors = [] # type: typing.List[InterceptorType] +_interceptors: List[InterceptorType] = [] def register(interceptor: InterceptorType) -> None: diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py index 41b9c63fd..b6d86f517 100644 --- a/qutebrowser/extensions/loader.py +++ b/qutebrowser/extensions/loader.py @@ -19,12 +19,13 @@ """Loader for qutebrowser extensions.""" -import importlib.abc import pkgutil import types -import typing import sys import pathlib +import importlib +import argparse +from typing import Callable, Iterator, List, Optional, Set, Tuple import attr @@ -35,9 +36,6 @@ from qutebrowser.config import config from qutebrowser.utils import log, standarddir from qutebrowser.misc import objects -if typing.TYPE_CHECKING: - import argparse - # ModuleInfo objects for all loaded plugins _module_infos = [] @@ -48,9 +46,9 @@ class InitContext: """Context an extension gets in its init hook.""" - data_dir = attr.ib() # type: pathlib.Path - config_dir = attr.ib() # type: pathlib.Path - args = attr.ib() # type: argparse.Namespace + data_dir: pathlib.Path = attr.ib() + config_dir: pathlib.Path = attr.ib() + args: argparse.Namespace = attr.ib() @attr.s @@ -61,13 +59,11 @@ class ModuleInfo: This gets used by qutebrowser.api.hook. """ - _ConfigChangedHooksType = typing.List[typing.Tuple[typing.Optional[str], - typing.Callable]] + _ConfigChangedHooksType = List[Tuple[Optional[str], Callable]] - skip_hooks = attr.ib(False) # type: bool - init_hook = attr.ib(None) # type: typing.Optional[typing.Callable] - config_changed_hooks = attr.ib( - attr.Factory(list)) # type: _ConfigChangedHooksType + skip_hooks: bool = attr.ib(False) + init_hook: Optional[Callable] = attr.ib(None) + config_changed_hooks: _ConfigChangedHooksType = attr.ib(attr.Factory(list)) @attr.s @@ -75,7 +71,7 @@ class ExtensionInfo: """Information about a qutebrowser extension.""" - name = attr.ib() # type: str + name: str = attr.ib() def add_module_info(module: types.ModuleType) -> ModuleInfo: @@ -92,7 +88,7 @@ def load_components(*, skip_hooks: bool = False) -> None: _load_component(info, skip_hooks=skip_hooks) -def walk_components() -> typing.Iterator[ExtensionInfo]: +def walk_components() -> Iterator[ExtensionInfo]: """Yield ExtensionInfo objects for all modules.""" if hasattr(sys, 'frozen'): yield from _walk_pyinstaller() @@ -104,7 +100,7 @@ def _on_walk_error(name: str) -> None: raise ImportError("Failed to import {}".format(name)) -def _walk_normal() -> typing.Iterator[ExtensionInfo]: +def _walk_normal() -> Iterator[ExtensionInfo]: """Walk extensions when not using PyInstaller.""" for _finder, name, ispkg in pkgutil.walk_packages( # Only packages have a __path__ attribute, @@ -117,7 +113,7 @@ def _walk_normal() -> typing.Iterator[ExtensionInfo]: yield ExtensionInfo(name=name) -def _walk_pyinstaller() -> typing.Iterator[ExtensionInfo]: +def _walk_pyinstaller() -> Iterator[ExtensionInfo]: """Walk extensions when using PyInstaller. See https://github.com/pyinstaller/pyinstaller/issues/1905 @@ -125,7 +121,7 @@ def _walk_pyinstaller() -> typing.Iterator[ExtensionInfo]: Inspired by: https://github.com/webcomics/dosage/blob/master/dosagelib/loader.py """ - toc = set() # type: typing.Set[str] + toc: Set[str] = set() for importer in pkgutil.iter_importers('qutebrowser'): if hasattr(importer, 'toc'): toc |= importer.toc diff --git a/qutebrowser/html/warning-sessions.html b/qutebrowser/html/warning-sessions.html index 6f447483f..82bc02aab 100644 --- a/qutebrowser/html/warning-sessions.html +++ b/qutebrowser/html/warning-sessions.html @@ -5,11 +5,11 @@ <span class="note">Note this warning will only appear once. Use <span class="mono">:open qute://warning/sessions</span> to show it again at a later time.</span> -<p>You're using qutebrowser with Qt 5.15.</p> +<p>You're using qutebrowser with Qt 5.15. While this is the recommended Qt version to use (due to QtWebEngine security updates), qutebrowser only provides partial support for session files.</p> <p>Since Qt doesn't provide an API to load the history of a tab, qutebrowser relies on a reverse-engineered binary serialization format to load tab history from session files. With Qt 5.15, unfortunately that format changed (due to the underlying Chromium upgrade), in a way which makes it impossible for qutebrowser to load tab history from existing session data.</p> -<p>At the time of writing (September 2020), a new session format which stores part of the needed binary data in saved sessions is <a href="https://github.com/qutebrowser/qutebrowser/issues/5359">in development</a> and is expected to be released with qutebrowser v1.15.0.</p> +<p>At the time of writing (October 2020), a new session format which stores part of the needed binary data in saved sessions is <a href="https://github.com/qutebrowser/qutebrowser/issues/5359">in development</a> and is expected to be released with qutebrowser v2.0.0 (planned to be released at the end of the year or early 2021).</p> <p>As a stop-gap measure:</p> diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index dea85aede..23b77cba1 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -21,7 +21,7 @@ import string import types -import typing +from typing import Mapping, MutableMapping, Optional, Sequence import attr from PyQt5.QtCore import pyqtSignal, QObject, Qt @@ -37,9 +37,9 @@ class MatchResult: """The result of matching a keybinding.""" - match_type = attr.ib() # type: QKeySequence.SequenceMatch - command = attr.ib() # type: typing.Optional[str] - sequence = attr.ib() # type: keyutils.KeySequence + match_type: QKeySequence.SequenceMatch = attr.ib() + command: Optional[str] = attr.ib() + sequence: keyutils.KeySequence = attr.ib() def __attrs_post_init__(self) -> None: if self.match_type == QKeySequence.ExactMatch: @@ -75,9 +75,8 @@ class BindingTrie: __slots__ = 'children', 'command' def __init__(self) -> None: - self.children = { - } # type: typing.MutableMapping[keyutils.KeyInfo, BindingTrie] - self.command = None # type: typing.Optional[str] + self.children: MutableMapping[keyutils.KeyInfo, BindingTrie] = {} + self.command: Optional[str] = None def __setitem__(self, sequence: keyutils.KeySequence, command: str) -> None: @@ -99,8 +98,7 @@ class BindingTrie: def __str__(self) -> str: return '\n'.join(self.string_lines(blank=True)) - def string_lines(self, indent: int = 0, - blank: bool = False) -> typing.Sequence[str]: + def string_lines(self, indent: int = 0, blank: bool = False) -> Sequence[str]: """Get a list of strings for a pretty-printed version of this trie.""" lines = [] if self.command is not None: @@ -114,7 +112,7 @@ class BindingTrie: return lines - def update(self, mapping: typing.Mapping) -> None: + def update(self, mapping: Mapping) -> None: """Add data from the given mapping to the trie.""" for key in mapping: self[key] = mapping[key] diff --git a/qutebrowser/keyinput/eventfilter.py b/qutebrowser/keyinput/eventfilter.py index 6ef0dd201..d77c8702d 100644 --- a/qutebrowser/keyinput/eventfilter.py +++ b/qutebrowser/keyinput/eventfilter.py @@ -19,7 +19,7 @@ """Global Qt event filter which dispatches key events.""" -import typing +from typing import cast from PyQt5.QtCore import pyqtSlot, QObject, QEvent from PyQt5.QtGui import QKeyEvent, QWindow @@ -102,7 +102,7 @@ class EventFilter(QObject): handler = self._handlers[typ] try: - return handler(typing.cast(QKeyEvent, event)) + return handler(cast(QKeyEvent, event)) except: # If there is an exception in here and we leave the eventfilter # activated, we'll get an infinite loop and a stack overflow. diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index b95f4a55d..aa5457c6d 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -32,7 +32,7 @@ handle what we actually think we do. """ import itertools -import typing +from typing import cast, overload, Iterable, Iterator, List, Mapping, Optional, Union import attr from PyQt5.QtCore import Qt, QEvent @@ -53,10 +53,10 @@ _MODIFIER_MAP = { _NIL_KEY = Qt.Key(0) -_ModifierType = typing.Union[Qt.KeyboardModifier, Qt.KeyboardModifiers] +_ModifierType = Union[Qt.KeyboardModifier, Qt.KeyboardModifiers] -def _build_special_names() -> typing.Mapping[Qt.Key, str]: +def _build_special_names() -> Mapping[Qt.Key, str]: """Build _SPECIAL_NAMES dict from the special_names_str mapping below. The reason we don't do this directly is that certain Qt versions don't have @@ -231,8 +231,7 @@ def _remap_unicode(key: Qt.Key, text: str) -> Qt.Key: return key -def _check_valid_utf8(s: str, - data: typing.Union[Qt.Key, _ModifierType]) -> None: +def _check_valid_utf8(s: str, data: Union[Qt.Key, _ModifierType]) -> None: """Make sure the given string is valid UTF-8. Makes sure there are no chars where Qt did fall back to weird UTF-16 @@ -288,7 +287,7 @@ class KeyParseError(Exception): """Raised by _parse_single_key/parse_keystring on parse errors.""" - def __init__(self, keystr: typing.Optional[str], error: str) -> None: + def __init__(self, keystr: Optional[str], error: str) -> None: if keystr is None: msg = "Could not parse keystring: {}".format(error) else: @@ -296,7 +295,7 @@ class KeyParseError(Exception): super().__init__(msg) -def _parse_keystring(keystr: str) -> typing.Iterator[str]: +def _parse_keystring(keystr: str) -> Iterator[str]: key = '' special = False for c in keystr: @@ -363,8 +362,8 @@ class KeyInfo: modifiers: A Qt::KeyboardModifiers enum value. """ - key = attr.ib() # type: Qt.Key - modifiers = attr.ib() # type: _ModifierType + key: Qt.Key = attr.ib() + modifiers: _ModifierType = attr.ib() @classmethod def from_event(cls, e: QKeyEvent) -> 'KeyInfo': @@ -377,7 +376,7 @@ class KeyInfo: modifiers = e.modifiers() _assert_plain_key(key) _assert_plain_modifier(modifiers) - return cls(key, typing.cast(Qt.KeyboardModifier, modifiers)) + return cls(key, cast(Qt.KeyboardModifier, modifiers)) def __hash__(self) -> int: """Convert KeyInfo to int before hashing. @@ -473,7 +472,7 @@ class KeySequence: _MAX_LEN = 4 def __init__(self, *keys: int) -> None: - self._sequences = [] # type: typing.List[QKeySequence] + self._sequences: List[QKeySequence] = [] for sub in utils.chunk(keys, self._MAX_LEN): args = [self._convert_key(key) for key in sub] sequence = QKeySequence(*args) @@ -493,7 +492,7 @@ class KeySequence: parts.append(str(info)) return ''.join(parts) - def __iter__(self) -> typing.Iterator[KeyInfo]: + def __iter__(self) -> Iterator[KeyInfo]: """Iterate over KeyInfo objects.""" for key_and_modifiers in self._iter_keys(): key = Qt.Key(int(key_and_modifiers) & ~Qt.KeyboardModifierMask) @@ -535,17 +534,15 @@ class KeySequence: def __bool__(self) -> bool: return bool(self._sequences) - @typing.overload + @overload def __getitem__(self, item: int) -> KeyInfo: ... - @typing.overload + @overload def __getitem__(self, item: slice) -> 'KeySequence': ... - def __getitem__( - self, item: typing.Union[int, slice] - ) -> typing.Union[KeyInfo, 'KeySequence']: + def __getitem__(self, item: Union[int, slice]) -> Union[KeyInfo, 'KeySequence']: if isinstance(item, slice): keys = list(self._iter_keys()) return self.__class__(*keys[item]) @@ -553,9 +550,8 @@ class KeySequence: infos = list(self) return infos[item] - def _iter_keys(self) -> typing.Iterator[int]: - sequences = typing.cast(typing.Iterable[typing.Iterable[int]], - self._sequences) + def _iter_keys(self) -> Iterator[int]: + sequences = cast(Iterable[Iterable[int]], self._sequences) return itertools.chain.from_iterable(sequences) def _validate(self, keystr: str = None) -> None: @@ -664,7 +660,7 @@ class KeySequence: def with_mappings( self, - mappings: typing.Mapping['KeySequence', 'KeySequence'] + mappings: Mapping['KeySequence', 'KeySequence'] ) -> 'KeySequence': """Get a new KeySequence with the given mappings applied.""" keys = [] diff --git a/qutebrowser/keyinput/macros.py b/qutebrowser/keyinput/macros.py index 6e48e5a3f..ee8883070 100644 --- a/qutebrowser/keyinput/macros.py +++ b/qutebrowser/keyinput/macros.py @@ -20,7 +20,7 @@ """Keyboard macro system.""" -import typing +from typing import cast, Dict, List, Optional, Tuple from qutebrowser.commands import runners from qutebrowser.api import cmdutils @@ -28,9 +28,9 @@ from qutebrowser.keyinput import modeman from qutebrowser.utils import message, objreg, usertypes -_CommandType = typing.Tuple[str, int] # command, type +_CommandType = Tuple[str, int] # command, type -macro_recorder = typing.cast('MacroRecorder', None) +macro_recorder = cast('MacroRecorder', None) class MacroRecorder: @@ -47,10 +47,10 @@ class MacroRecorder: """ def __init__(self) -> None: - self._macros = {} # type: typing.Dict[str, typing.List[_CommandType]] - self._recording_macro = None # type: typing.Optional[str] - self._macro_count = {} # type: typing.Dict[int, int] - self._last_register = None # type: typing.Optional[str] + self._macros: Dict[str, List[_CommandType]] = {} + self._recording_macro: Optional[str] = None + self._macro_count: Dict[int, int] = {} + self._last_register: Optional[str] = None @cmdutils.register(instance='macro-recorder', name='record-macro') @cmdutils.argument('win_id', value=cmdutils.Value.win_id) diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index 4febf98a8..27e4be34e 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. -"""Mode manager singleton which handles the current keyboard mode.""" +"""Mode manager (per window) which handles the current keyboard mode.""" import functools from typing import Mapping, Callable, MutableMapping, Union, Set, cast @@ -78,15 +78,17 @@ class UnavailableError(Exception): def init(win_id: int, parent: QObject) -> 'ModeManager': """Initialize the mode manager and the keyparsers for the given win_id.""" + commandrunner = runners.CommandRunner(win_id) + modeman = ModeManager(win_id, parent) objreg.register('mode-manager', modeman, scope='window', window=win_id) - commandrunner = runners.CommandRunner(win_id) - hintmanager = hints.HintManager(win_id, parent=parent) objreg.register('hintmanager', hintmanager, scope='window', window=win_id, command_only=True) + modeman.hintmanager = hintmanager + keyparsers = { usertypes.KeyMode.normal: modeparsers.NormalKeyParser( @@ -227,6 +229,7 @@ class ModeManager(QObject): Attributes: mode: The mode we're currently in. + hintmanager: The HintManager associated with this window. _win_id: The window ID of this ModeManager _prev_mode: Mode before a prompt popped up parsers: A dictionary of modes and their keyparsers. @@ -260,6 +263,8 @@ class ModeManager(QObject): self._prev_mode = usertypes.KeyMode.normal self.mode = usertypes.KeyMode.normal self._releaseevents_to_pass = set() # type: Set[KeyEvent] + # Set after __init__ + self.hintmanager = cast(hints.HintManager, None) def __repr__(self) -> str: return utils.get_repr(self, mode=self.mode) diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index a55639898..48f3594a5 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -23,9 +23,9 @@ Module attributes: STARTCHARS: Possible chars for starting a commandline input. """ -import typing import traceback import enum +from typing import TYPE_CHECKING, Sequence from PyQt5.QtCore import pyqtSlot, Qt, QObject from PyQt5.QtGui import QKeySequence, QKeyEvent @@ -35,12 +35,20 @@ from qutebrowser.commands import cmdexc from qutebrowser.config import config from qutebrowser.keyinput import basekeyparser, keyutils, macros from qutebrowser.utils import usertypes, log, message, objreg, utils -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from qutebrowser.commands import runners STARTCHARS = ":/?" -LastPress = enum.Enum('LastPress', ['none', 'filtertext', 'keystring']) + + +class LastPress(enum.Enum): + + """Whether the last keypress filtered a text or was part of a keystring.""" + + none = enum.auto() + filtertext = enum.auto() + keystring = enum.auto() class CommandKeyParser(basekeyparser.BaseKeyParser): @@ -224,7 +232,7 @@ class HintKeyParser(basekeyparser.BaseKeyParser): return match - def update_bindings(self, strings: typing.Sequence[str], + def update_bindings(self, strings: Sequence[str], preserve_filter: bool = False) -> None: """Update bindings when the hint strings changed. diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index faccdc73c..b8228545a 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -23,9 +23,9 @@ import binascii import base64 import itertools import functools -import typing +from typing import List, MutableSequence, Optional, Tuple, cast -from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QRect, QPoint, QTimer, Qt, +from PyQt5.QtCore import (pyqtBoundSignal, pyqtSlot, QRect, QPoint, QTimer, Qt, QCoreApplication, QEventLoop, QByteArray) from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QSizePolicy from PyQt5.QtGui import QPalette @@ -105,23 +105,20 @@ def raise_window(window, alert=True): def get_target_window(): """Get the target window for new tabs, or None if none exist.""" + getters = { + 'last-focused': objreg.last_focused_window, + 'first-opened': objreg.first_opened_window, + 'last-opened': objreg.last_opened_window, + 'last-visible': objreg.last_visible_window, + } + getter = getters[config.val.new_instance_open_target_window] try: - win_mode = config.val.new_instance_open_target_window - if win_mode == 'last-focused': - return objreg.last_focused_window() - elif win_mode == 'first-opened': - return objreg.window_by_index(0) - elif win_mode == 'last-opened': - return objreg.window_by_index(-1) - elif win_mode == 'last-visible': - return objreg.last_visible_window() - else: - raise ValueError("Invalid win_mode {}".format(win_mode)) + return getter() except objreg.NoWindow: return None -_OverlayInfoType = typing.Tuple[QWidget, pyqtSignal, bool, str] +_OverlayInfoType = Tuple[QWidget, pyqtBoundSignal, bool, str] class MainWindow(QWidget): @@ -190,8 +187,8 @@ class MainWindow(QWidget): def __init__(self, *, private: bool, - geometry: typing.Optional[QByteArray] = None, - parent: typing.Optional[QWidget] = None) -> None: + geometry: Optional[QByteArray] = None, + parent: Optional[QWidget] = None) -> None: """Create a new main window. Args: @@ -208,7 +205,7 @@ class MainWindow(QWidget): self.setAttribute(Qt.WA_DeleteOnClose) self.setAttribute(Qt.WA_TranslucentBackground) self.palette().setColor(QPalette.Window, Qt.transparent) - self._overlays = [] # type: typing.MutableSequence[_OverlayInfoType] + self._overlays: MutableSequence[_OverlayInfoType] = [] self.win_id = next(win_id_gen) self.registry = objreg.ObjectRegistry() objreg.window_registry[self.win_id] = self @@ -218,10 +215,6 @@ class MainWindow(QWidget): objreg.register('tab-registry', tab_registry, scope='window', window=self.win_id) - message_bridge = message.MessageBridge(self) - objreg.register('message-bridge', message_bridge, scope='window', - window=self.win_id) - self.setWindowTitle('qutebrowser') self._vbox = QVBoxLayout(self) self._vbox.setContentsMargins(0, 0, 0, 0) @@ -233,9 +226,8 @@ class MainWindow(QWidget): self.is_private = config.val.content.private_browsing or private - self.tabbed_browser = tabbedbrowser.TabbedBrowser( - win_id=self.win_id, private=self.is_private, parent=self - ) # type: tabbedbrowser.TabbedBrowser + self.tabbed_browser: tabbedbrowser.TabbedBrowser = tabbedbrowser.TabbedBrowser( + win_id=self.win_id, private=self.is_private, parent=self) objreg.register('tabbed-browser', self.tabbed_browser, scope='window', window=self.win_id) self._init_command_dispatcher() @@ -420,7 +412,7 @@ class MainWindow(QWidget): self._vbox.removeWidget(self.tabbed_browser.widget) self._vbox.removeWidget(self._downloadview) self._vbox.removeWidget(self.status) - widgets = [self.tabbed_browser.widget] # type: typing.List[QWidget] + widgets: List[QWidget] = [self.tabbed_browser.widget] downloads_position = config.val.downloads.position if downloads_position == 'top': @@ -484,13 +476,8 @@ class MainWindow(QWidget): """Set some sensible default geometry.""" self.setGeometry(QRect(50, 50, 800, 600)) - def _get_object(self, name): - """Get an object for this window in the object registry.""" - return objreg.get(name, scope='window', window=self.win_id) - def _connect_signals(self): """Connect all mainwindow signals.""" - message_bridge = self._get_object('message-bridge') mode_manager = modeman.instance(self.win_id) # misc @@ -498,23 +485,19 @@ class MainWindow(QWidget): mode_manager.entered.connect(hints.on_mode_entered) # status bar + mode_manager.hintmanager.set_text.connect(self.status.set_text) mode_manager.entered.connect(self.status.on_mode_entered) mode_manager.left.connect(self.status.on_mode_left) mode_manager.left.connect(self.status.cmd.on_mode_left) - mode_manager.left.connect( - message.global_bridge.mode_left) # type: ignore[arg-type] + mode_manager.left.connect(message.global_bridge.mode_left) # commands mode_manager.keystring_updated.connect( self.status.keystring.on_keystring_updated) - self.status.cmd.got_cmd[str].connect( # type: ignore[index] - self._commandrunner.run_safely) - self.status.cmd.got_cmd[str, int].connect( # type: ignore[index] - self._commandrunner.run_safely) - self.status.cmd.returnPressed.connect( - self.tabbed_browser.on_cmd_return_pressed) - self.status.cmd.got_search.connect( - self._command_dispatcher.search) + self.status.cmd.got_cmd[str].connect(self._commandrunner.run_safely) + self.status.cmd.got_cmd[str, int].connect(self._commandrunner.run_safely) + self.status.cmd.returnPressed.connect(self.tabbed_browser.on_cmd_return_pressed) + self.status.cmd.got_search.connect(self._command_dispatcher.search) # key hint popup mode_manager.keystring_updated.connect(self._keyhint.update_keyhint) @@ -526,10 +509,6 @@ class MainWindow(QWidget): message.global_bridge.clear_messages.connect( self._messageview.clear_messages) - message_bridge.s_set_text.connect(self.status.set_text) - message_bridge.s_maybe_reset_text.connect( - self.status.txt.maybe_reset_text) - # statusbar self.tabbed_browser.current_tab_changed.connect( self.status.on_tab_changed) @@ -578,11 +557,11 @@ class MainWindow(QWidget): def _set_decoration(self, hidden): """Set the visibility of the window decoration via Qt.""" - window_flags = Qt.Window # type: int + window_flags: int = Qt.Window refresh_window = self.isVisible() if hidden: window_flags |= Qt.CustomizeWindowHint | Qt.NoDropShadowWindowHint - self.setWindowFlags(typing.cast(Qt.WindowFlags, window_flags)) + self.setWindowFlags(cast(Qt.WindowFlags, window_flags)) if refresh_window: self.show() diff --git a/qutebrowser/mainwindow/messageview.py b/qutebrowser/mainwindow/messageview.py index 1f6295d89..9c4b63084 100644 --- a/qutebrowser/mainwindow/messageview.py +++ b/qutebrowser/mainwindow/messageview.py @@ -19,7 +19,7 @@ """Showing messages above the statusbar.""" -import typing +from typing import MutableSequence from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, Qt from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QSizePolicy @@ -76,7 +76,7 @@ class MessageView(QWidget): def __init__(self, parent=None): super().__init__(parent) - self._messages = [] # type: typing.MutableSequence[Message] + self._messages: MutableSequence[Message] = [] self._vbox = QVBoxLayout(self) self._vbox.setContentsMargins(0, 0, 0, 0) self._vbox.setSpacing(0) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index a929d6428..32fd9709e 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -23,7 +23,7 @@ import os.path import html import collections import functools -import typing +from typing import Deque, MutableSequence, Optional, cast import attr from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelIndex, @@ -40,7 +40,7 @@ from qutebrowser.api import cmdutils from qutebrowser.utils import urlmatch -prompt_queue = typing.cast('PromptQueue', None) +prompt_queue = cast('PromptQueue', None) @attr.s @@ -102,9 +102,8 @@ class PromptQueue(QObject): super().__init__(parent) self._question = None self._shutting_down = False - self._loops = [] # type: typing.MutableSequence[qtutils.EventLoop] - self._queue = collections.deque( - ) # type: typing.Deque[usertypes.Question] + self._loops: MutableSequence[qtutils.EventLoop] = [] + self._queue: Deque[usertypes.Question] = collections.deque() message.global_bridge.mode_left.connect(self._on_mode_left) def __repr__(self): @@ -196,8 +195,8 @@ class PromptQueue(QObject): question.completed.connect(loop.quit) question.completed.connect(loop.deleteLater) log.prompt.debug("Starting loop.exec_() for {}".format(question)) - flags = typing.cast(QEventLoop.ProcessEventsFlags, - QEventLoop.ExcludeSocketNotifiers) + flags = cast(QEventLoop.ProcessEventsFlags, + QEventLoop.ExcludeSocketNotifiers) loop.exec_(flags) log.prompt.debug("Ending loop.exec_() for {}".format(question)) @@ -289,7 +288,7 @@ class PromptContainer(QWidget): self._layout = QVBoxLayout(self) self._layout.setContentsMargins(10, 10, 10, 10) self._win_id = win_id - self._prompt = None # type: typing.Optional[_BasePrompt] + self._prompt: Optional[_BasePrompt] = None self.setObjectName('PromptContainer') self.setAttribute(Qt.WA_StyledBackground, True) @@ -794,8 +793,7 @@ class DownloadFilenamePrompt(FilenamePrompt): def download_open(self, cmdline, pdfjs): if pdfjs: - target = downloads.PDFJSDownloadTarget( - ) # type: downloads._DownloadTarget + target: 'downloads._DownloadTarget' = downloads.PDFJSDownloadTarget() else: target = downloads.OpenFileDownloadTarget(cmdline) diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index f83c77db9..821ea030b 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -31,8 +31,7 @@ from qutebrowser.keyinput import modeman from qutebrowser.utils import usertypes, log, objreg, utils from qutebrowser.mainwindow.statusbar import (backforward, command, progress, keystring, percentage, url, - tabindex) -from qutebrowser.mainwindow.statusbar import text as textwidget + tabindex, textbase) @attr.s @@ -49,7 +48,14 @@ class ColorFlags: passthrough: If we're currently in passthrough-mode. """ - CaretMode = enum.Enum('CaretMode', ['off', 'on', 'selection']) + class CaretMode(enum.Enum): + + """The current caret "sub-mode" we're in.""" + + off = enum.auto() + on = enum.auto() + selection = enum.auto() + prompt = attr.ib(False) insert = attr.ib(False) command = attr.ib(False) @@ -180,7 +186,7 @@ class StatusBar(QWidget): objreg.register('status-command', self.cmd, scope='window', window=win_id) - self.txt = textwidget.Text() + self.txt = textbase.TextBase() self._stack.addWidget(self.txt) self.cmd.show_cmd.connect(self._show_cmd_widget) @@ -328,7 +334,7 @@ class StatusBar(QWidget): else: suffix = '' text = "-- {} MODE --{}".format(mode.upper(), suffix) - self.txt.set_text(self.txt.Text.normal, text) + self.txt.setText(text) def _show_cmd_widget(self): """Show command widget instead of temporary text.""" @@ -342,9 +348,10 @@ class StatusBar(QWidget): self.maybe_hide() @pyqtSlot(str) - def set_text(self, val): + def set_text(self, text): """Set a normal (persistent) text in the status bar.""" - self.txt.set_text(self.txt.Text.normal, val) + log.message.debug(text) + self.txt.setText(text) @pyqtSlot(usertypes.KeyMode) def on_mode_entered(self, mode): @@ -372,7 +379,7 @@ class StatusBar(QWidget): if mode_manager.parsers[new_mode].passthrough: self._set_mode_text(new_mode.name) else: - self.txt.set_text(self.txt.Text.normal, '') + self.txt.setText('') if old_mode in [usertypes.KeyMode.insert, usertypes.KeyMode.command, usertypes.KeyMode.caret, diff --git a/qutebrowser/mainwindow/statusbar/command.py b/qutebrowser/mainwindow/statusbar/command.py index ebd9d3921..da48d1fbd 100644 --- a/qutebrowser/mainwindow/statusbar/command.py +++ b/qutebrowser/mainwindow/statusbar/command.py @@ -71,10 +71,8 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit): self.history.changed.connect(command_history.changed) self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Ignored) - self.cursorPositionChanged.connect( - self.update_completion) # type: ignore[arg-type] - self.textChanged.connect( - self.update_completion) # type: ignore[arg-type] + self.cursorPositionChanged.connect(self.update_completion) + self.textChanged.connect(self.update_completion) self.textChanged.connect(self.updateGeometry) self.textChanged.connect(self._incremental_search) @@ -149,7 +147,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit): raise cmdutils.CommandError( "Invalid command text '{}'.".format(text)) if run_on_count and count is not None: - self.got_cmd[str, int].emit(text, count) # type: ignore[index] + self.got_cmd[str, int].emit(text, count) else: self.set_cmd_text(text) @@ -199,7 +197,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit): 'cmd accept') if not was_search: - self.got_cmd[str].emit(text[1:]) # type: ignore[index] + self.got_cmd[str].emit(text[1:]) @cmdutils.register(instance='status-command', scope='window') def edit_command(self, run: bool = False) -> None: diff --git a/qutebrowser/mainwindow/statusbar/text.py b/qutebrowser/mainwindow/statusbar/text.py deleted file mode 100644 index 449836740..000000000 --- a/qutebrowser/mainwindow/statusbar/text.py +++ /dev/null @@ -1,82 +0,0 @@ -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2014-2020 Florian Bruhin (The Compiler) <mail@qutebrowser.org> -# -# This file is part of qutebrowser. -# -# qutebrowser is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# qutebrowser is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. - -"""Text displayed in the statusbar.""" - -import enum - -from PyQt5.QtCore import pyqtSlot - -from qutebrowser.mainwindow.statusbar import textbase -from qutebrowser.utils import log - - -class Text(textbase.TextBase): - - """Text displayed in the statusbar. - - Attributes: - _normaltext: The "permanent" text. Never automatically cleared. - _temptext: The temporary text to display. - - The temptext is shown from StatusBar when a temporary text or error is - available. If not, the permanent text is shown. - """ - - Text = enum.Enum('Text', ['normal', 'temp']) - - def __init__(self, parent=None): - super().__init__(parent) - self._normaltext = '' - self._temptext = '' - - def set_text(self, which, text): - """Set a text. - - Args: - which: Which text to set, a self.Text instance. - text: The text to set. - """ - log.statusbar.debug("Setting {} text to '{}'.".format( - which.name, text)) - if which is self.Text.normal: - self._normaltext = text - elif which is self.Text.temp: - self._temptext = text - else: - raise ValueError("Invalid value {} for which!".format(which)) - self.update_text() - - @pyqtSlot(str) - def maybe_reset_text(self, text): - """Clear a normal text if it still matches an expected text.""" - if self._normaltext == text: - log.statusbar.debug("Resetting: '{}'".format(text)) - self.set_text(self.Text.normal, '') - else: - log.statusbar.debug("Ignoring reset: '{}'".format(text)) - - def update_text(self): - """Update QLabel text when needed.""" - if self._temptext: - self.setText(self._temptext) - elif self._normaltext: - self.setText(self._normaltext) - else: - self.setText('') diff --git a/qutebrowser/mainwindow/statusbar/url.py b/qutebrowser/mainwindow/statusbar/url.py index c8300dc97..db8905345 100644 --- a/qutebrowser/mainwindow/statusbar/url.py +++ b/qutebrowser/mainwindow/statusbar/url.py @@ -29,9 +29,19 @@ from qutebrowser.config import stylesheet from qutebrowser.utils import usertypes, urlutils -# Note this has entries for success/error/warn from widgets.webview:LoadStatus -UrlType = enum.Enum('UrlType', ['success', 'success_https', 'error', 'warn', - 'hover', 'normal']) +class UrlType(enum.Enum): + + """The type/color of the URL being shown. + + Note this has entries for success/error/warn from widgets.webview:LoadStatus. + """ + + success = enum.auto() + success_https = enum.auto() + error = enum.auto() + warn = enum.auto() + hover = enum.auto() + normal = enum.auto() class UrlText(textbase.TextBase): diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 57a9ae018..2523cdaeb 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -22,8 +22,9 @@ import collections import functools import weakref -import typing import datetime +from typing import ( + Any, Deque, List, Mapping, MutableMapping, MutableSequence, Optional, Tuple) import attr from PyQt5.QtWidgets import QSizePolicy, QWidget, QApplication @@ -68,12 +69,10 @@ class TabDeque: size = config.val.tabs.focus_stack_size if size < 0: size = None - self._stack = collections.deque( - maxlen=size - ) # type: typing.Deque[weakref.ReferenceType[QWidget]] + self._stack: Deque[weakref.ReferenceType[QWidget]] = collections.deque( + maxlen=size) # Items that have been removed from the primary stack. - self._stack_deleted = [ - ] # type: typing.List[weakref.ReferenceType[QWidget]] + self._stack_deleted: List[weakref.ReferenceType[QWidget]] = [] self._ignore_next = False self._keep_deleted_next = False @@ -94,7 +93,7 @@ class TabDeque: Throws IndexError on failure. """ - tab = None # type: typing.Optional[QWidget] + tab: Optional[QWidget] = None while tab is None or tab.pending_removal or tab is cur_tab: tab = self._stack.pop()() self._stack_deleted.append(weakref.ref(cur_tab)) @@ -106,7 +105,7 @@ class TabDeque: Throws IndexError on failure. """ - tab = None # type: typing.Optional[QWidget] + tab: Optional[QWidget] = None while tab is None or tab.pending_removal or tab is cur_tab: tab = self._stack_deleted.pop()() # On next tab-switch, current tab will be added to stack as normal. @@ -224,18 +223,15 @@ class TabbedBrowser(QWidget): # This init is never used, it is immediately thrown away in the next # line. - self.undo_stack = ( - collections.deque() - ) # type: typing.MutableSequence[typing.MutableSequence[_UndoEntry]] + self.undo_stack: MutableSequence[MutableSequence[_UndoEntry]] = ( + collections.deque()) self._update_stack_size() self._filter = signalfilter.SignalFilter(win_id, self) self._now_focused = None self.search_text = None - self.search_options = {} # type: typing.Mapping[str, typing.Any] - self._local_marks = { - } # type: typing.MutableMapping[QUrl, typing.MutableMapping[str, int]] - self._global_marks = { - } # type: typing.MutableMapping[str, typing.Tuple[int, QUrl]] + self.search_options: Mapping[str, Any] = {} + self._local_marks: MutableMapping[QUrl, MutableMapping[str, int]] = {} + self._global_marks: MutableMapping[str, Tuple[int, QUrl]] = {} self.default_window_icon = self.widget.window().windowIcon() self.is_private = private self.tab_deque = TabDeque() diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index fdefa075e..b40c59bd5 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -19,9 +19,9 @@ """The tab widget used for TabbedBrowser from browser.py.""" -import typing import functools import contextlib +from typing import Optional, cast import attr from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QSize, QRect, QPoint, @@ -60,8 +60,7 @@ class TabWidget(QTabWidget): bar = TabBar(win_id, self) self.setStyle(TabBarStyle()) self.setTabBar(bar) - bar.tabCloseRequested.connect( - self.tabCloseRequested) # type: ignore[arg-type] + bar.tabCloseRequested.connect(self.tabCloseRequested) bar.tabMoved.connect(functools.partial( QTimer.singleShot, 0, self.update_tab_titles)) bar.currentChanged.connect(self._on_current_changed) @@ -340,8 +339,7 @@ class TabWidget(QTabWidget): def setTabIcon(self, idx: int, icon: QIcon) -> None: """Always show tab icons for pinned tabs in some circumstances.""" - tab = typing.cast(typing.Optional[browsertab.AbstractTab], - self.widget(idx)) + tab = cast(Optional[browsertab.AbstractTab], self.widget(idx)) if (icon.isNull() and config.cache['tabs.favicons.show'] != 'never' and config.cache['tabs.pinned.shrink'] and diff --git a/qutebrowser/mainwindow/windowundo.py b/qutebrowser/mainwindow/windowundo.py index 4a4ea5d66..af7b2766a 100644 --- a/qutebrowser/mainwindow/windowundo.py +++ b/qutebrowser/mainwindow/windowundo.py @@ -20,7 +20,7 @@ """Code for :undo --window.""" import collections -import typing +from typing import MutableSequence, cast import attr from PyQt5.QtCore import QObject @@ -30,7 +30,7 @@ from qutebrowser.config import config from qutebrowser.mainwindow import mainwindow -instance = typing.cast('WindowUndoManager', None) +instance = cast('WindowUndoManager', None) @attr.s @@ -48,9 +48,7 @@ class WindowUndoManager(QObject): def __init__(self, parent=None): super().__init__(parent) - self._undos = ( - collections.deque() - ) # type: typing.MutableSequence[_WindowUndoEntry] + self._undos: MutableSequence[_WindowUndoEntry] = collections.deque() QApplication.instance().window_closing.connect(self._on_window_closing) config.instance.changed.connect(self._on_config_changed) diff --git a/qutebrowser/misc/autoupdate.py b/qutebrowser/misc/autoupdate.py index 4838d55ed..49d648f58 100644 --- a/qutebrowser/misc/autoupdate.py +++ b/qutebrowser/misc/autoupdate.py @@ -55,7 +55,7 @@ class PyPIVersionClient(QObject): self._client = httpclient.HTTPClient(self) else: self._client = client - self._client.error.connect(self.error) # type: ignore[arg-type] + self._client.error.connect(self.error) self._client.success.connect(self.on_client_success) def get_version(self, package='qutebrowser'): diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py index 089e3191f..f9c210f15 100644 --- a/qutebrowser/misc/backendproblem.py +++ b/qutebrowser/misc/backendproblem.py @@ -25,8 +25,8 @@ import functools import html import enum import shutil -import typing import argparse +from typing import Any, List, Sequence, Tuple import attr from PyQt5.QtCore import Qt @@ -55,15 +55,13 @@ class _Button: """A button passed to BackendProblemDialog.""" - text = attr.ib() # type: str - setting = attr.ib() # type: str - value = attr.ib() # type: typing.Any - default = attr.ib(default=False) # type: bool + text: str = attr.ib() + setting: str = attr.ib() + value: Any = attr.ib() + default: bool = attr.ib(default=False) -def _other_backend( - backend: usertypes.Backend -) -> typing.Tuple[usertypes.Backend, str]: +def _other_backend(backend: usertypes.Backend) -> Tuple[usertypes.Backend, str]: """Get the other backend enum/setting for a given backend.""" other_backend = { usertypes.Backend.QtWebKit: usertypes.Backend.QtWebEngine, @@ -103,7 +101,7 @@ class _Dialog(QDialog): def __init__(self, *, because: str, text: str, backend: usertypes.Backend, - buttons: typing.Sequence[_Button] = None, + buttons: Sequence[_Button] = None, parent: QWidget = None) -> None: super().__init__(parent) vbox = QVBoxLayout(self) @@ -157,10 +155,10 @@ class _BackendImports: """Whether backend modules could be imported.""" - webkit_available = attr.ib(default=None) # type: bool - webengine_available = attr.ib(default=None) # type: bool - webkit_error = attr.ib(default=None) # type: str - webengine_error = attr.ib(default=None) # type: str + webkit_available: bool = attr.ib(default=None) + webengine_available: bool = attr.ib(default=None) + webkit_error: str = attr.ib(default=None) + webengine_error: str = attr.ib(default=None) class _BackendProblemChecker: @@ -181,7 +179,7 @@ class _BackendProblemChecker: self._save_manager = save_manager self._no_err_windows = no_err_windows - def _show_dialog(self, *args: typing.Any, **kwargs: typing.Any) -> None: + def _show_dialog(self, *args: Any, **kwargs: Any) -> None: """Show a dialog for a backend problem.""" if self._no_err_windows: text = _error_text(*args, **kwargs) @@ -255,7 +253,7 @@ class _BackendProblemChecker: raise utils.Unreachable - def _xwayland_options(self) -> typing.Tuple[str, typing.List[_Button]]: + def _xwayland_options(self) -> Tuple[str, List[_Button]]: """Get buttons/text for a possible XWayland solution.""" buttons = [] text = "<p>You can work around this in one of the following ways:</p>" diff --git a/qutebrowser/misc/checkpyver.py b/qutebrowser/misc/checkpyver.py index 8283dd13e..116514b8d 100644 --- a/qutebrowser/misc/checkpyver.py +++ b/qutebrowser/misc/checkpyver.py @@ -43,11 +43,11 @@ except ImportError: # pragma: no cover # to stderr. def check_python_version(): """Check if correct python version is run.""" - if sys.hexversion < 0x03050200: + if sys.hexversion < 0x03060000: # We don't use .format() and print_function here just in case someone # still has < 2.6 installed. version_str = '.'.join(map(str, sys.version_info[:3])) - text = ("At least Python 3.5.2 is required to run qutebrowser, but " + + text = ("At least Python 3.6 is required to run qutebrowser, but " + "it's running with " + version_str + ".\n") if (Tk and # type: ignore[unreachable] '--no-err-windows' not in sys.argv): # pragma: no cover diff --git a/qutebrowser/misc/cmdhistory.py b/qutebrowser/misc/cmdhistory.py index 810bbd1f9..1403ee56d 100644 --- a/qutebrowser/misc/cmdhistory.py +++ b/qutebrowser/misc/cmdhistory.py @@ -19,7 +19,7 @@ """Command history for the status bar.""" -import typing +from typing import MutableSequence from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject @@ -60,7 +60,7 @@ class History(QObject): super().__init__(parent) self._tmphist = None if history is None: - self.history = [] # type: typing.MutableSequence[str] + self.history: MutableSequence[str] = [] else: self.history = history @@ -82,9 +82,9 @@ class History(QObject): """ log.misc.debug("Preset text: '{}'".format(text)) if text: - items = [ + items: MutableSequence[str] = [ e for e in self.history - if e.startswith(text)] # type: typing.MutableSequence[str] + if e.startswith(text)] else: items = self.history if not items: diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py index 3f80db769..65e584bc9 100644 --- a/qutebrowser/misc/crashsignal.py +++ b/qutebrowser/misc/crashsignal.py @@ -29,7 +29,7 @@ import argparse import functools import threading import faulthandler -import typing +from typing import TYPE_CHECKING, Optional, MutableMapping, cast try: # WORKAROUND for segfaults when using pdb in pytest for some reason... import readline # pylint: disable=unused-import @@ -45,7 +45,7 @@ from qutebrowser.api import cmdutils from qutebrowser.misc import earlyinit, crashdialog, ipc, objects from qutebrowser.utils import usertypes, standarddir, log, objreg, debug, utils from qutebrowser.qt import sip -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from qutebrowser.misc import quitter @@ -59,7 +59,7 @@ class ExceptionInfo: objects = attr.ib() -crash_handler = typing.cast('CrashHandler', None) +crash_handler = cast('CrashHandler', None) class CrashHandler(QObject): @@ -337,10 +337,9 @@ class SignalHandler(QObject): self._quitter = quitter self._notifier = None self._timer = usertypes.Timer(self, 'python_hacks') - self._orig_handlers = { - } # type: typing.MutableMapping[int, signal._HANDLER] + self._orig_handlers: MutableMapping[int, 'signal._HANDLER'] = {} self._activated = False - self._orig_wakeup_fd = None # type: typing.Optional[int] + self._orig_wakeup_fd: Optional[int] = None def activate(self): """Set up signal handlers. @@ -363,7 +362,7 @@ class SignalHandler(QObject): for fd in [read_fd, write_fd]: flags = fcntl.fcntl(fd, fcntl.F_GETFL) fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) - self._notifier = QSocketNotifier(typing.cast(sip.voidptr, read_fd), + self._notifier = QSocketNotifier(cast(sip.voidptr, read_fd), QSocketNotifier.Read, self) self._notifier.activated.connect( # type: ignore[attr-defined] diff --git a/qutebrowser/misc/debugcachestats.py b/qutebrowser/misc/debugcachestats.py index 417e4505a..227f9d668 100644 --- a/qutebrowser/misc/debugcachestats.py +++ b/qutebrowser/misc/debugcachestats.py @@ -23,17 +23,17 @@ Because many modules depend on this command, this needs to have as few dependencies as possible to avoid cyclic dependencies. """ -import typing +from typing import Any, Callable, List, Optional, Tuple, TypeVar # The second element of each tuple should be a lru_cache wrapped function -_CACHE_FUNCTIONS = [] # type: typing.List[typing.Tuple[str, typing.Any]] +_CACHE_FUNCTIONS: List[Tuple[str, Any]] = [] -_T = typing.TypeVar('_T', bound=typing.Callable) +_T = TypeVar('_T', bound=Callable) -def register(name: typing.Optional[str] = None) -> typing.Callable[[_T], _T]: +def register(name: Optional[str] = None) -> Callable[[_T], _T]: """Register a lru_cache wrapped function for debug_cache_stats.""" def wrapper(fn: _T) -> _T: _CACHE_FUNCTIONS.append((fn.__name__ if name is None else name, fn)) diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py index c02b2f03c..704db1777 100644 --- a/qutebrowser/misc/earlyinit.py +++ b/qutebrowser/misc/earlyinit.py @@ -19,7 +19,7 @@ """Things which need to be done really early (e.g. before importing Qt). -At this point we can be sure we have all python 3.5 features available. +At this point we can be sure we have all python 3.6 features available. """ try: @@ -210,13 +210,13 @@ def _check_modules(modules): try: # https://bitbucket.org/fdik/pypeg/commits/dd15ca462b532019c0a3be1d39b8ee2f3fa32f4e # pylint: disable=bad-continuation - with log.ignore_py_warnings( + with log.py_warning_filter( category=DeprecationWarning, message=r'invalid escape sequence' - ), log.ignore_py_warnings( + ), log.py_warning_filter( category=ImportWarning, message=r'Not importing directory .*: missing __init__' - ), log.ignore_py_warnings( + ), log.py_warning_filter( category=DeprecationWarning, message=r'the imp module is deprecated', ): @@ -260,7 +260,7 @@ def configure_pyqt(): from qutebrowser.qt import sip try: # Added in sip 4.19.4 - sip.enableoverflowchecking(True) # type: ignore[attr-defined] + sip.enableoverflowchecking(True) except AttributeError: pass diff --git a/qutebrowser/misc/guiprocess.py b/qutebrowser/misc/guiprocess.py index 3702715c4..872a594f3 100644 --- a/qutebrowser/misc/guiprocess.py +++ b/qutebrowser/misc/guiprocess.py @@ -63,11 +63,11 @@ class GUIProcess(QObject): self._proc = QProcess(self) self._proc.errorOccurred.connect(self._on_error) - self._proc.errorOccurred.connect(self.error) # type: ignore[arg-type] + self._proc.errorOccurred.connect(self.error) self._proc.finished.connect(self._on_finished) - self._proc.finished.connect(self.finished) # type: ignore[arg-type] + self._proc.finished.connect(self.finished) self._proc.started.connect(self._on_started) - self._proc.started.connect(self.started) # type: ignore[arg-type] + self._proc.started.connect(self.started) if additional_env is not None: procenv = QProcessEnvironment.systemEnvironment() diff --git a/qutebrowser/misc/httpclient.py b/qutebrowser/misc/httpclient.py index 822ba8805..b9502ca43 100644 --- a/qutebrowser/misc/httpclient.py +++ b/qutebrowser/misc/httpclient.py @@ -19,10 +19,9 @@ """An HTTP client based on QNetworkAccessManager.""" -import typing import functools -import urllib.request import urllib.parse +from typing import MutableMapping from PyQt5.QtCore import pyqtSignal, QObject, QTimer from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkRequest, @@ -67,7 +66,7 @@ class HTTPClient(QObject): def __init__(self, parent=None): super().__init__(parent) self._nam = QNetworkAccessManager(self) - self._timers = {} # type: typing.MutableMapping[QNetworkReply, QTimer] + self._timers: MutableMapping[QNetworkReply, QTimer] = {} def post(self, url, data=None): """Create a new POST request. diff --git a/qutebrowser/misc/lineparser.py b/qutebrowser/misc/lineparser.py index 749c5dff1..d3e5a2db0 100644 --- a/qutebrowser/misc/lineparser.py +++ b/qutebrowser/misc/lineparser.py @@ -22,7 +22,7 @@ import os import os.path import contextlib -import typing +from typing import Sequence from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject @@ -150,7 +150,7 @@ class LineParser(BaseLineParser): """ super().__init__(configdir, fname, binary=binary, parent=parent) if not os.path.isfile(self._configfile): - self.data = [] # type: typing.Sequence[str] + self.data: Sequence[str] = [] else: log.init.debug("Reading {}".format(self._configfile)) self._read() diff --git a/qutebrowser/misc/miscwidgets.py b/qutebrowser/misc/miscwidgets.py index 2310a1926..a38607390 100644 --- a/qutebrowser/misc/miscwidgets.py +++ b/qutebrowser/misc/miscwidgets.py @@ -19,7 +19,7 @@ """Misc. widgets used at different places.""" -import typing +from typing import Optional from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QSize, QTimer from PyQt5.QtWidgets import (QLineEdit, QWidget, QHBoxLayout, QLabel, @@ -239,8 +239,8 @@ class WrapperLayout(QLayout): def __init__(self, parent=None): super().__init__(parent) - self._widget = None # type: typing.Optional[QWidget] - self._container = None # type: typing.Optional[QWidget] + self._widget: Optional[QWidget] = None + self._container: Optional[QWidget] = None def addItem(self, _widget): raise utils.Unreachable @@ -390,10 +390,10 @@ class InspectorSplitter(QSplitter): self.addWidget(main_webview) self.setFocusProxy(main_webview) self.splitterMoved.connect(self._on_splitter_moved) - self._main_idx = None # type: typing.Optional[int] - self._inspector_idx = None # type: typing.Optional[int] - self._position = None # type: typing.Optional[inspector.Position] - self._preferred_size = None # type: typing.Optional[int] + self._main_idx: Optional[int] = None + self._inspector_idx: Optional[int] = None + self._position: Optional[inspector.Position] = None + self._preferred_size: Optional[int] = None def cycle_focus(self): """Cycle keyboard focus between the main/inspector widget.""" diff --git a/qutebrowser/misc/objects.py b/qutebrowser/misc/objects.py index 28a1830d5..c2e20e9ad 100644 --- a/qutebrowser/misc/objects.py +++ b/qutebrowser/misc/objects.py @@ -22,10 +22,10 @@ # NOTE: We need to be careful with imports here, as this is imported from # earlyinit. -import typing import argparse +from typing import TYPE_CHECKING, Any, Dict, Set, Union, cast -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from qutebrowser.utils import usertypes from qutebrowser.commands import command @@ -38,11 +38,11 @@ class NoBackend: def name(self) -> str: raise AssertionError("No backend set!") - def __eq__(self, other: typing.Any) -> bool: + def __eq__(self, other: Any) -> bool: raise AssertionError("No backend set!") -backend = NoBackend() # type: typing.Union[usertypes.Backend, NoBackend] -commands = {} # type: typing.Dict[str, command.Command] -debug_flags = set() # type: typing.Set[str] -args = typing.cast(argparse.Namespace, None) +backend: Union['usertypes.Backend', NoBackend] = NoBackend() +commands: Dict[str, 'command.Command'] = {} +debug_flags: Set[str] = set() +args = cast(argparse.Namespace, None) diff --git a/qutebrowser/misc/quitter.py b/qutebrowser/misc/quitter.py index c7f0c8072..86342d57b 100644 --- a/qutebrowser/misc/quitter.py +++ b/qutebrowser/misc/quitter.py @@ -25,11 +25,11 @@ import sys import json import atexit import shutil -import typing import argparse import tokenize import functools import subprocess +from typing import Iterable, Mapping, MutableSequence, Sequence, cast from PyQt5.QtCore import QObject, pyqtSignal, QTimer from PyQt5.QtWidgets import QApplication @@ -46,7 +46,7 @@ from qutebrowser.mainwindow import prompt from qutebrowser.completion.models import miscmodels -instance = typing.cast('Quitter', None) +instance = cast('Quitter', None) class Quitter(QObject): @@ -97,10 +97,10 @@ class Quitter(QObject): compile(f.read(), fn, 'exec') def _get_restart_args( - self, pages: typing.Iterable[str] = (), + self, pages: Iterable[str] = (), session: str = None, - override_args: typing.Mapping[str, str] = None - ) -> typing.Sequence[str]: + override_args: Mapping[str, str] = None + ) -> Sequence[str]: """Get args to relaunch qutebrowser. Args: @@ -120,7 +120,7 @@ class Quitter(QObject): args = [sys.executable, '-m', 'qutebrowser'] # Add all open pages so they get reopened. - page_args = [] # type: typing.MutableSequence[str] + page_args: MutableSequence[str] = [] for win in pages: page_args.extend(win) page_args.append('') @@ -157,9 +157,9 @@ class Quitter(QObject): return args - def restart(self, pages: typing.Sequence[str] = (), + def restart(self, pages: Sequence[str] = (), session: str = None, - override_args: typing.Mapping[str, str] = None) -> bool: + override_args: Mapping[str, str] = None) -> bool: """Inner logic to restart qutebrowser. The "better" way to restart is to pass a session (_restart usually) as diff --git a/qutebrowser/misc/savemanager.py b/qutebrowser/misc/savemanager.py index daa484934..c7a20adbb 100644 --- a/qutebrowser/misc/savemanager.py +++ b/qutebrowser/misc/savemanager.py @@ -21,7 +21,7 @@ import os.path import collections -import typing +from typing import MutableMapping from PyQt5.QtCore import pyqtSlot, QObject, QTimer @@ -112,8 +112,7 @@ class SaveManager(QObject): def __init__(self, parent=None): super().__init__(parent) - self.saveables = collections.OrderedDict( - ) # type: typing.MutableMapping[str, Saveable] + self.saveables: MutableMapping[str, Saveable] = collections.OrderedDict() self._save_timer = usertypes.Timer(self, name='save-timer') self._save_timer.timeout.connect(self.autosave) self._set_autosave_interval() diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index 0ebb415ac..eecc1964e 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -23,9 +23,9 @@ import os import os.path import itertools import urllib -import typing import glob import shutil +from typing import Any, Iterable, MutableMapping, MutableSequence, Optional, Union, cast from PyQt5.QtCore import Qt, QUrl, QObject, QPoint, QTimer, QDateTime from PyQt5.QtWidgets import QApplication @@ -40,7 +40,7 @@ from qutebrowser.mainwindow import mainwindow from qutebrowser.qt import sip -_JsonType = typing.MutableMapping[str, typing.Any] +_JsonType = MutableMapping[str, Any] class Sentinel: @@ -49,9 +49,9 @@ class Sentinel: default = Sentinel() -session_manager = typing.cast('SessionManager', None) +session_manager = cast('SessionManager', None) -ArgType = typing.Union[str, Sentinel] +ArgType = Union[str, Sentinel] def init(parent=None): @@ -80,7 +80,7 @@ def init(parent=None): session_manager = SessionManager(base_path, parent) -def shutdown(session: typing.Optional[ArgType], last_window: bool) -> None: +def shutdown(session: Optional[ArgType], last_window: bool) -> None: """Handle a shutdown by saving sessions and removing the autosave file.""" if session_manager is None: return # type: ignore @@ -153,7 +153,7 @@ class SessionManager(QObject): def __init__(self, base_path, parent=None): super().__init__(parent) - self.current = None # type: typing.Optional[str] + self.current: Optional[str] = None self._base_path = base_path self._last_window_session = None self.did_load = False @@ -196,9 +196,9 @@ class SessionManager(QObject): Return: A dict with the saved data for this item. """ - data = { + data: _JsonType = { 'url': bytes(item.url().toEncoded()).decode('ascii'), - } # type: _JsonType + } if item.title(): data['title'] = item.title() @@ -246,7 +246,7 @@ class SessionManager(QObject): tab: The WebView to save. active: Whether the tab is currently active. """ - data = {'history': []} # type: _JsonType + data: _JsonType = {'history': []} if active: data['active'] = True for idx, item in enumerate(tab.history): @@ -263,9 +263,9 @@ class SessionManager(QObject): def _save_all(self, *, only_window=None, with_private=False): """Get a dict with data for all windows/tabs.""" - data = {'windows': []} # type: _JsonType + data: _JsonType = {'windows': []} if only_window is not None: - winlist = [only_window] # type: typing.Iterable[int] + winlist: Iterable[int] = [only_window] else: winlist = objreg.window_registry @@ -282,7 +282,7 @@ class SessionManager(QObject): if tabbed_browser.is_private and not with_private: continue - win_data = {} # type: _JsonType + win_data: _JsonType = {} active_window = QApplication.instance().activeWindow() if getattr(active_window, 'win_id', None) == win_id: win_data['active'] = True @@ -377,7 +377,7 @@ class SessionManager(QObject): def _load_tab(self, new_tab, data): # noqa: C901 """Load yaml data into a newly opened tab.""" entries = [] - lazy_load = [] # type: typing.MutableSequence[_JsonType] + lazy_load: MutableSequence[_JsonType] = [] # use len(data['history']) # -> dropwhile empty if not session.lazy_session lazy_index = len(data['history']) @@ -436,10 +436,10 @@ class SessionManager(QObject): orig_url = url if histentry.get("last_visited"): - last_visited = QDateTime.fromString( + last_visited: Optional[QDateTime] = QDateTime.fromString( histentry.get("last_visited"), Qt.ISODate, - ) # type: typing.Optional[QDateTime] + ) else: last_visited = None diff --git a/qutebrowser/misc/throttle.py b/qutebrowser/misc/throttle.py index a8d24bd12..3540d8824 100644 --- a/qutebrowser/misc/throttle.py +++ b/qutebrowser/misc/throttle.py @@ -19,8 +19,8 @@ """A throttle for throttling function calls.""" -import typing import time +from typing import Any, Callable, Mapping, Optional, Sequence import attr from PyQt5.QtCore import QObject @@ -31,8 +31,8 @@ from qutebrowser.utils import usertypes @attr.s class _CallArgs: - args = attr.ib() # type: typing.Sequence[typing.Any] - kwargs = attr.ib() # type: typing.Mapping[str, typing.Any] + args: Sequence[Any] = attr.ib() + kwargs: Mapping[str, Any] = attr.ib() class Throttle(QObject): @@ -45,7 +45,7 @@ class Throttle(QObject): """ def __init__(self, - func: typing.Callable, + func: Callable, delay_ms: int, parent: QObject = None) -> None: """Constructor. @@ -59,8 +59,8 @@ class Throttle(QObject): super().__init__(parent) self._delay_ms = delay_ms self._func = func - self._pending_call = None # type: typing.Optional[_CallArgs] - self._last_call_ms = None # type: typing.Optional[int] + self._pending_call: Optional[_CallArgs] = None + self._last_call_ms: Optional[int] = None self._timer = usertypes.Timer(self, 'throttle-timer') self._timer.setSingleShot(True) @@ -71,7 +71,7 @@ class Throttle(QObject): self._pending_call = None self._last_call_ms = int(time.monotonic() * 1000) - def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> typing.Any: + def __call__(self, *args: Any, **kwargs: Any) -> Any: cur_time_ms = int(time.monotonic() * 1000) if self._pending_call is None: if (self._last_call_ms is None or diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index 8c2462b2b..56138c798 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -24,7 +24,7 @@ import functools import os import traceback -import typing +from typing import Optional from PyQt5.QtCore import QUrl from PyQt5.QtWidgets import QApplication @@ -274,7 +274,7 @@ def version(win_id: int, paste: bool = False) -> None: pastebin_version() -_keytester_widget = None # type: typing.Optional[miscwidgets.KeyTesterWidget] +_keytester_widget: Optional[miscwidgets.KeyTesterWidget] = None @cmdutils.register(debug=True) diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index 93c38d841..fb2776376 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -170,12 +170,14 @@ def debug_flag_error(flag): log-scroll-pos: Log all scrolling changes. stack: Enable Chromium stack logging. chromium: Enable Chromium logging. + wait-renderer-process: Wait for debugger in renderer process. + avoid-chromium-init: Enable `--version` without initializing Chromium. werror: Turn Python warnings into errors. """ valid_flags = ['debug-exit', 'pdb-postmortem', 'no-sql-history', 'no-scroll-filtering', 'log-requests', 'log-cookies', 'lost-focusproxy', 'log-scroll-pos', 'stack', 'chromium', - 'werror'] + 'wait-renderer-process', 'avoid-chromium-init', 'werror'] if flag in valid_flags: return flag diff --git a/qutebrowser/utils/debug.py b/qutebrowser/utils/debug.py index 66cfeed9e..cd2edc39a 100644 --- a/qutebrowser/utils/debug.py +++ b/qutebrowser/utils/debug.py @@ -24,22 +24,23 @@ import inspect import logging import functools import datetime -import typing import types +from typing import ( + Any, Callable, List, Mapping, MutableSequence, Optional, Sequence, Type, Union) -from PyQt5.QtCore import Qt, QEvent, QMetaMethod, QObject, pyqtSignal +from PyQt5.QtCore import Qt, QEvent, QMetaMethod, QObject, pyqtBoundSignal from PyQt5.QtWidgets import QApplication from qutebrowser.utils import log, utils, qtutils, objreg from qutebrowser.qt import sip -def log_events(klass: typing.Type) -> typing.Type: +def log_events(klass: Type) -> Type: """Class decorator to log Qt events.""" old_event = klass.event @functools.wraps(old_event) - def new_event(self: typing.Any, e: QEvent) -> bool: + def new_event(self: Any, e: QEvent) -> bool: """Wrapper for event() which logs events.""" log.misc.debug("Event in {}: {}".format(utils.qualname(klass), qenum_key(QEvent, e.type()))) @@ -54,7 +55,7 @@ def log_signals(obj: QObject) -> QObject: Can be used as class decorator. """ - def log_slot(obj: QObject, signal: pyqtSignal, *args: typing.Any) -> None: + def log_slot(obj: QObject, signal: pyqtBoundSignal, *args: Any) -> None: """Slot connected to a signal to log it.""" dbg = dbg_signal(signal, args) try: @@ -83,9 +84,7 @@ def log_signals(obj: QObject) -> QObject: old_init = obj.__init__ # type: ignore[misc] @functools.wraps(old_init) - def new_init(self: typing.Any, - *args: typing.Any, - **kwargs: typing.Any) -> None: + def new_init(self: Any, *args: Any, **kwargs: Any) -> None: """Wrapper for __init__() which logs signals.""" old_init(self, *args, **kwargs) connect_log_slot(self) @@ -97,10 +96,10 @@ def log_signals(obj: QObject) -> QObject: return obj -def qenum_key(base: typing.Type, - value: typing.Union[int, sip.simplewrapper], +def qenum_key(base: Type, + value: Union[int, sip.simplewrapper], add_base: bool = False, - klass: typing.Type = None) -> str: + klass: Type = None) -> str: """Convert a Qt Enum value to its key as a string. Args: @@ -140,10 +139,10 @@ def qenum_key(base: typing.Type, return ret -def qflags_key(base: typing.Type, - value: typing.Union[int, sip.simplewrapper], +def qflags_key(base: Type, + value: Union[int, sip.simplewrapper], add_base: bool = False, - klass: typing.Type = None) -> str: + klass: Type = None) -> str: """Convert a Qt QFlags value to its keys as string. Note: Passing a combined value (such as Qt.AlignCenter) will get the names @@ -188,7 +187,7 @@ def qflags_key(base: typing.Type, return '|'.join(names) -def signal_name(sig: pyqtSignal) -> str: +def signal_name(sig: pyqtBoundSignal) -> str: """Get a cleaned up name of a signal. Unfortunately, the way to get the name of a signal differs based on: @@ -199,7 +198,7 @@ def signal_name(sig: pyqtSignal) -> str: fails, extract it from the repr(). Args: - sig: The pyqtSignal + sig: A bound signal. Return: The cleaned up signal name. @@ -209,8 +208,7 @@ def signal_name(sig: pyqtSignal) -> str: # Examples: # sig.signal == '2signal1' # sig.signal == '2signal2(QString,QString)' - m = re.fullmatch(r'[0-9]+(?P<name>.*)\(.*\)', - sig.signal) # type: ignore[attr-defined] + m = re.fullmatch(r'[0-9]+(?P<name>.*)\(.*\)', sig.signal) elif hasattr(sig, 'signatures'): # Unbound signal, PyQt >= 5.11 # Examples: @@ -238,8 +236,7 @@ def signal_name(sig: pyqtSignal) -> str: return m.group('name') -def format_args(args: typing.Sequence = None, - kwargs: typing.Mapping = None) -> str: +def format_args(args: Sequence = None, kwargs: Mapping = None) -> str: """Format a list of arguments/kwargs to a function-call like string.""" if args is not None: arglist = [utils.compact_text(repr(arg), 200) for arg in args] @@ -251,11 +248,11 @@ def format_args(args: typing.Sequence = None, return ', '.join(arglist) -def dbg_signal(sig: pyqtSignal, args: typing.Any) -> str: +def dbg_signal(sig: pyqtBoundSignal, args: Any) -> str: """Get a string representation of a signal for debugging. Args: - sig: A pyqtSignal. + sig: A bound signal. args: The arguments as list of strings. Return: @@ -264,9 +261,9 @@ def dbg_signal(sig: pyqtSignal, args: typing.Any) -> str: return '{}({})'.format(signal_name(sig), format_args(args)) -def format_call(func: typing.Callable, - args: typing.Sequence = None, - kwargs: typing.Mapping = None, +def format_call(func: Callable, + args: Sequence = None, + kwargs: Mapping = None, full: bool = True) -> str: """Get a string representation of a function calls with the given args. @@ -293,7 +290,7 @@ class log_time: # noqa: N801,N806 pylint: disable=invalid-name Usable as context manager or as decorator. """ - def __init__(self, logger: typing.Union[logging.Logger, str], + def __init__(self, logger: Union[logging.Logger, str], action: str = 'operation') -> None: """Constructor. @@ -305,28 +302,25 @@ class log_time: # noqa: N801,N806 pylint: disable=invalid-name self._logger = logging.getLogger(logger) else: self._logger = logger - self._started = None # type: typing.Optional[datetime.datetime] + self._started: Optional[datetime.datetime] = None self._action = action def __enter__(self) -> None: self._started = datetime.datetime.now() - # The string annotation is a WORKAROUND for a Python 3.5.2 bug: - # https://github.com/python/typing/issues/266 - def __exit__(self, - _exc_type: 'typing.Optional[typing.Type[BaseException]]', - _exc_val: typing.Optional[BaseException], - _exc_tb: typing.Optional[types.TracebackType]) -> None: + _exc_type: Optional[Type[BaseException]], + _exc_val: Optional[BaseException], + _exc_tb: Optional[types.TracebackType]) -> None: assert self._started is not None finished = datetime.datetime.now() delta = (finished - self._started).total_seconds() self._logger.debug("{} took {} seconds.".format( self._action.capitalize(), delta)) - def __call__(self, func: typing.Callable) -> typing.Callable: + def __call__(self, func: Callable) -> Callable: @functools.wraps(func) - def wrapped(*args: typing.Any, **kwargs: typing.Any) -> typing.Any: + def wrapped(*args: Any, **kwargs: Any) -> Any: """Call the original function.""" with self: return func(*args, **kwargs) @@ -334,14 +328,14 @@ class log_time: # noqa: N801,N806 pylint: disable=invalid-name return wrapped -def _get_widgets() -> typing.Sequence[str]: +def _get_widgets() -> Sequence[str]: """Get a string list of all widgets.""" widgets = QApplication.instance().allWidgets() widgets.sort(key=repr) return [repr(w) for w in widgets] -def _get_pyqt_objects(lines: typing.MutableSequence[str], +def _get_pyqt_objects(lines: MutableSequence[str], obj: QObject, depth: int = 0) -> None: """Recursive method for get_all_objects to get Qt objects.""" @@ -362,7 +356,7 @@ def get_all_objects(start_obj: QObject = None) -> str: if start_obj is None: start_obj = QApplication.instance() - pyqt_lines = [] # type: typing.List[str] + pyqt_lines: List[str] = [] _get_pyqt_objects(pyqt_lines, start_obj) pyqt_lines = [' ' + e for e in pyqt_lines] pyqt_lines.insert(0, 'Qt objects - {} objects:'.format(len(pyqt_lines))) diff --git a/qutebrowser/utils/docutils.py b/qutebrowser/utils/docutils.py index 0ef971dfc..5a3a0d263 100644 --- a/qutebrowser/utils/docutils.py +++ b/qutebrowser/utils/docutils.py @@ -25,7 +25,7 @@ import inspect import os.path import collections import enum -import typing +from typing import Callable, Dict, Optional, List, Union import qutebrowser from qutebrowser.utils import log, utils @@ -77,21 +77,28 @@ class DocstringParser: arg_descs: A dict of argument names to their descriptions """ - State = enum.Enum('State', ['short', 'desc', 'desc_hidden', - 'arg_start', 'arg_inside', 'misc']) + class State(enum.Enum): - def __init__(self, func: typing.Callable) -> None: + """The current state of the parser.""" + + short = enum.auto() + desc = enum.auto() + desc_hidden = enum.auto() + arg_start = enum.auto() + arg_inside = enum.auto() + misc = enum.auto() + + def __init__(self, func: Callable) -> None: """Constructor. Args: func: The function to parse the docstring for. """ self._state = self.State.short - self._cur_arg_name = None # type: typing.Optional[str] - self._short_desc_parts = [] # type: typing.List[str] - self._long_desc_parts = [] # type: typing.List[str] - self.arg_descs = collections.OrderedDict( - ) # type: typing.Dict[str, typing.Union[str, typing.List[str]]] + self._cur_arg_name: Optional[str] = None + self._short_desc_parts: List[str] = [] + self._long_desc_parts: List[str] = [] + self.arg_descs: Dict[str, Union[str, List[str]]] = collections.OrderedDict() doc = inspect.getdoc(func) handlers = { self.State.short: self._parse_short, diff --git a/qutebrowser/utils/javascript.py b/qutebrowser/utils/javascript.py index 1b499a377..94068c640 100644 --- a/qutebrowser/utils/javascript.py +++ b/qutebrowser/utils/javascript.py @@ -19,10 +19,10 @@ """Utilities related to javascript interaction.""" -import typing +from typing import Sequence, Union -_InnerJsArgType = typing.Union[None, str, bool, int, float] -_JsArgType = typing.Union[_InnerJsArgType, typing.Sequence[_InnerJsArgType]] +_InnerJsArgType = Union[None, str, bool, int, float] +_JsArgType = Union[_InnerJsArgType, Sequence[_InnerJsArgType]] def string_escape(text: str) -> str: diff --git a/qutebrowser/utils/jinja.py b/qutebrowser/utils/jinja.py index 78663645d..f60c46bbc 100644 --- a/qutebrowser/utils/jinja.py +++ b/qutebrowser/utils/jinja.py @@ -21,10 +21,10 @@ import os import os.path -import typing import functools import contextlib import html +from typing import Any, Callable, FrozenSet, Iterator, List, Set, Tuple import jinja2 import jinja2.nodes @@ -68,7 +68,7 @@ class Loader(jinja2.BaseLoader): self, _env: jinja2.Environment, template: str - ) -> typing.Tuple[str, str, typing.Callable[[], bool]]: + ) -> Tuple[str, str, Callable[[], bool]]: path = os.path.join(self._subdir, template) try: source = utils.read_file(path) @@ -98,7 +98,7 @@ class Environment(jinja2.Environment): self._autoescape = True @contextlib.contextmanager - def no_autoescape(self) -> typing.Iterator[None]: + def no_autoescape(self) -> Iterator[None]: """Context manager to temporarily turn off autoescaping.""" self._autoescape = False yield @@ -122,7 +122,7 @@ class Environment(jinja2.Environment): mimetype = utils.guess_mimetype(filename) return urlutils.data_url(mimetype, data).toString() - def getattr(self, obj: typing.Any, attribute: str) -> typing.Any: + def getattr(self, obj: Any, attribute: str) -> Any: """Override jinja's getattr() to be less clever. This means it doesn't fall back to __getitem__, and it doesn't hide @@ -131,7 +131,7 @@ class Environment(jinja2.Environment): return getattr(obj, attribute) -def render(template: str, **kwargs: typing.Any) -> str: +def render(template: str, **kwargs: Any) -> str: """Render the given template and pass the given arguments to it.""" return environment.get_template(template).render(**kwargs) @@ -142,10 +142,10 @@ js_environment = jinja2.Environment(loader=Loader('javascript')) @debugcachestats.register() @functools.lru_cache() -def template_config_variables(template: str) -> typing.FrozenSet[str]: +def template_config_variables(template: str) -> FrozenSet[str]: """Return the config variables used in the template.""" unvisted_nodes = [environment.parse(template)] - result = set() # type: typing.Set[str] + result: Set[str] = set() while unvisted_nodes: node = unvisted_nodes.pop() if not isinstance(node, jinja2.nodes.Getattr): @@ -154,7 +154,7 @@ def template_config_variables(template: str) -> typing.FrozenSet[str]: # List of attribute names in reverse order. # For example it's ['ab', 'c', 'd'] for 'conf.d.c.ab'. - attrlist = [] # type: typing.List[str] + attrlist: List[str] = [] while isinstance(node, jinja2.nodes.Getattr): attrlist.append(node.attr) # type: ignore[attr-defined] node = node.node # type: ignore[attr-defined] diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 165e5143f..d8378788c 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -31,8 +31,9 @@ import traceback import warnings import json import inspect -import typing import argparse +from typing import (TYPE_CHECKING, Any, Iterator, Mapping, MutableSequence, + Optional, Set, Tuple, Union, cast) from PyQt5 import QtCore # Optional imports @@ -41,7 +42,7 @@ try: except ImportError: colorama = None -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from qutebrowser.config import config as configmodule _log_inited = False @@ -98,8 +99,8 @@ LOG_LEVELS = { def vdebug(self: logging.Logger, msg: str, - *args: typing.Any, - **kwargs: typing.Any) -> None: + *args: Any, + **kwargs: Any) -> None: """Log with a VDEBUG level. VDEBUG is used when a debug message is rather verbose, and probably of @@ -159,8 +160,8 @@ LOGGER_NAMES = [ ] -ram_handler = None # type: typing.Optional[RAMHandler] -console_handler = None # type: typing.Optional[logging.Handler] +ram_handler: Optional['RAMHandler'] = None +console_handler: Optional[logging.Handler] = None console_filter = None @@ -233,7 +234,7 @@ def _init_py_warnings() -> None: @contextlib.contextmanager -def disable_qt_msghandler() -> typing.Iterator[None]: +def disable_qt_msghandler() -> Iterator[None]: """Contextmanager which temporarily disables the Qt message handler.""" old_handler = QtCore.qInstallMessageHandler(None) try: @@ -243,9 +244,9 @@ def disable_qt_msghandler() -> typing.Iterator[None]: @contextlib.contextmanager -def ignore_py_warnings(**kwargs: typing.Any) -> typing.Iterator[None]: +def py_warning_filter(action: str = 'ignore', **kwargs: Any) -> Iterator[None]: """Contextmanager to temporarily disable certain Python warnings.""" - warnings.filterwarnings('ignore', **kwargs) + warnings.filterwarnings(action, **kwargs) yield if _log_inited: _init_py_warnings() @@ -257,7 +258,7 @@ def _init_handlers( force_color: bool, json_logging: bool, ram_capacity: int -) -> typing.Tuple[logging.StreamHandler, typing.Optional['RAMHandler']]: +) -> Tuple[logging.StreamHandler, Optional['RAMHandler']]: """Init log handlers. Args: @@ -311,8 +312,8 @@ def _init_formatters( color: bool, force_color: bool, json_logging: bool -) -> typing.Tuple[typing.Union['JSONFormatter', 'ColoredFormatter'], - 'ColoredFormatter', 'HTMLFormatter', bool]: +) -> Tuple[Union['JSONFormatter', 'ColoredFormatter'], + 'ColoredFormatter', 'HTMLFormatter', bool]: """Init log formatters. Args: @@ -364,7 +365,7 @@ def change_console_formatter(level: int) -> None: """ assert console_handler is not None - old_formatter = typing.cast(ColoredFormatter, console_handler.formatter) + old_formatter = cast(ColoredFormatter, console_handler.formatter) console_fmt = get_console_format(level) console_formatter = ColoredFormatter(console_fmt, DATEFMT, '{', use_colors=old_formatter.use_colors) @@ -504,7 +505,7 @@ def qt_message_handler(msg_type: QtCore.QtMsgType, assert _args is not None if _args.debug: - stack = ''.join(traceback.format_stack()) # type: typing.Optional[str] + stack: Optional[str] = ''.join(traceback.format_stack()) else: stack = None @@ -515,7 +516,7 @@ def qt_message_handler(msg_type: QtCore.QtMsgType, @contextlib.contextmanager -def hide_qt_warning(pattern: str, logger: str = 'qt') -> typing.Iterator[None]: +def hide_qt_warning(pattern: str, logger: str = 'qt') -> Iterator[None]: """Hide Qt warnings matching the given regex.""" log_filter = QtWarningFilter(pattern) logger_obj = logging.getLogger(logger) @@ -578,7 +579,7 @@ class InvalidLogFilterError(Exception): """Raised when an invalid filter string is passed to LogFilter.parse().""" - def __init__(self, names: typing.Set[str]): + def __init__(self, names: Set[str]): invalid = names - set(LOGGER_NAMES) super().__init__("Invalid log category {} - valid categories: {}" .format(', '.join(sorted(invalid)), @@ -599,7 +600,7 @@ class LogFilter(logging.Filter): than debug. """ - def __init__(self, names: typing.Set[str], *, negated: bool = False, + def __init__(self, names: Set[str], *, negated: bool = False, only_debug: bool = True) -> None: super().__init__() self.names = names @@ -607,7 +608,7 @@ class LogFilter(logging.Filter): self.only_debug = only_debug @classmethod - def parse(cls, filter_str: typing.Optional[str], *, + def parse(cls, filter_str: Optional[str], *, only_debug: bool = True) -> 'LogFilter': """Parse a log filter from a string.""" if filter_str is None or filter_str == 'none': @@ -661,11 +662,11 @@ class RAMHandler(logging.Handler): def __init__(self, capacity: int) -> None: super().__init__() - self.html_formatter = None # type: typing.Optional[HTMLFormatter] + self.html_formatter: Optional[HTMLFormatter] = None if capacity != -1: - self._data = collections.deque( + self._data: MutableSequence[logging.LogRecord] = collections.deque( maxlen=capacity - ) # type: typing.MutableSequence[logging.LogRecord] + ) else: self._data = collections.deque() @@ -748,9 +749,7 @@ class HTMLFormatter(logging.Formatter): _colordict: The colordict passed to the logger. """ - def __init__(self, fmt: str, - datefmt: str, - log_colors: typing.Mapping[str, str]) -> None: + def __init__(self, fmt: str, datefmt: str, log_colors: Mapping[str, str]) -> None: """Constructor. Args: @@ -759,8 +758,8 @@ class HTMLFormatter(logging.Formatter): log_colors: The colors to use for logging levels. """ super().__init__(fmt, datefmt) - self._log_colors = log_colors # type: typing.Mapping[str, str] - self._colordict = {} # type: typing.Mapping[str, str] + self._log_colors: Mapping[str, str] = log_colors + self._colordict: Mapping[str, str] = {} # We could solve this nicer by using CSS, but for this simple case this # works. for color in COLORS: diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py index 2754d87e7..1009f1e98 100644 --- a/qutebrowser/utils/message.py +++ b/qutebrowser/utils/message.py @@ -23,11 +23,11 @@ """Message singleton so we don't have to define unneeded signals.""" import traceback -import typing +from typing import Any, Callable, Iterable, List, Tuple, Union -from PyQt5.QtCore import pyqtSignal, QObject +from PyQt5.QtCore import pyqtSignal, pyqtBoundSignal, QObject -from qutebrowser.utils import usertypes, log, utils +from qutebrowser.utils import usertypes, log def _log_stack(typ: str, stack: str) -> None: @@ -86,8 +86,8 @@ def info(message: str, *, replace: bool = False) -> None: def _build_question(title: str, text: str = None, *, mode: usertypes.PromptMode, - default: typing.Union[None, bool, str] = None, - abort_on: typing.Iterable[pyqtSignal] = (), + default: Union[None, bool, str] = None, + abort_on: Iterable[pyqtBoundSignal] = (), url: str = None, option: bool = None) -> usertypes.Question: """Common function for ask/ask_async.""" @@ -110,7 +110,7 @@ def _build_question(title: str, return question -def ask(*args: typing.Any, **kwargs: typing.Any) -> typing.Any: +def ask(*args: Any, **kwargs: Any) -> Any: """Ask a modular question in the statusbar (blocking). Args: @@ -134,8 +134,8 @@ def ask(*args: typing.Any, **kwargs: typing.Any) -> typing.Any: def ask_async(title: str, mode: usertypes.PromptMode, - handler: typing.Callable[[typing.Any], None], - **kwargs: typing.Any) -> None: + handler: Callable[[Any], None], + **kwargs: Any) -> None: """Ask an async question in the statusbar. Args: @@ -151,13 +151,13 @@ def ask_async(title: str, global_bridge.ask(question, blocking=False) -_ActionType = typing.Callable[[], typing.Any] +_ActionType = Callable[[], Any] def confirm_async(*, yes_action: _ActionType, no_action: _ActionType = None, cancel_action: _ActionType = None, - **kwargs: typing.Any) -> usertypes.Question: + **kwargs: Any) -> usertypes.Question: """Ask a yes/no question to the user and execute the given actions. Args: @@ -219,8 +219,7 @@ class GlobalMessageBridge(QObject): def __init__(self, parent: QObject = None) -> None: super().__init__(parent) self._connected = False - self._cache = [ - ] # type: typing.List[typing.Tuple[usertypes.MessageLevel, str, bool]] + self._cache: List[Tuple[usertypes.MessageLevel, str, bool]] = [] def ask(self, question: usertypes.Question, blocking: bool, *, @@ -259,42 +258,4 @@ class GlobalMessageBridge(QObject): self._cache = [] -class MessageBridge(QObject): - - """Bridge for messages to be shown in the statusbar. - - Signals: - s_set_text: Set a persistent text in the statusbar. - arg: The text to set. - s_maybe_reset_text: Reset the text if it hasn't been changed yet. - arg: The expected text. - """ - - s_set_text = pyqtSignal(str) - s_maybe_reset_text = pyqtSignal(str) - - def __repr__(self) -> str: - return utils.get_repr(self) - - def set_text(self, text: str, *, log_stack: bool = False) -> None: - """Set the normal text of the statusbar. - - Args: - text: The text to set. - log_stack: ignored - """ - text = str(text) - log.message.debug(text) - self.s_set_text.emit(text) - - def maybe_reset_text(self, text: str, *, log_stack: bool = False) -> None: - """Reset the text in the statusbar if it matches an expected text. - - Args: - text: The expected text. - log_stack: ignored - """ - self.s_maybe_reset_text.emit(str(text)) - - global_bridge = GlobalMessageBridge() diff --git a/qutebrowser/utils/objreg.py b/qutebrowser/utils/objreg.py index 015334990..90b70be17 100644 --- a/qutebrowser/utils/objreg.py +++ b/qutebrowser/utils/objreg.py @@ -22,18 +22,19 @@ import collections import functools -import typing +from typing import (TYPE_CHECKING, Any, Callable, MutableMapping, MutableSequence, + Optional, Sequence, Union) from PyQt5.QtCore import QObject, QTimer from PyQt5.QtWidgets import QApplication -from PyQt5.QtWidgets import QWidget # pylint: disable=unused-import +from PyQt5.QtWidgets import QWidget -from qutebrowser.utils import log, usertypes -if typing.TYPE_CHECKING: +from qutebrowser.utils import log, usertypes, utils +if TYPE_CHECKING: from qutebrowser.mainwindow import mainwindow -_WindowTab = typing.Union[str, int, None] +_WindowTab = Union[str, int, None] class RegistryUnavailableError(Exception): @@ -51,7 +52,7 @@ class CommandOnlyError(Exception): """Raised when an object is requested which is used for commands only.""" -_IndexType = typing.Union[str, int] +_IndexType = Union[str, int] class ObjectRegistry(collections.UserDict): @@ -67,11 +68,10 @@ class ObjectRegistry(collections.UserDict): def __init__(self) -> None: super().__init__() - self._partial_objs = { - } # type: typing.MutableMapping[_IndexType, typing.Callable[[], None]] - self.command_only = [] # type: typing.MutableSequence[str] + self._partial_objs: MutableMapping[_IndexType, Callable[[], None]] = {} + self.command_only: MutableSequence[str] = [] - def __setitem__(self, name: _IndexType, obj: typing.Any) -> None: + def __setitem__(self, name: _IndexType, obj: Any) -> None: """Register an object in the object registry. Sets a slot to remove QObjects when they are destroyed. @@ -139,7 +139,7 @@ class ObjectRegistry(collections.UserDict): except KeyError: pass - def dump_objects(self) -> typing.Sequence[str]: + def dump_objects(self) -> Sequence[str]: """Dump all objects as a list of strings.""" lines = [] for name, obj in self.data.items(): @@ -166,7 +166,7 @@ def _get_tab_registry(win_id: _WindowTab, if tab_id is None: raise ValueError("Got tab_id None (win_id {})".format(win_id)) if tab_id == 'current' and win_id is None: - window = QApplication.activeWindow() # type: typing.Optional[QWidget] + window: Optional[QWidget] = QApplication.activeWindow() if window is None or not hasattr(window, 'win_id'): raise RegistryUnavailableError('tab') win_id = window.win_id @@ -192,7 +192,7 @@ def _get_window_registry(window: _WindowTab) -> ObjectRegistry: raise TypeError("window is None with scope window!") try: if window == 'current': - win = QApplication.activeWindow() # type: typing.Optional[QWidget] + win: Optional[QWidget] = QApplication.activeWindow() elif window == 'last-focused': win = last_focused_window() else: @@ -228,11 +228,11 @@ def _get_registry(scope: str, def get(name: str, - default: typing.Any = usertypes.UNSET, + default: Any = usertypes.UNSET, scope: str = 'global', window: _WindowTab = None, tab: _WindowTab = None, - from_command: bool = False) -> typing.Any: + from_command: bool = False) -> Any: """Helper function to get an object. Args: @@ -253,7 +253,7 @@ def get(name: str, def register(name: str, - obj: typing.Any, + obj: Any, update: bool = False, scope: str = None, registry: ObjectRegistry = None, @@ -296,7 +296,7 @@ def delete(name: str, del reg[name] -def dump_objects() -> typing.Sequence[str]: +def dump_objects() -> Sequence[str]: """Get all registered objects in all registries as a string.""" blocks = [] lines = [] @@ -321,22 +321,46 @@ def dump_objects() -> typing.Sequence[str]: def last_visible_window() -> 'mainwindow.MainWindow': """Get the last visible window, or the last focused window if none.""" try: - return get('last-visible-main-window') + window = get('last-visible-main-window') except KeyError: return last_focused_window() + if window.tabbed_browser.is_shutting_down: + return last_focused_window() + return window def last_focused_window() -> 'mainwindow.MainWindow': """Get the last focused window, or the last window if none.""" try: - return get('last-focused-main-window') + window = get('last-focused-main-window') except KeyError: - return window_by_index(-1) + return last_opened_window() + if window.tabbed_browser.is_shutting_down: + return last_opened_window() + return window -def window_by_index(idx: int) -> 'mainwindow.MainWindow': +def _window_by_index(idx: int) -> 'mainwindow.MainWindow': """Get the Nth opened window object.""" if not window_registry: raise NoWindow() key = sorted(window_registry)[idx] return window_registry[key] + + +def last_opened_window() -> 'mainwindow.MainWindow': + """Get the last opened window object.""" + for idx in range(-1, -(len(window_registry)+1), -1): + window = _window_by_index(idx) + if not window.tabbed_browser.is_shutting_down: + return window + raise utils.Unreachable() + + +def first_opened_window() -> 'mainwindow.MainWindow': + """Get the first opened window object.""" + for idx in range(0, len(window_registry)+1): + window = _window_by_index(idx) + if not window.tabbed_browser.is_shutting_down: + return window + raise utils.Unreachable() diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index e853b38f8..c92827f99 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -31,7 +31,7 @@ Module attributes: import io import operator import contextlib -import typing +from typing import TYPE_CHECKING, BinaryIO, IO, Iterator, Optional, Union, cast import pkg_resources from PyQt5.QtCore import (qVersion, QEventLoop, QDataStream, QByteArray, @@ -43,6 +43,9 @@ try: from PyQt5.QtWebKit import qWebKitVersion except ImportError: # pragma: no cover qWebKitVersion = None # type: ignore[assignment] # noqa: N816 +if TYPE_CHECKING: + from PyQt5.QtWebKit import QWebHistory + from PyQt5.QtWebEngineWidgets import QWebEngineHistory from qutebrowser.misc import objects from qutebrowser.utils import usertypes @@ -71,7 +74,7 @@ class QtOSError(OSError): if msg is None: msg = dev.errorString() - self.qt_errno = None # type: typing.Optional[QFileDevice.FileError] + self.qt_errno: Optional[QFileDevice.FileError] = None if isinstance(dev, QFileDevice): msg = self._init_filedev(dev, msg) @@ -155,8 +158,11 @@ def check_overflow(arg: int, ctype: str, fatal: bool = True) -> int: return arg -if typing.TYPE_CHECKING: - class Validatable(typing.Protocol): +if TYPE_CHECKING: + # Protocol was added in Python 3.8 + from typing import Protocol + + class Validatable(Protocol): """An object with an isValid() method (e.g. QUrl).""" @@ -184,7 +190,13 @@ def check_qdatastream(stream: QDataStream) -> None: raise OSError(status_to_str[stream.status()]) -_QtSerializableType = typing.Union[QObject, QByteArray, QUrl] +_QtSerializableType = Union[ + QObject, + QByteArray, + QUrl, + 'QWebEngineHistory', + 'QWebHistory' +] def serialize(obj: _QtSerializableType) -> QByteArray: @@ -222,7 +234,7 @@ def savefile_open( filename: str, binary: bool = False, encoding: str = 'utf-8' -) -> typing.Iterator[typing.IO]: +) -> Iterator[IO]: """Context manager to easily use a QSaveFile.""" f = QSaveFile(filename) cancelled = False @@ -231,10 +243,10 @@ def savefile_open( if not open_ok: raise QtOSError(f) - dev = typing.cast(typing.BinaryIO, PyQIODevice(f)) + dev = cast(BinaryIO, PyQIODevice(f)) if binary: - new_f = dev # type: typing.IO + new_f: IO = dev else: new_f = io.TextIOWrapper(dev, encoding=encoding) @@ -352,21 +364,21 @@ class PyQIODevice(io.BufferedIOBase): def readable(self) -> bool: return self.dev.isReadable() - def readline(self, size: int = -1) -> bytes: + def readline(self, size: Optional[int] = -1) -> bytes: self._check_open() self._check_readable() - if size < 0: + if size is None or size < 0: qt_size = 0 # no maximum size elif size == 0: return b'' else: qt_size = size + 1 # Qt also counts the NUL byte - buf = None # type: typing.Union[QByteArray, bytes, None] + buf: Union[QByteArray, bytes, None] = None if self.dev.canReadLine(): buf = self.dev.readLine(qt_size) - elif size < 0: + elif size is None or size < 0: buf = self.dev.readAll() else: buf = self.dev.read(size) @@ -392,7 +404,10 @@ class PyQIODevice(io.BufferedIOBase): def writable(self) -> bool: return self.dev.isWritable() - def write(self, data: typing.Union[bytes, bytearray]) -> int: + def write( # type: ignore[override] + self, + data: Union[bytes, bytearray] + ) -> int: self._check_open() self._check_writable() num = self.dev.write(data) @@ -400,11 +415,11 @@ class PyQIODevice(io.BufferedIOBase): raise QtOSError(self.dev) return num - def read(self, size: typing.Optional[int] = None) -> bytes: + def read(self, size: Optional[int] = None) -> bytes: self._check_open() self._check_readable() - buf = None # type: typing.Union[QByteArray, bytes, None] + buf: Union[QByteArray, bytes, None] = None if size in [None, -1]: buf = self.dev.readAll() else: @@ -451,7 +466,7 @@ class EventLoop(QEventLoop): def exec_( self, flags: QEventLoop.ProcessEventsFlags = - typing.cast(QEventLoop.ProcessEventsFlags, QEventLoop.AllEvents) + cast(QEventLoop.ProcessEventsFlags, QEventLoop.AllEvents) ) -> int: """Override exec_ to raise an exception when re-running.""" if self._executing: diff --git a/qutebrowser/utils/standarddir.py b/qutebrowser/utils/standarddir.py index 8e5a91c30..a86fd9bdc 100644 --- a/qutebrowser/utils/standarddir.py +++ b/qutebrowser/utils/standarddir.py @@ -26,7 +26,7 @@ import shutil import contextlib import enum import argparse -import typing +from typing import Iterator, Optional from PyQt5.QtCore import QStandardPaths from PyQt5.QtWidgets import QApplication @@ -41,14 +41,14 @@ class _Location(enum.Enum): """A key for _locations.""" - config = 1 - auto_config = 2 - data = 3 - system_data = 4 - cache = 5 - download = 6 - runtime = 7 - config_py = 8 + config = enum.auto() + auto_config = enum.auto() + data = enum.auto() + system_data = enum.auto() + cache = enum.auto() + download = enum.auto() + runtime = enum.auto() + config_py = enum.auto() APPNAME = 'qutebrowser' @@ -60,7 +60,7 @@ class EmptyValueError(Exception): @contextlib.contextmanager -def _unset_organization() -> typing.Iterator[None]: +def _unset_organization() -> Iterator[None]: """Temporarily unset QApplication.organizationName(). This is primarily needed in config.py. @@ -76,7 +76,7 @@ def _unset_organization() -> typing.Iterator[None]: qapp.setOrganizationName(orgname) -def _init_config(args: typing.Optional[argparse.Namespace]) -> None: +def _init_config(args: Optional[argparse.Namespace]) -> None: """Initialize the location for configs.""" typ = QStandardPaths.ConfigLocation path = _from_args(typ, args) @@ -127,7 +127,7 @@ def config_py() -> str: return _locations[_Location.config_py] -def _init_data(args: typing.Optional[argparse.Namespace]) -> None: +def _init_data(args: Optional[argparse.Namespace]) -> None: """Initialize the location for data.""" typ = QStandardPaths.DataLocation path = _from_args(typ, args) @@ -167,7 +167,7 @@ def data(system: bool = False) -> str: return _locations[_Location.data] -def _init_cache(args: typing.Optional[argparse.Namespace]) -> None: +def _init_cache(args: Optional[argparse.Namespace]) -> None: """Initialize the location for the cache.""" typ = QStandardPaths.CacheLocation path = _from_args(typ, args) @@ -187,7 +187,7 @@ def cache() -> str: return _locations[_Location.cache] -def _init_download(args: typing.Optional[argparse.Namespace]) -> None: +def _init_download(args: Optional[argparse.Namespace]) -> None: """Initialize the location for downloads. Note this is only the default directory as found by Qt. @@ -204,7 +204,7 @@ def download() -> str: return _locations[_Location.download] -def _init_runtime(args: typing.Optional[argparse.Namespace]) -> None: +def _init_runtime(args: Optional[argparse.Namespace]) -> None: """Initialize location for runtime data.""" if utils.is_mac or utils.is_windows: # RuntimeLocation is a weird path on macOS and Windows. @@ -279,8 +279,8 @@ def _writable_location(typ: QStandardPaths.StandardLocation) -> str: def _from_args( typ: QStandardPaths.StandardLocation, - args: typing.Optional[argparse.Namespace] -) -> typing.Optional[str]: + args: Optional[argparse.Namespace] +) -> Optional[str]: """Get the standard directory from an argparse namespace. Return: @@ -332,7 +332,7 @@ def _init_dirs(args: argparse.Namespace = None) -> None: _init_runtime(args) -def init(args: typing.Optional[argparse.Namespace]) -> None: +def init(args: Optional[argparse.Namespace]) -> None: """Initialize all standard dirs.""" if args is not None: # args can be None during tests diff --git a/qutebrowser/utils/urlmatch.py b/qutebrowser/utils/urlmatch.py index 503436ef8..e4e4984db 100644 --- a/qutebrowser/utils/urlmatch.py +++ b/qutebrowser/utils/urlmatch.py @@ -25,14 +25,14 @@ https://cs.chromium.org/chromium/src/extensions/common/url_pattern.cc https://cs.chromium.org/chromium/src/extensions/common/url_pattern.h Based on the following commit in Chromium: -https://chromium.googlesource.com/chromium/src/+/757854e199e159523e7789de5cb2f6ba49b79b63 -(February 4 2020, newest commit as per July 1st 2020) +https://chromium.googlesource.com/chromium/src/+/6f4a6681eae01c2036336c18b06303e16a304a7c +(October 10 2020, newest commit as per October 28th 2020) """ import ipaddress import fnmatch -import typing import urllib.parse +from typing import Any, Optional, Tuple from PyQt5.QtCore import QUrl @@ -73,11 +73,11 @@ class UrlPattern: # Make sure all attributes are initialized if we exit early. self._pattern = pattern self._match_all = False - self._match_subdomains = False # type: bool - self._scheme = None # type: typing.Optional[str] - self.host = None # type: typing.Optional[str] - self._path = None # type: typing.Optional[str] - self._port = None # type: typing.Optional[int] + self._match_subdomains: bool = False + self._scheme: Optional[str] = None + self.host: Optional[str] = None + self._path: Optional[str] = None + self._port: Optional[int] = None # > The special pattern <all_urls> matches any URL that starts with a # > permitted scheme. @@ -104,7 +104,7 @@ class UrlPattern: self._init_path(parsed) self._init_port(parsed) - def _to_tuple(self) -> typing.Tuple: + def _to_tuple(self) -> Tuple: """Get a pattern with information used for __eq__/__hash__.""" return (self._match_all, self._match_subdomains, self._scheme, self.host, self._path, self._port) @@ -112,7 +112,7 @@ class UrlPattern: def __hash__(self) -> int: return hash(self._to_tuple()) - def __eq__(self, other: typing.Any) -> bool: + def __eq__(self, other: Any) -> bool: if not isinstance(other, UrlPattern): return NotImplemented return self._to_tuple() == other._to_tuple() diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index a14be78a8..598210010 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -25,7 +25,7 @@ import os.path import ipaddress import posixpath import urllib.parse -import typing +from typing import Optional, Tuple, Union from PyQt5.QtCore import QUrl, QUrlQuery from PyQt5.QtNetwork import QHostInfo, QHostAddress, QNetworkProxy @@ -72,8 +72,7 @@ class InvalidUrlError(Error): super().__init__(self.msg) -def _parse_search_term(s: str) -> typing.Tuple[typing.Optional[str], - typing.Optional[str]]: +def _parse_search_term(s: str) -> Tuple[Optional[str], Optional[str]]: """Get a search engine name and search term from a string. Args: @@ -89,8 +88,8 @@ def _parse_search_term(s: str) -> typing.Tuple[typing.Optional[str], if len(split) == 2: if split[0] in config.val.url.searchengines: - engine = split[0] # type: typing.Optional[str] - term = split[1] # type: typing.Optional[str] + engine: Optional[str] = split[0] + term: Optional[str] = split[1] else: engine = None term = s @@ -385,7 +384,7 @@ def raise_cmdexc_if_invalid(url: QUrl) -> None: def get_path_if_valid(pathstr: str, cwd: str = None, relative: bool = False, - check_exists: bool = False) -> typing.Optional[str]: + check_exists: bool = False) -> Optional[str]: """Check if path is a valid path. Args: @@ -403,7 +402,7 @@ def get_path_if_valid(pathstr: str, expanded = os.path.expanduser(pathstr) if os.path.isabs(expanded): - path = expanded # type: typing.Optional[str] + path: Optional[str] = expanded elif relative and cwd: path = os.path.join(cwd, expanded) elif relative: @@ -430,7 +429,7 @@ def get_path_if_valid(pathstr: str, return path -def filename_from_url(url: QUrl) -> typing.Optional[str]: +def filename_from_url(url: QUrl) -> Optional[str]: """Get a suitable filename from a URL. Args: @@ -450,7 +449,7 @@ def filename_from_url(url: QUrl) -> typing.Optional[str]: return None -HostTupleType = typing.Tuple[str, str, int] +HostTupleType = Tuple[str, str, int] def host_tuple(url: QUrl) -> HostTupleType: @@ -594,7 +593,7 @@ class InvalidProxyTypeError(Exception): super().__init__("Invalid proxy type {}!".format(typ)) -def proxy_from_url(url: QUrl) -> typing.Union[QNetworkProxy, pac.PACFetcher]: +def proxy_from_url(url: QUrl) -> Union[QNetworkProxy, pac.PACFetcher]: """Create a QNetworkProxy from QUrl and a proxy type. Args: diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index 0b6f9c219..9ecae9e92 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -21,7 +21,7 @@ import operator import enum -import typing +from typing import TYPE_CHECKING, Any, Optional, Sequence, TypeVar, Union import attr from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer @@ -30,7 +30,19 @@ from PyQt5.QtCore import QUrl from qutebrowser.utils import log, qtutils, utils -_T = typing.TypeVar('_T') +if TYPE_CHECKING: + # Protocol was added in Python 3.8 + from typing import Protocol + + class SupportsLessThan(Protocol): + + """Protocol for the _T TypeVar below.""" + + def __lt__(self, other: Any) -> bool: + ... + + +_T = TypeVar('_T', bound='SupportsLessThan') class Unset: @@ -46,7 +58,7 @@ class Unset: UNSET = Unset() -class NeighborList(typing.Sequence[_T]): +class NeighborList(Sequence[_T]): """A list of items which saves its current position. @@ -60,10 +72,15 @@ class NeighborList(typing.Sequence[_T]): _mode: The current mode. """ - Modes = enum.Enum('Modes', ['edge', 'exception']) + class Modes(enum.Enum): + + """Behavior for the 'mode' argument.""" + + edge = enum.auto() + exception = enum.auto() - def __init__(self, items: typing.Sequence[_T] = None, - default: typing.Union[_T, Unset] = UNSET, + def __init__(self, items: Sequence[_T] = None, + default: Union[_T, Unset] = UNSET, mode: Modes = Modes.exception) -> None: """Constructor. @@ -77,19 +94,19 @@ class NeighborList(typing.Sequence[_T]): if not isinstance(mode, self.Modes): raise TypeError("Mode {} is not a Modes member!".format(mode)) if items is None: - self._items = [] # type: typing.Sequence[_T] + self._items: Sequence[_T] = [] else: self._items = list(items) self._default = default if not isinstance(default, Unset): idx = self._items.index(default) - self._idx = idx # type: typing.Optional[int] + self._idx: Optional[int] = idx else: self._idx = None self._mode = mode - self.fuzzyval = None # type: typing.Optional[int] + self.fuzzyval: Optional[int] = None def __getitem__(self, key: int) -> _T: # type: ignore[override] return self._items[key] @@ -158,7 +175,7 @@ class NeighborList(typing.Sequence[_T]): return new @property - def items(self) -> typing.Sequence[_T]: + def items(self) -> Sequence[_T]: """Getter for items, which should not be set.""" return self._items @@ -224,42 +241,48 @@ class NeighborList(typing.Sequence[_T]): return self.curitem() -# The mode of a Question. -PromptMode = enum.Enum('PromptMode', ['yesno', 'text', 'user_pwd', 'alert', - 'download']) +class PromptMode(enum.Enum): + + """The mode of a Question.""" + + yesno = enum.auto() + text = enum.auto() + user_pwd = enum.auto() + alert = enum.auto() + download = enum.auto() class ClickTarget(enum.Enum): """How to open a clicked link.""" - normal = 0 #: Open the link in the current tab - tab = 1 #: Open the link in a new foreground tab - tab_bg = 2 #: Open the link in a new background tab - window = 3 #: Open the link in a new window - hover = 4 #: Only hover over the link + normal = enum.auto() #: Open the link in the current tab + tab = enum.auto() #: Open the link in a new foreground tab + tab_bg = enum.auto() #: Open the link in a new background tab + window = enum.auto() #: Open the link in a new window + hover = enum.auto() #: Only hover over the link class KeyMode(enum.Enum): """Key input modes.""" - normal = 1 #: Normal mode (no mode was entered) - hint = 2 #: Hint mode (showing labels for links) - command = 3 #: Command mode (after pressing the colon key) - yesno = 4 #: Yes/No prompts - prompt = 5 #: Text prompts - insert = 6 #: Insert mode (passing through most keys) - passthrough = 7 #: Passthrough mode (passing through all keys) - caret = 8 #: Caret mode (moving cursor with keys) - set_mark = 9 - jump_mark = 10 - record_macro = 11 - run_macro = 12 + normal = enum.auto() #: Normal mode (no mode was entered) + hint = enum.auto() #: Hint mode (showing labels for links) + command = enum.auto() #: Command mode (after pressing the colon key) + yesno = enum.auto() #: Yes/No prompts + prompt = enum.auto() #: Text prompts + insert = enum.auto() #: Insert mode (passing through most keys) + passthrough = enum.auto() #: Passthrough mode (passing through all keys) + caret = enum.auto() #: Caret mode (moving cursor with keys) + set_mark = enum.auto() + jump_mark = enum.auto() + record_macro = enum.auto() + run_macro = enum.auto() # 'register' is a bit of an oddball here: It's not really a "real" mode, # but it's used in the config for common bindings for # set_mark/jump_mark/record_macro/run_macro. - register = 13 + register = enum.auto() class Exit(enum.IntEnum): @@ -273,44 +296,76 @@ class Exit(enum.IntEnum): err_init = 4 -# Load status of a tab -LoadStatus = enum.Enum('LoadStatus', ['none', 'success', 'success_https', - 'error', 'warn', 'loading']) +class LoadStatus(enum.Enum): + + """Load status of a tab.""" + + none = enum.auto() + success = enum.auto() + success_https = enum.auto() + error = enum.auto() + warn = enum.auto() + loading = enum.auto() + + +class Backend(enum.Enum): + """The backend being used (usertypes.backend).""" -# Backend of a tab -Backend = enum.Enum('Backend', ['QtWebKit', 'QtWebEngine']) + QtWebKit = enum.auto() + QtWebEngine = enum.auto() class JsWorld(enum.Enum): """World/context to run JavaScript code in.""" - main = 1 #: Same world as the web page's JavaScript. - application = 2 #: Application world, used by qutebrowser internally. - user = 3 #: User world, currently not used. - jseval = 4 #: World used for the jseval-command. + main = enum.auto() #: Same world as the web page's JavaScript. + application = enum.auto() #: Application world, used by qutebrowser internally. + user = enum.auto() #: User world, currently not used. + jseval = enum.auto() #: World used for the jseval-command. -# Log level of a JS message. This needs to match up with the keys allowed for -# the content.javascript.log setting. -JsLogLevel = enum.Enum('JsLogLevel', ['unknown', 'info', 'warning', 'error']) +class JsLogLevel(enum.Enum): + """Log level of a JS message. -MessageLevel = enum.Enum('MessageLevel', ['error', 'warning', 'info']) + This needs to match up with the keys allowed for the + content.javascript.log setting. + """ + + unknown = enum.auto() + info = enum.auto() + warning = enum.auto() + error = enum.auto() + + +class MessageLevel(enum.Enum): + + """The level of a message being shown.""" + + error = enum.auto() + warning = enum.auto() + info = enum.auto() -IgnoreCase = enum.Enum('IgnoreCase', ['smart', 'never', 'always']) +class IgnoreCase(enum.Enum): + + """Possible values for the 'search.ignore_case' setting.""" + + smart = enum.auto() + never = enum.auto() + always = enum.auto() class CommandValue(enum.Enum): """Special values which are injected when running a command handler.""" - count = 1 - win_id = 2 - cur_tab = 3 - count_tab = 4 + count = enum.auto() + win_id = enum.auto() + cur_tab = enum.auto() + count_tab = enum.auto() class Question(QObject): @@ -360,13 +415,13 @@ class Question(QObject): def __init__(self, parent: QObject = None) -> None: super().__init__(parent) - self.mode = None # type: typing.Optional[PromptMode] - self.default = None # type: typing.Union[bool, str, None] - self.title = None # type: typing.Optional[str] - self.text = None # type: typing.Optional[str] - self.url = None # type: typing.Optional[str] - self.option = None # type: typing.Optional[bool] - self.answer = None # type: typing.Union[str, bool, None] + self.mode: Optional[PromptMode] = None + self.default: Union[bool, str, None] = None + self.title: Optional[str] = None + self.text: Optional[str] = None + self.url: Optional[str] = None + self.option: Optional[bool] = None + self.answer: Union[str, bool, None] = None self.is_aborted = False self.interrupted = False @@ -440,7 +495,7 @@ class AbstractCertificateErrorWrapper: """A wrapper over an SSL/certificate error.""" - def __init__(self, error: typing.Any) -> None: + def __init__(self, error: Any) -> None: self._error = error def __str__(self) -> str: @@ -458,18 +513,32 @@ class NavigationRequest: """A request to navigate to the given URL.""" - Type = enum.Enum('Type', [ - 'link_clicked', - 'typed', # QtWebEngine only - 'form_submitted', - 'form_resubmitted', # QtWebKit only - 'back_forward', - 'reloaded', - 'redirect', # QtWebEngine >= 5.14 only - 'other' - ]) - - url = attr.ib() # type: QUrl - navigation_type = attr.ib() # type: Type - is_main_frame = attr.ib() # type: bool - accepted = attr.ib(default=True) # type: bool + class Type(enum.Enum): + + """The type of a request. + + Based on QWebEngineUrlRequestInfo::NavigationType and QWebPage::NavigationType. + """ + + #: Navigation initiated by clicking a link. + link_clicked = 1 + #: Navigation explicitly initiated by typing a URL (QtWebEngine only). + typed = 2 + #: Navigation submits a form. + form_submitted = 3 + #: An HTML form was submitted a second time (QtWebKit only). + form_resubmitted = 4 + #: Navigation initiated by a history action. + back_forward = 5 + #: Navigation initiated by refreshing the page. + reloaded = 6 + #: Navigation triggered automatically by page content or remote server + #: (QtWebEngine >= 5.14 only) + redirect = 7 + #: None of the above. + other = 8 + + url: QUrl = attr.ib() + navigation_type: Type = attr.ib() + is_main_frame: bool = attr.ib() + accepted: bool = attr.ib(default=True) diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 0bbba9a4f..6952acf97 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -35,9 +35,9 @@ import socket import shlex import glob import mimetypes -import typing import ctypes import ctypes.util +from typing import Any, Callable, IO, Iterator, Optional, Sequence, Tuple, Type, Union from PyQt5.QtCore import QUrl from PyQt5.QtGui import QColor, QClipboard, QDesktopServices @@ -159,7 +159,7 @@ def preload_resources() -> None: # FIXME:typing Return value should be bytes/str -def read_file(filename: str, binary: bool = False) -> typing.Any: +def read_file(filename: str, binary: bool = False) -> Any: """Get the contents of a file contained with qutebrowser. Args: @@ -181,7 +181,7 @@ def read_file(filename: str, binary: bool = False) -> typing.Any: # https://github.com/pyinstaller/pyinstaller/wiki/FAQ#misc fn = os.path.join(os.path.dirname(sys.executable), filename) if binary: - with open(fn, 'rb') as f: # type: typing.IO + with open(fn, 'rb') as f: # type: IO return f.read() else: with open(fn, 'r', encoding='utf-8') as f: @@ -212,7 +212,7 @@ def resource_filename(filename: str) -> str: def _get_color_percentage(a_c1: int, a_c2: int, a_c3: int, b_c1: int, b_c2: int, b_c3: int, - percent: int) -> typing.Tuple[int, int, int]: + percent: int) -> Tuple[int, int, int]: """Get a color which is percent% interpolated between start and end. Args: @@ -237,7 +237,7 @@ def interpolate_color( start: QColor, end: QColor, percent: int, - colorspace: typing.Optional[QColor.Spec] = QColor.Rgb + colorspace: Optional[QColor.Spec] = QColor.Rgb ) -> QColor: """Get an interpolated color value. @@ -303,9 +303,7 @@ def format_seconds(total_seconds: int) -> str: return prefix + ':'.join(chunks) -def format_size(size: typing.Optional[float], - base: int = 1024, - suffix: str = '') -> str: +def format_size(size: Optional[float], base: int = 1024, suffix: str = '') -> str: """Format a byte size so it's human readable. Inspired by http://stackoverflow.com/q/1094841 @@ -324,13 +322,13 @@ class FakeIOStream(io.TextIOBase): """A fake file-like stream which calls a function for write-calls.""" - def __init__(self, write_func: typing.Callable[[str], int]) -> None: + def __init__(self, write_func: Callable[[str], int]) -> None: super().__init__() self.write = write_func # type: ignore[assignment] @contextlib.contextmanager -def fake_io(write_func: typing.Callable[[str], int]) -> typing.Iterator[None]: +def fake_io(write_func: Callable[[str], int]) -> Iterator[None]: """Run code with stdout and stderr replaced by FakeIOStreams. Args: @@ -354,7 +352,7 @@ def fake_io(write_func: typing.Callable[[str], int]) -> typing.Iterator[None]: @contextlib.contextmanager -def disabled_excepthook() -> typing.Iterator[None]: +def disabled_excepthook() -> Iterator[None]: """Run code with the exception hook temporarily disabled.""" old_excepthook = sys.excepthook sys.excepthook = sys.__excepthook__ @@ -387,7 +385,7 @@ class prevent_exceptions: # noqa: N801,N806 pylint: disable=invalid-name _predicate: The condition which needs to be True to prevent exceptions """ - def __init__(self, retval: typing.Any, predicate: bool = True) -> None: + def __init__(self, retval: Any, predicate: bool = True) -> None: """Save decorator arguments. Gets called on parse-time with the decorator arguments. @@ -398,7 +396,7 @@ class prevent_exceptions: # noqa: N801,N806 pylint: disable=invalid-name self._retval = retval self._predicate = predicate - def __call__(self, func: typing.Callable) -> typing.Callable: + def __call__(self, func: Callable) -> Callable: """Called when a function should be decorated. Args: @@ -413,7 +411,7 @@ class prevent_exceptions: # noqa: N801,N806 pylint: disable=invalid-name retval = self._retval @functools.wraps(func) - def wrapper(*args: typing.Any, **kwargs: typing.Any) -> typing.Any: + def wrapper(*args: Any, **kwargs: Any) -> Any: """Call the original function.""" try: return func(*args, **kwargs) @@ -424,7 +422,7 @@ class prevent_exceptions: # noqa: N801,N806 pylint: disable=invalid-name return wrapper -def is_enum(obj: typing.Any) -> bool: +def is_enum(obj: Any) -> bool: """Check if a given object is an enum.""" try: return issubclass(obj, enum.Enum) @@ -432,9 +430,7 @@ def is_enum(obj: typing.Any) -> bool: return False -def get_repr(obj: typing.Any, - constructor: bool = False, - **attrs: typing.Any) -> str: +def get_repr(obj: Any, constructor: bool = False, **attrs: Any) -> str: """Get a suitable __repr__ string for an object. Args: @@ -457,7 +453,7 @@ def get_repr(obj: typing.Any, return '<{}>'.format(cls) -def qualname(obj: typing.Any) -> str: +def qualname(obj: Any) -> str: """Get the fully qualified name of an object. Based on twisted.python.reflect.fullyQualifiedName. @@ -485,14 +481,10 @@ def qualname(obj: typing.Any) -> str: return repr(obj) -# The string annotation is a WORKAROUND for a Python 3.5.2 bug: -# https://github.com/python/typing/issues/266 +_ExceptionType = Union[Type[BaseException], Tuple[Type[BaseException]]] -def raises(exc: ('typing.Union[' # pylint: disable=bad-docstring-quotes - ' typing.Type[BaseException], ' - ' typing.Tuple[typing.Type[BaseException]]]'), - func: typing.Callable, - *args: typing.Any) -> bool: + +def raises(exc: _ExceptionType, func: Callable, *args: Any) -> bool: """Check if a function raises a given exception. Args: @@ -520,7 +512,7 @@ def force_encoding(text: str, encoding: str) -> str: def sanitize_filename(name: str, - replacement: typing.Optional[str] = '_', + replacement: Optional[str] = '_', shorten: bool = False) -> str: """Replace invalid filename characters. @@ -708,7 +700,7 @@ def open_file(filename: str, cmdline: str = None) -> None: proc.start_detached(cmd, args) -def unused(_arg: typing.Any) -> None: +def unused(_arg: Any) -> None: """Function which does nothing to avoid pylint complaining.""" @@ -730,16 +722,22 @@ def expand_windows_drive(path: str) -> str: return path -def yaml_load(f: typing.Union[str, typing.IO[str]]) -> typing.Any: +def yaml_load(f: Union[str, IO[str]]) -> Any: """Wrapper over yaml.load using the C loader if possible.""" start = datetime.datetime.now() # WORKAROUND for https://github.com/yaml/pyyaml/pull/181 - with log.ignore_py_warnings( + with log.py_warning_filter( category=DeprecationWarning, message=r"Using or importing the ABCs from 'collections' instead " r"of from 'collections\.abc' is deprecated.*"): - data = yaml.load(f, Loader=YamlLoader) + try: + data = yaml.load(f, Loader=YamlLoader) + except ValueError as e: + if str(e).startswith('could not convert string to float'): + # WORKAROUND for https://github.com/yaml/pyyaml/issues/168 + raise yaml.YAMLError(e) + raise # pragma: no cover end = datetime.datetime.now() @@ -760,8 +758,7 @@ def yaml_load(f: typing.Union[str, typing.IO[str]]) -> typing.Any: return data -def yaml_dump(data: typing.Any, - f: typing.IO[str] = None) -> typing.Optional[str]: +def yaml_dump(data: Any, f: IO[str] = None) -> Optional[str]: """Wrapper over yaml.dump using the C dumper if possible. Also returns a str instead of bytes. @@ -774,7 +771,7 @@ def yaml_dump(data: typing.Any, return yaml_data.decode('utf-8') -def chunk(elems: typing.Sequence, n: int) -> typing.Iterator[typing.Sequence]: +def chunk(elems: Sequence, n: int) -> Iterator[Sequence]: """Yield successive n-sized chunks from elems. If elems % n != 0, the last chunk will be smaller. diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 476926d34..031a8410c 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -30,8 +30,8 @@ import collections import enum import datetime import getpass -import typing import functools +from typing import Mapping, Optional, Sequence, Tuple, cast import attr import pkg_resources @@ -82,20 +82,36 @@ class DistributionInfo: """Information about the running distribution.""" - id = attr.ib() # type: typing.Optional[str] - parsed = attr.ib() # type: Distribution - version = attr.ib() # type: typing.Optional[typing.Tuple[str, ...]] - pretty = attr.ib() # type: str + id: Optional[str] = attr.ib() + parsed: 'Distribution' = attr.ib() + version: Optional[Tuple[str, ...]] = attr.ib() + pretty: str = attr.ib() pastebin_url = None -Distribution = enum.Enum( - 'Distribution', ['unknown', 'ubuntu', 'debian', 'void', 'arch', - 'gentoo', 'fedora', 'opensuse', 'linuxmint', 'manjaro', - 'kde_flatpak']) -def distribution() -> typing.Optional[DistributionInfo]: +class Distribution(enum.Enum): + + """A known Linux distribution. + + Usually lines up with ID=... in /etc/os-release. + """ + + unknown = enum.auto() + ubuntu = enum.auto() + debian = enum.auto() + void = enum.auto() + arch = enum.auto() + gentoo = enum.auto() # includes funtoo + fedora = enum.auto() + opensuse = enum.auto() + linuxmint = enum.auto() + manjaro = enum.auto() + kde_flatpak = enum.auto() # org.kde.Platform + + +def distribution() -> Optional[DistributionInfo]: """Get some information about the running Linux distribution. Returns: @@ -123,9 +139,8 @@ def distribution() -> typing.Optional[DistributionInfo]: assert pretty is not None if 'VERSION_ID' in info: - dist_version = pkg_resources.parse_version( - info['VERSION_ID'] - ) # type: typing.Optional[typing.Tuple[str, ...]] + dist_version: Optional[Tuple[str, ...]] = pkg_resources.parse_version( + info['VERSION_ID']) else: dist_version = None @@ -154,7 +169,7 @@ def is_sandboxed() -> bool: return current_distro.parsed == Distribution.kde_flatpak -def _git_str() -> typing.Optional[str]: +def _git_str() -> Optional[str]: """Try to find out git version. Return: @@ -188,7 +203,7 @@ def _call_git(gitpath: str, *args: str) -> str: stdout=subprocess.PIPE).stdout.decode('UTF-8').strip() -def _git_str_subprocess(gitpath: str) -> typing.Optional[str]: +def _git_str_subprocess(gitpath: str) -> Optional[str]: """Try to get the git commit ID and timestamp by calling git. Args: @@ -210,7 +225,7 @@ def _git_str_subprocess(gitpath: str) -> typing.Optional[str]: return None -def _release_info() -> typing.Sequence[typing.Tuple[str, str]]: +def _release_info() -> Sequence[Tuple[str, str]]: """Try to gather distribution release information. Return: @@ -234,14 +249,14 @@ def _release_info() -> typing.Sequence[typing.Tuple[str, str]]: return data -def _module_versions() -> typing.Sequence[str]: +def _module_versions() -> Sequence[str]: """Get versions of optional modules. Return: A list of lines with version info. """ lines = [] - modules = collections.OrderedDict([ + modules: Mapping[str, Sequence[str]] = collections.OrderedDict([ ('sip', ['SIP_VERSION_STR']), ('colorama', ['VERSION', '__version__']), ('pypeg2', ['__version__']), @@ -254,7 +269,7 @@ def _module_versions() -> typing.Sequence[str]: ('PyQt5.QtWebEngineWidgets', []), ('PyQt5.QtWebEngine', ['PYQT_WEBENGINE_VERSION_STR']), ('PyQt5.QtWebKitWidgets', []), - ]) # type: typing.Mapping[str, typing.Sequence[str]] + ]) for modname, attributes in modules.items(): try: module = importlib.import_module(modname) @@ -274,7 +289,7 @@ def _module_versions() -> typing.Sequence[str]: return lines -def _path_info() -> typing.Mapping[str, str]: +def _path_info() -> Mapping[str, str]: """Get info about important path names. Return: @@ -293,7 +308,7 @@ def _path_info() -> typing.Mapping[str, str]: return info -def _os_info() -> typing.Sequence[str]: +def _os_info() -> Sequence[str]: """Get operating system info. Return: @@ -403,6 +418,7 @@ def _chromium_version() -> str: 5.12.7: Security fixes up to 79.0.3945.130 (2020-01-16) 5.12.8: Security fixes up to 80.0.3987.149 (2020-03-18) 5.12.9: Security fixes up to 83.0.4103.97 (2020-06-03) + 5.12.10: Security fixes up to 86.0.4240.75 (2020-10-06) Qt 5.13: Chromium 73 73.0.3683.105 (~2019-02-28) @@ -419,6 +435,10 @@ def _chromium_version() -> str: Qt 5.15: Chromium 80 80.0.3987.163 (2020-04-02) 5.15.0: Security fixes up to 81.0.4044.138 (2020-05-05) + 5.15.1: Security fixes up to 85.0.4183.83 (2020-08-25) + + 5.15.2: Updated to 83.0.4103.122 (~2020-06-24) + Security fixes up to 86.0.4240.111 (2020-10-20) Also see: @@ -430,6 +450,8 @@ def _chromium_version() -> str: return 'unavailable' # type: ignore[unreachable] if webenginesettings.parsed_user_agent is None: + if 'avoid-chromium-init' in objects.debug_flags: + return 'avoided' webenginesettings.init_user_agent() assert webenginesettings.parsed_user_agent is not None @@ -550,21 +572,21 @@ class OpenGLInfo: """Information about the OpenGL setup in use.""" # If we're using OpenGL ES. If so, no further information is available. - gles = attr.ib(False) # type: bool + gles: bool = attr.ib(False) # The name of the vendor. Examples: # - nouveau # - "Intel Open Source Technology Center", "Intel", "Intel Inc." - vendor = attr.ib(None) # type: typing.Optional[str] + vendor: Optional[str] = attr.ib(None) # The OpenGL version as a string. See tests for examples. - version_str = attr.ib(None) # type: typing.Optional[str] + version_str: Optional[str] = attr.ib(None) # The parsed version as a (major, minor) tuple of ints - version = attr.ib(None) # type: typing.Optional[typing.Tuple[int, ...]] + version: Optional[Tuple[int, ...]] = attr.ib(None) # The vendor specific information following the version number - vendor_specific = attr.ib(None) # type: typing.Optional[str] + vendor_specific: Optional[str] = attr.ib(None) def __str__(self) -> str: if self.gles: @@ -602,7 +624,7 @@ class OpenGLInfo: @functools.lru_cache(maxsize=1) -def opengl_info() -> typing.Optional[OpenGLInfo]: # pragma: no cover +def opengl_info() -> Optional[OpenGLInfo]: # pragma: no cover """Get the OpenGL vendor used. This returns a string such as 'nouveau' or @@ -620,8 +642,7 @@ def opengl_info() -> typing.Optional[OpenGLInfo]: # pragma: no cover vendor, version = override.split(', ', maxsplit=1) return OpenGLInfo.parse(vendor=vendor, version=version) - old_context = typing.cast(typing.Optional[QOpenGLContext], - QOpenGLContext.currentContext()) + old_context = cast(Optional[QOpenGLContext], QOpenGLContext.currentContext()) old_surface = None if old_context is None else old_context.surface() surface = QOffscreenSurface() diff --git a/requirements.txt b/requirements.txt index 0223e896a..dad7a8f50 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,10 +2,10 @@ adblock==0.3.2 attrs==20.2.0 -colorama==0.4.3 +colorama==0.4.4 cssutils==1.0.2 Jinja2==2.11.2 MarkupSafe==1.1.1 -Pygments==2.7.1 +Pygments==2.7.2 pyPEG2==2.15.2 PyYAML==5.3.1 diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py index 313aa13e3..f4e4ac07f 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -53,7 +53,12 @@ class Message: print(self.text) -MsgType = enum.Enum('MsgType', 'insufficient_coverage, perfect_file') +class MsgType(enum.Enum): + + """The type of a message to be output.""" + + insufficient_coverage = enum.auto() + perfect_file = enum.auto() # A list of (test_file, tested_file) tuples. test_file can be None. @@ -213,6 +218,8 @@ PERFECT_FILES = [ 'qutebrowser/browser/webengine/spell.py'), ('tests/unit/browser/webengine/test_webengine_cookies.py', 'qutebrowser/browser/webengine/cookies.py'), + ('tests/unit/browser/webengine/test_darkmode.py', + 'qutebrowser/browser/webengine/darkmode.py'), ] diff --git a/scripts/dev/recompile_requirements.py b/scripts/dev/recompile_requirements.py index 439976c34..5b79b801d 100644 --- a/scripts/dev/recompile_requirements.py +++ b/scripts/dev/recompile_requirements.py @@ -84,6 +84,7 @@ CHANGELOG_URLS = { 'jaraco.functools': 'https://github.com/jaraco/jaraco.functools/blob/master/CHANGES.rst', 'parse': 'https://github.com/r1chardj0n3s/parse#potential-gotchas', 'py': 'https://py.readthedocs.io/en/latest/changelog.html#changelog', + 'Pympler': 'https://github.com/pympler/pympler/blob/master/CHANGELOG.md', 'pytest-mock': 'https://github.com/pytest-dev/pytest-mock/blob/master/CHANGELOG.rst', 'pytest-qt': 'https://github.com/pytest-dev/pytest-qt/blob/master/CHANGELOG.rst', 'wcwidth': 'https://github.com/jquast/wcwidth#history', @@ -99,17 +100,18 @@ CHANGELOG_URLS = { 'urllib3': 'https://github.com/urllib3/urllib3/blob/master/CHANGES.rst', 'wheel': 'https://github.com/pypa/wheel/blob/master/docs/news.rst', 'mako': 'https://docs.makotemplates.org/en/latest/changelog.html', - 'lxml': 'https://lxml.de/4.5/changes-4.5.0.html', + 'lxml': 'https://lxml.de/4.6/changes-4.6.0.html', 'jwcrypto': 'https://github.com/latchset/jwcrypto/commits/master', 'tox-pip-version': 'https://github.com/pglass/tox-pip-version/commits/master', 'wrapt': 'https://github.com/GrahamDumpleton/wrapt/blob/develop/docs/changes.rst', - 'pep517': 'https://github.com/pypa/pep517/commits/master', - 'cryptography': 'https://cryptography.io/en/latest/changelog/', + 'pep517': 'https://github.com/pypa/pep517/blob/master/doc/changelog.rst', + 'cryptography': 'https://cryptography.io/en/latest/changelog.html', 'toml': 'https://github.com/uiri/toml/releases', 'PyQt5': 'https://www.riverbankcomputing.com/news', 'PyQtWebEngine': 'https://www.riverbankcomputing.com/news', 'PyQt-builder': 'https://www.riverbankcomputing.com/news', 'PyQt5-sip': 'https://www.riverbankcomputing.com/news', + 'PyQt5_stubs': 'https://github.com/stlehmann/PyQt5-stubs/blob/master/CHANGELOG.md', 'sip': 'https://www.riverbankcomputing.com/news', 'Pygments': 'https://pygments.org/docs/changelog/', 'vulture': 'https://github.com/jendrikseipp/vulture/blob/master/CHANGELOG.md', @@ -122,6 +124,15 @@ CHANGELOG_URLS = { 'tldextract': 'https://github.com/john-kurkowski/tldextract/blob/master/CHANGELOG.md', 'typing_extensions': 'https://github.com/python/typing/commits/master/typing_extensions', 'diff_cover': 'https://github.com/Bachmann1234/diff_cover/blob/master/CHANGELOG', + 'pytest-clarity': 'https://github.com/darrenburns/pytest-clarity/commits/master', + 'pytest-icdiff': 'https://github.com/hjwp/pytest-icdiff/blob/master/HISTORY.rst', + 'icdiff': 'https://github.com/jeffkaufman/icdiff/blob/master/ChangeLog', + 'termcolor': 'https://pypi.org/project/termcolor/', + 'pprintpp': 'https://github.com/wolever/pprintpp/blob/master/CHANGELOG.txt', + 'beautifulsoup4': 'https://bazaar.launchpad.net/~leonardr/beautifulsoup/bs4/view/head:/CHANGELOG', + 'check-manifest': 'https://github.com/mgedmin/check-manifest/blob/master/CHANGES.rst', + 'yamllint': 'https://github.com/adrienverge/yamllint/blob/master/CHANGELOG.rst', + 'filelock': 'https://github.com/benediktschmitt/py-filelock/commits/master', } # PyQt versions which need SIP v4 @@ -303,8 +314,8 @@ class Change: return '| {} | {} | {} |'.format(self.link, self.old, self.new) -def print_changed_files(): - """Output all changed files from this run.""" +def _get_changed_files(): + """Get a list of changed files via git.""" changed_files = set() filenames = git_diff('--name-only') for filename in filenames: @@ -312,8 +323,12 @@ def print_changed_files(): filename = filename.replace('misc/requirements/requirements-', '') filename = filename.replace('.txt', '') changed_files.add(filename) - files_text = '\n'.join('- ' + line for line in sorted(changed_files)) + return sorted(changed_files) + + +def _get_changes(): + """Get a list of changed versions from git.""" changes_dict = {} diff = git_diff() for line in diff: @@ -326,10 +341,16 @@ def print_changed_files(): name, version = line[1:].split('==') if ';' in version: # pip environment markers version = version.split(';')[0].strip() + elif line[1:].startswith('-e'): + rest, name = line.split('#egg=') + version = rest.split('@')[1][:7] else: name = line[1:] version = '?' + if name.startswith('#'): # duplicate requirements + name = name[1:].strip() + if name not in changes_dict: changes_dict[name] = Change(name) @@ -338,7 +359,15 @@ def print_changed_files(): elif line.startswith('+'): changes_dict[name].new = version - changes = [change for _name, change in sorted(changes_dict.items())] + return [change for _name, change in sorted(changes_dict.items())] + + +def print_changed_files(): + """Output all changed files from this run.""" + changed_files = _get_changed_files() + files_text = '\n'.join('- ' + line for line in changed_files) + + changes = _get_changes() diff_text = '\n'.join(str(change) for change in changes) utils.print_title('Changed') diff --git a/scripts/dev/run_vulture.py b/scripts/dev/run_vulture.py index 3e7b21898..1d024c6e5 100755 --- a/scripts/dev/run_vulture.py +++ b/scripts/dev/run_vulture.py @@ -32,7 +32,7 @@ import vulture import qutebrowser.app # pylint: disable=unused-import from qutebrowser.extensions import loader from qutebrowser.misc import objects -from qutebrowser.utils import utils +from qutebrowser.utils import utils, version from qutebrowser.browser.webkit import rfc6266 # To run the decorators from there # pylint: disable=unused-import @@ -124,6 +124,9 @@ def whitelist_generator(): # noqa '_get_default_metavar_for_positional', '_metavar_formatter']: yield 'scripts.dev.src2asciidoc.UsageFormatter.' + attr + for dist in version.Distribution: + yield 'qutebrowser.utils.version.Distribution.{}'.format(dist.name) + # attrs yield 'qutebrowser.browser.webkit.network.networkmanager.ProxyId.hostname' yield 'qutebrowser.command.command.ArgInfo._validate_exclusive' diff --git a/scripts/dev/ua_fetch.py b/scripts/dev/ua_fetch.py new file mode 100644 index 000000000..a4ef889a0 --- /dev/null +++ b/scripts/dev/ua_fetch.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +"""Fetch and print the most common user agents. + +This script fetches the most common user agents according to +https://github.com/Kikobeats/top-user-agents, and prints the most recent +Chrome user agent for Windows, macOS and Linux. +""" + +import math +import sys +import textwrap + +import requests +import qutebrowser.config.websettings + + +def version(ua): + """Comparable version of a user agent.""" + return tuple(int(v) for v in ua.upstream_browser_version.split('.')[:2]) + + +def wrap(ini, sub, string): + return textwrap.wrap(string, width=80, initial_indent=ini, subsequent_indent=sub) + + +response = requests.get('https://raw.githubusercontent.com/Kikobeats/top-user-agents/master/index.json') + +if response.status_code != 200: + print('Unable to fetch the user agent index', file=sys.stderr) + sys.exit(1) + +ua_checks = { + 'Win10': lambda ua: ua.os_info.startswith('Windows NT'), + 'macOS': lambda ua: ua.os_info.startswith('Macintosh'), + 'Linux': lambda ua: ua.os_info.startswith('X11'), +} + +ua_strings = {} +ua_versions = {} +ua_names = {} + +for ua_string in reversed(response.json()): + # reversed to prefer more common versions + + # Filter out browsers that are not Chrome-based + parts = ua_string.split() + if not any(part.startswith("Chrome/") for part in parts): + continue + if any(part.startswith("OPR/") or part.startswith("Edg/") for part in parts): + continue + + user_agent = qutebrowser.config.websettings.UserAgent.parse(ua_string) + + # check which os_string conditions are met and select the most recent version + for key, check in ua_checks.items(): + if check(user_agent): + v = version(user_agent) + if v >= ua_versions.get(key, (-math.inf,)): + ua_versions[key] = v + ua_strings[key] = ua_string + ua_names[key] = f'Chrome {v[0]} {key}' + +for key, ua_string in ua_strings.items(): + quoted_ua_string = f'"{ua_string}"' + for line in wrap(" - - ", " ", quoted_ua_string): + print(line) + for line in wrap(" - ", " ", ua_names[key]): + print(line) diff --git a/scripts/dev/update_version.py b/scripts/dev/update_version.py index 7c12dab15..5f7df5cae 100644 --- a/scripts/dev/update_version.py +++ b/scripts/dev/update_version.py @@ -82,7 +82,7 @@ if __name__ == "__main__": .format(v=version)) print("* Windows: git fetch; git checkout v{v}; " "py -3.7 -m tox -e build-release -- --asciidoc " - "$env:userprofile\\bin\\asciidoc-9.9.2\\asciidoc.py --upload" + "$env:userprofile\\bin\\asciidoc-9.0.2\\asciidoc.py --upload" .format(v=version)) print("* macOS: git fetch && git checkout v{v} && " "tox -e build-release -- --upload" diff --git a/scripts/open_url_in_instance.sh b/scripts/open_url_in_instance.sh index ec2a5a26d..0d6edef51 100755 --- a/scripts/open_url_in_instance.sh +++ b/scripts/open_url_in_instance.sh @@ -12,4 +12,4 @@ printf '{"args": ["%s"], "target_arg": null, "version": "%s", "protocol_version" "${_url}" \ "${_qb_version}" \ "${_proto_version}" \ - "${PWD}" | socat - UNIX-CONNECT:"${_ipc_socket}" 2>/dev/null || "$_qute_bin" "$@" & + "${PWD}" | socat -lf /dev/null - UNIX-CONNECT:"${_ipc_socket}" || "$_qute_bin" "$@" & @@ -72,7 +72,7 @@ try: ['qutebrowser = qutebrowser.qutebrowser:main']}, zip_safe=True, install_requires=['pypeg2', 'jinja2', 'pygments', 'PyYAML', 'attrs'], - python_requires='>=3.5', + python_requires='>=3.6', name='qutebrowser', version=_get_constant('version'), description=_get_constant('description'), @@ -94,7 +94,6 @@ try: 'Operating System :: MacOS', 'Operating System :: POSIX :: BSD', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', diff --git a/tests/conftest.py b/tests/conftest.py index d4d06c6bc..ef169be4f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -34,7 +34,7 @@ pytest.register_assert_rewrite('helpers') from helpers import logfail from helpers.logfail import fail_on_logging -from helpers.messagemock import message_mock, message_bridge +from helpers.messagemock import message_mock from helpers.fixtures import * # noqa: F403 from helpers import utils as testutils from qutebrowser.utils import qtutils, standarddir, usertypes, utils, version diff --git a/tests/end2end/features/conftest.py b/tests/end2end/features/conftest.py index 0208cce05..87748a43a 100644 --- a/tests/end2end/features/conftest.py +++ b/tests/end2end/features/conftest.py @@ -378,6 +378,7 @@ def hint(quteproc, args): @bdd.when(bdd.parsers.parse('I hint with args "{args}" and follow {letter}')) def hint_and_follow(quteproc, args, letter): args = args.replace('(testdata)', testutils.abs_datapath()) + args = args.replace('(python-executable)', sys.executable) quteproc.send_cmd(':hint {}'.format(args)) quteproc.wait_for(message='hints: *') quteproc.send_cmd(':follow-hint {}'.format(letter)) diff --git a/tests/end2end/features/hints.feature b/tests/end2end/features/hints.feature index caf1200e2..35e48c483 100644 --- a/tests/end2end/features/hints.feature +++ b/tests/end2end/features/hints.feature @@ -61,17 +61,17 @@ Feature: Using hints Scenario: Using :hint spawn with flags and -- (issue 797) When I open data/hints/html/simple.html - And I hint with args "-- all spawn -v python -c ''" and follow a + And I hint with args "-- all spawn -v (python-executable) -c ''" and follow a Then the message "Command exited successfully." should be shown Scenario: Using :hint spawn with flags (issue 797) When I open data/hints/html/simple.html - And I hint with args "all spawn -v python -c ''" and follow a + And I hint with args "all spawn -v (python-executable) -c ''" and follow a Then the message "Command exited successfully." should be shown Scenario: Using :hint spawn with flags and --rapid (issue 797) When I open data/hints/html/simple.html - And I hint with args "--rapid all spawn -v python -c ''" and follow a + And I hint with args "--rapid all spawn -v (python-executable) -c ''" and follow a Then the message "Command exited successfully." should be shown @posix diff --git a/tests/end2end/features/marks.feature b/tests/end2end/features/marks.feature index 7ac60edeb..921e0e76c 100644 --- a/tests/end2end/features/marks.feature +++ b/tests/end2end/features/marks.feature @@ -112,11 +112,11 @@ Feature: Setting positional marks Then the page should be scrolled to 0 0 Scenario: Hovering a hint does not set the ' mark - When I run :scroll-px 30 20 - And I wait until the scroll position changed to 30/20 - And I run :scroll-to-perc 0 + When I run :scroll-px 10 20 + And I wait until the scroll position changed to 10/20 + And I run :scroll-to-perc 0 And I wait until the scroll position changed And I hint with args "links hover" and follow s And I run :jump-mark "'" - And I wait until the scroll position changed to 30/20 - Then the page should be scrolled to 30 20 + And I wait until the scroll position changed to 10/20 + Then the page should be scrolled to 10 20 diff --git a/tests/end2end/features/private.feature b/tests/end2end/features/private.feature index 07ff225a3..fe870dded 100644 --- a/tests/end2end/features/private.feature +++ b/tests/end2end/features/private.feature @@ -172,7 +172,7 @@ Feature: Using private browsing - url: http://localhost:*/data/numbers/1.txt - url: http://localhost:*/data/numbers/2.txt - @flaky + @skip # Too flaky Scenario: Saving a private session with only-active-window When I open data/numbers/1.txt And I open data/numbers/2.txt in a new tab @@ -181,7 +181,12 @@ Feature: Using private browsing And I open data/numbers/5.txt in a new tab And I run :session-save --only-active-window window_session_name And I run :window-only + And I wait for "removed: tab" in the log + And I wait for "removed: tab" in the log And I run :tab-only + And I wait for "removed: tab" in the log + And I wait for "removed: tab" in the log + And I wait for "removed: tab" in the log And I run :session-load -c window_session_name And I wait until data/numbers/5.txt is loaded Then the session should look like: diff --git a/tests/end2end/features/qutescheme.feature b/tests/end2end/features/qutescheme.feature index 55e366b4f..84e1b04e8 100644 --- a/tests/end2end/features/qutescheme.feature +++ b/tests/end2end/features/qutescheme.feature @@ -89,6 +89,7 @@ Feature: Special qute:// pages And I open qute://help/img/ without waiting Then "*Error while * qute://*" should be logged And "* url='qute://help/img'* LoadStatus.error" should be logged + And "Load error: ERR_FILE_NOT_FOUND" should be logged # :history diff --git a/tests/end2end/features/urlmarks.feature b/tests/end2end/features/urlmarks.feature index ec38116c3..e55b5839d 100644 --- a/tests/end2end/features/urlmarks.feature +++ b/tests/end2end/features/urlmarks.feature @@ -231,7 +231,7 @@ Feature: quickmarks and bookmarks And the page should contain the plaintext "twentyone" Scenario: Listing bookmarks - When I open data/title.html in a new tab + When I open data/title.html#unique-url in a new tab And I run :bookmark-add And I open qute://bookmarks Then the page should contain the plaintext "Test title" diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py index 78fd0e48a..f5cc2bb19 100644 --- a/tests/end2end/fixtures/quteprocess.py +++ b/tests/end2end/fixtures/quteprocess.py @@ -308,6 +308,27 @@ def is_ignored_chromium_message(line): 'Could not open platform files for entry.', 'Unable to terminate process *: No such process (3)', 'Failed to read /proc/*/stat', + + # Qt 5.15.1 debug build (Chromium 83) + # '[314297:7:0929/214605.491958:ERROR:context_provider_command_buffer.cc(145)] + # GpuChannelHost failed to create command buffer.' + 'GpuChannelHost failed to create command buffer.', + # [338691:4:0929/220114.488847:WARNING:ipc_message_attachment_set.cc(49)] + # MessageAttachmentSet destroyed with unconsumed attachments: 0/1 + 'MessageAttachmentSet destroyed with unconsumed attachments: *', + + # GitHub Actions with Qt 5.15.1 + ('SharedImageManager::ProduceGLTexture: Trying to produce a ' + 'representation from a non-existent mailbox. *'), + ('[.DisplayCompositor]GL ERROR :GL_INVALID_OPERATION : ' + 'DoCreateAndTexStorage2DSharedImageINTERNAL: invalid mailbox name'), + ('[.DisplayCompositor]GL ERROR :GL_INVALID_OPERATION : ' + 'DoBeginSharedImageAccessCHROMIUM: bound texture is not a shared image'), + ('[.DisplayCompositor]RENDER WARNING: texture bound to texture unit 0 is ' + 'not renderable. It might be non-power-of-2 or have incompatible texture ' + 'filtering (maybe)?'), + ('[.DisplayCompositor]GL ERROR :GL_INVALID_OPERATION : ' + 'DoEndSharedImageAccessCHROMIUM: bound texture is not a shared image'), ] return any(testutils.pattern_match(pattern=pattern, value=message) for pattern in ignored_messages) @@ -749,6 +770,7 @@ class QuteProc(testprocess.Process): Return: The parsed log line with "command called: ..." or None. """ + __tracebackhide__ = lambda e: e.errisinstance(testprocess.WaitForTimeout) summary = command if count is not None: summary += ' (count {})'.format(count) diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py index cd9aefe16..3e8731fad 100644 --- a/tests/end2end/test_invocations.py +++ b/tests/end2end/test_invocations.py @@ -333,16 +333,17 @@ def test_command_on_start(request, quteproc_new): quteproc_new.wait_for_quit() -def test_launching_with_python2(): +@pytest.mark.parametrize('python', ['python2', 'python3.5']) +def test_launching_with_old_python(python): try: proc = subprocess.run( - ['python2', '-m', 'qutebrowser', '--no-err-windows'], + [python, '-m', 'qutebrowser', '--no-err-windows'], stderr=subprocess.PIPE, check=False) except FileNotFoundError: - pytest.skip("python2 not found") + pytest.skip(f"{python} not found") assert proc.returncode == 1 - error = "At least Python 3.5.2 is required to run qutebrowser" + error = "At least Python 3.6 is required to run qutebrowser" assert proc.stderr.decode('ascii').startswith(error) diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py index 015238d1b..2183ac1d1 100644 --- a/tests/helpers/fixtures.py +++ b/tests/helpers/fixtures.py @@ -97,6 +97,10 @@ class WinRegistryHelper: def windowTitle(self): return 'window title - qutebrowser' + @property + def tabbed_browser(self): + return self.registry['tabbed-browser'] + def __init__(self): self._ids = [] @@ -705,3 +709,16 @@ def state_config(data_tmpdir, monkeypatch): state = configfiles.StateConfig() monkeypatch.setattr(configfiles, 'state', state) return state + + +@pytest.fixture +def unwritable_tmp_path(tmp_path): + tmp_path.chmod(0) + if os.access(str(tmp_path), os.W_OK): + # Docker container or similar + pytest.skip("Directory was still writable") + + yield tmp_path + + # Make sure pytest can clean up the tmp_path + tmp_path.chmod(0o755) diff --git a/tests/helpers/messagemock.py b/tests/helpers/messagemock.py index 4c1107029..03320a98f 100644 --- a/tests/helpers/messagemock.py +++ b/tests/helpers/messagemock.py @@ -24,7 +24,7 @@ import logging import attr import pytest -from qutebrowser.utils import usertypes, message, objreg +from qutebrowser.utils import usertypes, message @attr.s @@ -90,12 +90,3 @@ def message_mock(): mmock.patch() yield mmock mmock.unpatch() - - -@pytest.fixture -def message_bridge(win_registry): - """Fixture to get a MessageBridge.""" - bridge = message.MessageBridge() - objreg.register('message-bridge', bridge, scope='window', window=0) - yield bridge - objreg.delete('message-bridge', scope='window', window=0) diff --git a/tests/helpers/utils.py b/tests/helpers/utils.py index 41fb4f100..77221be4c 100644 --- a/tests/helpers/utils.py +++ b/tests/helpers/utils.py @@ -234,7 +234,7 @@ def change_cwd(path): @contextlib.contextmanager def ignore_bs4_warning(): """WORKAROUND for https://bugs.launchpad.net/beautifulsoup/+bug/1847592.""" - with log.ignore_py_warnings( + with log.py_warning_filter( category=DeprecationWarning, message="Using or importing the ABCs from 'collections' instead " "of from 'collections.abc' is deprecated", module='bs4.element'): diff --git a/tests/unit/api/test_cmdutils.py b/tests/unit/api/test_cmdutils.py index 58643640c..3ee486303 100644 --- a/tests/unit/api/test_cmdutils.py +++ b/tests/unit/api/test_cmdutils.py @@ -290,7 +290,11 @@ class TestRegister: else: assert pos_args == [('arg', 'arg')] - Enum = enum.Enum('Test', ['x', 'y']) + class Enum(enum.Enum): + + # pylint: disable=invalid-name + x = enum.auto() + y = enum.auto() @pytest.mark.parametrize('typ, inp, choices, expected', [ (int, '42', None, 42), diff --git a/tests/unit/browser/test_hints.py b/tests/unit/browser/test_hints.py index 56e5b980c..382c322fb 100644 --- a/tests/unit/browser/test_hints.py +++ b/tests/unit/browser/test_hints.py @@ -46,8 +46,7 @@ def tabbed_browser(tabbed_browser_stubs, web_tab): return tb -def test_show_benchmark(benchmark, tabbed_browser, qtbot, message_bridge, - mode_manager): +def test_show_benchmark(benchmark, tabbed_browser, qtbot, mode_manager): """Benchmark showing/drawing of hint labels.""" tab = tabbed_browser.widget.tabs[0] @@ -66,8 +65,8 @@ def test_show_benchmark(benchmark, tabbed_browser, qtbot, message_bridge, benchmark(bench) -def test_match_benchmark(benchmark, tabbed_browser, qtbot, message_bridge, - mode_manager, qapp, config_stub): +def test_match_benchmark(benchmark, tabbed_browser, qtbot, mode_manager, qapp, + config_stub): """Benchmark matching of hint labels.""" tab = tabbed_browser.widget.tabs[0] diff --git a/tests/unit/browser/webengine/test_darkmode.py b/tests/unit/browser/webengine/test_darkmode.py new file mode 100644 index 000000000..b2ca6a20a --- /dev/null +++ b/tests/unit/browser/webengine/test_darkmode.py @@ -0,0 +1,228 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: +# Copyright 2020 Florian Bruhin (The Compiler) <mail@qutebrowser.org> + +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. + +import logging + +import pytest + +from qutebrowser.config import configdata +from qutebrowser.utils import usertypes, version +from qutebrowser.browser.webengine import darkmode +from qutebrowser.misc import objects +from helpers import utils + + +pytestmark = utils.qt510 + + +@pytest.fixture(autouse=True) +def patch_backend(monkeypatch): + monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine) + + +@pytest.mark.parametrize('settings, expected', [ + # Disabled + ({}, []), + + # Enabled without customization + ({'enabled': True}, [('forceDarkModeEnabled', 'true')]), + + # Algorithm + ( + {'enabled': True, 'algorithm': 'brightness-rgb'}, + [ + ('forceDarkModeEnabled', 'true'), + ('forceDarkModeInversionAlgorithm', '2') + ], + ), +]) +def test_basics(config_stub, monkeypatch, settings, expected): + for k, v in settings.items(): + config_stub.set_obj('colors.webpage.darkmode.' + k, v) + monkeypatch.setattr(darkmode, '_variant', + lambda: darkmode.Variant.qt_515_2) + + if expected: + expected.append(('forceDarkModeImagePolicy', '2')) + + assert list(darkmode.settings()) == expected + + +QT_514_SETTINGS = [ + ('darkMode', '2'), + ('darkModeImagePolicy', '2'), + ('darkModeGrayscale', 'true'), +] + + +QT_515_0_SETTINGS = [ + ('darkModeEnabled', 'true'), + ('darkModeInversionAlgorithm', '2'), + ('darkModeGrayscale', 'true'), +] + + +QT_515_1_SETTINGS = [ + ('darkModeEnabled', 'true'), + ('darkModeInversionAlgorithm', '2'), + ('darkModeImagePolicy', '2'), + ('darkModeGrayscale', 'true'), +] + + +QT_515_2_SETTINGS = [ + ('forceDarkModeEnabled', 'true'), + ('forceDarkModeInversionAlgorithm', '2'), + ('forceDarkModeImagePolicy', '2'), + ('forceDarkModeGrayscale', 'true'), +] + + +@pytest.mark.parametrize('qversion, expected', [ + ('5.14.0', QT_514_SETTINGS), + ('5.14.1', QT_514_SETTINGS), + ('5.14.2', QT_514_SETTINGS), + + ('5.15.0', QT_515_0_SETTINGS), + ('5.15.1', QT_515_1_SETTINGS), + + ('5.15.2', QT_515_2_SETTINGS), +]) +def test_qt_version_differences(config_stub, monkeypatch, qversion, expected): + monkeypatch.setattr(darkmode.qtutils, 'qVersion', lambda: qversion) + + major, minor, patch = [int(part) for part in qversion.split('.')] + hexversion = major << 16 | minor << 8 | patch + if major > 5 or minor >= 13: + # Added in Qt 5.13 + monkeypatch.setattr(darkmode, 'PYQT_WEBENGINE_VERSION', hexversion) + + settings = { + 'enabled': True, + 'algorithm': 'brightness-rgb', + 'grayscale.all': True, + } + for k, v in settings.items(): + config_stub.set_obj('colors.webpage.darkmode.' + k, v) + + assert list(darkmode.settings()) == expected + + +@utils.qt514 +@pytest.mark.parametrize('setting, value, exp_key, exp_val', [ + ('contrast', -0.5, + 'Contrast', '-0.5'), + ('policy.page', 'smart', + 'PagePolicy', '1'), + ('policy.images', 'smart', + 'ImagePolicy', '2'), + ('threshold.text', 100, + 'TextBrightnessThreshold', '100'), + ('threshold.background', 100, + 'BackgroundBrightnessThreshold', '100'), + ('grayscale.all', True, + 'Grayscale', 'true'), + ('grayscale.images', 0.5, + 'ImageGrayscale', '0.5'), +]) +def test_customization(config_stub, monkeypatch, setting, value, exp_key, exp_val): + config_stub.val.colors.webpage.darkmode.enabled = True + config_stub.set_obj('colors.webpage.darkmode.' + setting, value) + monkeypatch.setattr(darkmode, '_variant', lambda: darkmode.Variant.qt_515_2) + + expected = [] + expected.append(('forceDarkModeEnabled', 'true')) + if exp_key != 'ImagePolicy': + expected.append(('forceDarkModeImagePolicy', '2')) + expected.append(('forceDarkMode' + exp_key, exp_val)) + + assert list(darkmode.settings()) == expected + + +@pytest.mark.parametrize('qversion, webengine_version, expected', [ + # Without PYQT_WEBENGINE_VERSION + ('5.9.9', None, darkmode.Variant.unavailable), + ('5.10.1', None, darkmode.Variant.qt_510), + ('5.11.3', None, darkmode.Variant.qt_511_to_513), + ('5.12.9', None, darkmode.Variant.qt_511_to_513), + + # With PYQT_WEBENGINE_VERSION + (None, 0x050d00, darkmode.Variant.qt_511_to_513), + (None, 0x050e00, darkmode.Variant.qt_514), + (None, 0x050f00, darkmode.Variant.qt_515_0), + (None, 0x050f01, darkmode.Variant.qt_515_1), + (None, 0x050f02, darkmode.Variant.qt_515_2), + (None, 0x060000, darkmode.Variant.qt_515_2), # Qt 6 +]) +def test_variant(monkeypatch, qversion, webengine_version, expected): + monkeypatch.setattr(darkmode.qtutils, 'qVersion', lambda: qversion) + monkeypatch.setattr(darkmode, 'PYQT_WEBENGINE_VERSION', webengine_version) + assert darkmode._variant() == expected + + +def test_broken_smart_images_policy(config_stub, monkeypatch, caplog): + config_stub.val.colors.webpage.darkmode.enabled = True + config_stub.val.colors.webpage.darkmode.policy.images = 'smart' + monkeypatch.setattr(darkmode, 'PYQT_WEBENGINE_VERSION', 0x050f00) + + with caplog.at_level(logging.WARNING): + settings = list(darkmode.settings()) + + assert caplog.messages[-1] == ( + 'Ignoring colors.webpage.darkmode.policy.images = smart because of ' + 'Qt 5.15.0 bug') + + expected = [ + [('darkModeEnabled', 'true')], # Qt 5.15 + [('darkMode', '4')], # Qt 5.14 + ] + assert settings in expected + + +@utils.qt510 +def test_new_chromium(): + """Fail if we encounter an unknown Chromium version. + + Dark mode in Chromium (or rather, the underlying Blink) is being changed with + almost every Chromium release. + + Make this test fail deliberately with newer Chromium versions, so that + we can test whether dark mode still works manually, and adjust if not. + """ + assert version._chromium_version() in [ + 'unavailable', # QtWebKit + '61.0.3163.140', # Qt 5.10 + '65.0.3325.230', # Qt 5.11 + '69.0.3497.128', # Qt 5.12 + '73.0.3683.105', # Qt 5.13 + '77.0.3865.129', # Qt 5.14 + '80.0.3987.163', # Qt 5.15.0 + '83.0.4103.122', # Qt 5.15.2 + ] + + +def test_options(configdata_init): + """Make sure all darkmode options have the right attributes set.""" + for name, opt in configdata.DATA.items(): + if not name.startswith('colors.webpage.darkmode.'): + continue + + assert not opt.supports_pattern, name + assert opt.restart, name + assert not opt.raw_backends['QtWebKit'], name + assert opt.raw_backends['QtWebEngine'] in ['Qt 5.10', 'Qt 5.14'], name diff --git a/tests/unit/browser/webkit/test_mhtml.py b/tests/unit/browser/webkit/test_mhtml.py index 8d4289f4b..58e5602b3 100644 --- a/tests/unit/browser/webkit/test_mhtml.py +++ b/tests/unit/browser/webkit/test_mhtml.py @@ -29,10 +29,7 @@ mhtml = pytest.importorskip('qutebrowser.browser.webkit.mhtml') try: import cssutils -except (ImportError, re.error): - # Catching re.error because cssutils in earlier releases (<= 1.0) is - # broken on Python 3.5 - # See https://bitbucket.org/cthedot/cssutils/issues/52 +except ImportError: cssutils = None diff --git a/tests/unit/commands/test_argparser.py b/tests/unit/commands/test_argparser.py index ccf81edd1..69119c4cf 100644 --- a/tests/unit/commands/test_argparser.py +++ b/tests/unit/commands/test_argparser.py @@ -28,7 +28,10 @@ from PyQt5.QtCore import QUrl from qutebrowser.commands import argparser, cmdexc -Enum = enum.Enum('Enum', ['foo', 'foo_bar']) +class Enum(enum.Enum): + + foo = enum.auto() + foo_bar = enum.auto() class TestArgumentParser: diff --git a/tests/unit/config/test_configcommands.py b/tests/unit/config/test_configcommands.py index cb2608145..331d214b1 100644 --- a/tests/unit/config/test_configcommands.py +++ b/tests/unit/config/test_configcommands.py @@ -499,7 +499,8 @@ class TestSource: else: assert False, location - pyfile.write_text('c.content.javascript.enabled = False\n', + pyfile.write_text('\n'.join(['config.load_autoconfig(False)', + 'c.content.javascript.enabled = False']), encoding='utf-8') commands.config_source(arg, clear=clear) @@ -511,14 +512,16 @@ class TestSource: def test_config_py_arg_source(self, commands, config_py_arg, config_stub): assert config_stub.val.content.javascript.enabled - config_py_arg.write_text('c.content.javascript.enabled = False\n', + config_py_arg.write_text('\n'.join(['config.load_autoconfig(False)', + 'c.content.javascript.enabled = False']), encoding='utf-8') commands.config_source() assert not config_stub.val.content.javascript.enabled def test_errors(self, commands, config_tmpdir): pyfile = config_tmpdir / 'config.py' - pyfile.write_text('c.foo = 42', encoding='utf-8') + pyfile.write_text('\n'.join(['config.load_autoconfig(False)', + 'c.foo = 42']), encoding='utf-8') with pytest.raises(cmdutils.CommandError) as excinfo: commands.config_source() @@ -529,7 +532,8 @@ class TestSource: def test_invalid_source(self, commands, config_tmpdir): pyfile = config_tmpdir / 'config.py' - pyfile.write_text('1/0', encoding='utf-8') + pyfile.write_text('\n'.join(['config.load_autoconfig(False)', + '1/0']), encoding='utf-8') with pytest.raises(cmdutils.CommandError) as excinfo: commands.config_source() @@ -571,7 +575,8 @@ class TestEdit: def test_with_sourcing(self, commands, config_stub, patch_editor): assert config_stub.val.content.javascript.enabled - mock = patch_editor('c.content.javascript.enabled = False') + mock = patch_editor('\n'.join(['config.load_autoconfig(False)', + 'c.content.javascript.enabled = False'])) commands.config_edit() @@ -580,16 +585,16 @@ class TestEdit: def test_config_py_with_sourcing(self, commands, config_stub, patch_editor, config_py_arg): assert config_stub.val.content.javascript.enabled - conf = 'c.content.javascript.enabled = False' - mock = patch_editor(conf) + conf = ['config.load_autoconfig(False)', 'c.content.javascript.enabled = False'] + mock = patch_editor("\n".join(conf)) commands.config_edit() mock.assert_called_once_with(unittest.mock.ANY) assert not config_stub.val.content.javascript.enabled - assert config_py_arg.read_text('utf-8').splitlines() == [conf] + assert config_py_arg.read_text('utf-8').splitlines() == conf def test_error(self, commands, config_stub, patch_editor, message_mock, caplog): - patch_editor('c.foo = 42') + patch_editor('\n'.join(['config.load_autoconfig(False)', 'c.foo = 42'])) with caplog.at_level(logging.ERROR): commands.config_edit() diff --git a/tests/unit/config/test_configfiles.py b/tests/unit/config/test_configfiles.py index bef4ef004..11808e2c2 100644 --- a/tests/unit/config/test_configfiles.py +++ b/tests/unit/config/test_configfiles.py @@ -659,6 +659,7 @@ class ConfPy: def __init__(self, tmpdir, filename: str = "config.py"): self._file = tmpdir / filename self.filename = str(self._file) + config.instance.warn_autoconfig = False def write(self, *lines): text = '\n'.join(lines) @@ -954,6 +955,19 @@ class TestConfigPy: # points at the location *after* the + assert " ^" in tblines or " ^" in tblines + def test_load_autoconfig_warning(self, confpy): + confpy.write('') + config.instance.warn_autoconfig = True + with pytest.raises(configexc.ConfigFileErrors) as excinfo: + configfiles.read_config_py(confpy.filename) + assert len(excinfo.value.errors) == 1 + error = excinfo.value.errors[0] + assert error.text == "autoconfig loading not specified" + exception_text = ('Your config.py should call either `config.load_autoconfig()`' + ' (to load settings configured via the GUI) or ' + '`config.load_autoconfig(False)` (to not do so)') + assert str(error.exception) == exception_text + def test_unhandled_exception(self, confpy): confpy.write("1/0") error = confpy.read(error=True) @@ -1144,8 +1158,8 @@ class TestConfigPyWriter: # qute://help/configuring.html # qute://help/settings.html - # Uncomment this to still load settings configured via autoconfig.yml - # config.load_autoconfig() + # Change the argument to True to still load settings configured via autoconfig.yml + config.load_autoconfig(False) # This is an option description. Nullam eu ante vel est convallis # dignissim. Fusce suscipit, wisi nec facilisis facilisis, est dui @@ -1197,7 +1211,7 @@ class TestConfigPyWriter: lines = list(writer._gen_lines()) assert "## Autogenerated config.py" in lines - assert "# config.load_autoconfig()" in lines + assert "# config.load_autoconfig(True)" in lines assert "# c.opt = 'val'" in lines assert "## Bindings for normal mode" in lines assert "# config.bind(',x', 'message-info normal')" in lines @@ -1246,7 +1260,7 @@ class TestConfigPyWriter: commented=False) lines = list(writer._gen_lines()) assert lines[0] == '# Autogenerated config.py' - assert lines[-2] == '# config.load_autoconfig()' + assert lines[-2] == 'config.load_autoconfig(False)' assert not lines[-1] def test_pattern(self): diff --git a/tests/unit/config/test_configinit.py b/tests/unit/config/test_configinit.py index 8381456e1..6b44196b6 100644 --- a/tests/unit/config/test_configinit.py +++ b/tests/unit/config/test_configinit.py @@ -63,7 +63,8 @@ def configdata_init(monkeypatch): class TestEarlyInit: def test_config_py_path(self, args, init_patch, config_py_arg): - config_py_arg.write('c.colors.hints.bg = "red"\n') + config_py_arg.write('\n'.join(['config.load_autoconfig()', + 'c.colors.hints.bg = "red"'])) configinit.early_init(args) expected = 'colors.hints.bg = red' assert config.instance.dump_userconfig() == expected @@ -75,7 +76,8 @@ class TestEarlyInit: config_py_file = config_tmpdir / 'config.py' if config_py: - config_py_lines = ['c.colors.hints.bg = "red"'] + config_py_lines = ['c.colors.hints.bg = "red"', + 'config.load_autoconfig(False)'] if config_py == 'error': config_py_lines.append('c.foo = 42') config_py_file.write_text('\n'.join(config_py_lines), @@ -147,8 +149,7 @@ class TestEarlyInit: if config_py: config_py_lines = ['c.colors.hints.bg = "red"'] - if load_autoconfig: - config_py_lines.append('config.load_autoconfig()') + config_py_lines.append('config.load_autoconfig({})'.format(load_autoconfig)) if config_py == 'error': config_py_lines.append('c.foo = 42') config_py_file.write_text('\n'.join(config_py_lines), @@ -310,6 +311,7 @@ class TestLateInit: elif method == 'py': config_py_file = config_tmpdir / 'config.py' lines = ["c.{} = '{}'".format(k, v) for k, v in settings] + lines.append("config.load_autoconfig(False)") config_py_file.write_text('\n'.join(lines), 'utf-8', ensure=True) configinit.early_init(args) diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index a98584164..b637ec13c 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -19,6 +19,7 @@ """Tests for qutebrowser.config.configtypes.""" import re +import sys import json import math import warnings @@ -248,16 +249,11 @@ class TestAll: configtypes.PercOrInt, # ditto ]: return - elif (isinstance(typ, functools.partial) and - isinstance(typ.func, (configtypes.ListOrValue, - configtypes.List))): + elif (isinstance(klass, functools.partial) and + klass.func in [configtypes.ListOrValue, configtypes.List]): # ListOrValue: "- /" -> "/" # List: "- /" -> ["/"] return - elif (isinstance(typ, configtypes.ListOrValue) and - isinstance(typ.valtype, configtypes.Int)): - # "00" -> "0" - return assert converted == s @@ -1487,27 +1483,19 @@ class TestRegex: @pytest.mark.parametrize('val', [ pytest.param(r'(foo|bar))?baz[fis]h', id='unmatched parens'), pytest.param('(' * 500, id='too many parens'), + pytest.param(r'foo\Xbar', id='invalid escape X'), + pytest.param(r'foo\Cbar', id='invalid escape C'), + pytest.param(r'[[]]', id='nested set', marks=pytest.mark.skipif( + sys.hexversion < 0x03070000, + reason="Warning was added in Python 3.7")), + pytest.param(r'[a||b]', id='set operation', marks=pytest.mark.skipif( + sys.hexversion < 0x03070000, + reason="Warning was added in Python 3.7")), ]) def test_to_py_invalid(self, klass, val): with pytest.raises(configexc.ValidationError): klass().to_py(val) - @pytest.mark.parametrize('val', [ - r'foo\Xbar', - r'foo\Cbar', - ]) - def test_to_py_maybe_valid(self, klass, val): - """Those values are valid on some Python versions (and systems?). - - On others, they raise a DeprecationWarning because of an invalid - escape. This tests makes sure this gets translated to a - ValidationError. - """ - try: - klass().to_py(val) - except configexc.ValidationError: - pass - @pytest.mark.parametrize('warning', [ Warning('foo'), DeprecationWarning('foo'), ]) @@ -1523,20 +1511,6 @@ class TestRegex: with pytest.raises(type(warning)): regex.to_py('foo') - def test_bad_pattern_warning(self, mocker, klass): - """Test a simulated bad pattern warning. - - This only seems to happen with Python 3.5, so we simulate this for - better coverage. - """ - regex = klass() - m = mocker.patch('qutebrowser.config.configtypes.re') - m.compile.side_effect = lambda *args: warnings.warn(r'bad escape \C', - DeprecationWarning) - m.error = re.error - with pytest.raises(configexc.ValidationError): - regex.to_py('foo') - @pytest.mark.parametrize('flags, expected', [ (None, 0), ('IGNORECASE', re.IGNORECASE), diff --git a/tests/unit/config/test_qtargs.py b/tests/unit/config/test_qtargs.py index 0b3cc7c2b..a3a7f910d 100644 --- a/tests/unit/config/test_qtargs.py +++ b/tests/unit/config/test_qtargs.py @@ -22,8 +22,8 @@ import os import pytest from qutebrowser import qutebrowser -from qutebrowser.config import qtargs, configdata -from qutebrowser.utils import usertypes, version +from qutebrowser.config import qtargs +from qutebrowser.utils import usertypes from helpers import utils @@ -134,18 +134,24 @@ class TestQtArgs: assert '--disable-in-process-stack-traces' in args assert '--enable-in-process-stack-traces' not in args - @pytest.mark.parametrize('flags, added', [ - ([], False), - (['--debug-flag', 'chromium'], True), + @pytest.mark.parametrize('flags, args', [ + ([], []), + (['--debug-flag', 'chromium'], ['--enable-logging', '--v=1']), + (['--debug-flag', 'wait-renderer-process'], ['--renderer-startup-dialog']), ]) - def test_chromium_debug(self, monkeypatch, parser, flags, added): + def test_chromium_flags(self, monkeypatch, parser, flags, args): monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine) parsed = parser.parse_args(flags) args = qtargs.qt_args(parsed) - for arg in ['--enable-logging', '--v=1']: - assert (arg in args) == added + if args: + for arg in args: + assert arg in args + else: + assert '--enable-logging' not in args + assert '--v=1' not in args + assert '--renderer-startup-dialog' not in args @pytest.mark.parametrize('config, added', [ ('none', False), @@ -381,125 +387,23 @@ class TestQtArgs: assert combined_flag in args assert overlay_flag not in args - @utils.qt514 + @utils.qt510 def test_blink_settings(self, config_stub, monkeypatch, parser): + from qutebrowser.browser.webengine import darkmode monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine) - monkeypatch.setattr(qtargs.qtutils, 'version_check', - lambda version, exact=False, compiled=True: - True) + monkeypatch.setattr(darkmode, '_variant', + lambda: darkmode.Variant.qt_515_2) config_stub.val.colors.webpage.darkmode.enabled = True parsed = parser.parse_args([]) args = qtargs.qt_args(parsed) - assert '--blink-settings=darkModeEnabled=true' in args - - -class TestDarkMode: + expected = ('--blink-settings=forceDarkModeEnabled=true,' + 'forceDarkModeImagePolicy=2') - pytestmark = utils.qt514 - - @pytest.fixture(autouse=True) - def patch_backend(self, monkeypatch): - monkeypatch.setattr(qtargs.objects, 'backend', - usertypes.Backend.QtWebEngine) - - @pytest.mark.parametrize('settings, new_qt, expected', [ - # Disabled - ({}, True, []), - ({}, False, []), - - # Enabled without customization - ( - {'enabled': True}, - True, - [('darkModeEnabled', 'true')] - ), - ( - {'enabled': True}, - False, - [('darkMode', '4')] - ), - - # Algorithm - ( - {'enabled': True, 'algorithm': 'brightness-rgb'}, - True, - [('darkModeEnabled', 'true'), - ('darkModeInversionAlgorithm', '2')], - ), - ( - {'enabled': True, 'algorithm': 'brightness-rgb'}, - False, - [('darkMode', '2')], - ), - - ]) - def test_basics(self, config_stub, monkeypatch, - settings, new_qt, expected): - for k, v in settings.items(): - config_stub.set_obj('colors.webpage.darkmode.' + k, v) - monkeypatch.setattr(qtargs.qtutils, 'version_check', - lambda version, exact=False, compiled=True: - new_qt) - - assert list(qtargs._darkmode_settings()) == expected - - @pytest.mark.parametrize('setting, value, exp_key, exp_val', [ - ('contrast', -0.5, - 'darkModeContrast', '-0.5'), - ('policy.page', 'smart', - 'darkModePagePolicy', '1'), - ('policy.images', 'smart', - 'darkModeImagePolicy', '2'), - ('threshold.text', 100, - 'darkModeTextBrightnessThreshold', '100'), - ('threshold.background', 100, - 'darkModeBackgroundBrightnessThreshold', '100'), - ('grayscale.all', True, - 'darkModeGrayscale', 'true'), - ('grayscale.images', 0.5, - 'darkModeImageGrayscale', '0.5'), - ]) - def test_customization(self, config_stub, monkeypatch, - setting, value, exp_key, exp_val): - config_stub.val.colors.webpage.darkmode.enabled = True - config_stub.set_obj('colors.webpage.darkmode.' + setting, value) - monkeypatch.setattr(qtargs.qtutils, 'version_check', - lambda version, exact=False, compiled=True: - True) - - expected = [('darkModeEnabled', 'true'), (exp_key, exp_val)] - assert list(qtargs._darkmode_settings()) == expected - - def test_new_chromium(self): - """Fail if we encounter an unknown Chromium version. - - Dark mode in Chromium currently is undergoing various changes (as it's - relatively recent), and Qt 5.15 is supposed to update the underlying - Chromium at some point. - - Make this test fail deliberately with newer Chromium versions, so that - we can test whether dark mode still works manually, and adjust if not. - """ - assert version._chromium_version() in [ - 'unavailable', # QtWebKit - '77.0.3865.129', # Qt 5.14 - '80.0.3987.163', # Qt 5.15 - ] - - def test_options(self, configdata_init): - """Make sure all darkmode options have the right attributes set.""" - for name, opt in configdata.DATA.items(): - if not name.startswith('colors.webpage.darkmode.'): - continue - - backends = {'QtWebEngine': 'Qt 5.14', 'QtWebKit': False} - assert not opt.supports_pattern, name - assert opt.restart, name - assert opt.raw_backends == backends, name + assert expected in args class TestEnvVars: diff --git a/tests/unit/javascript/stylesheet/test_appendchild.js b/tests/unit/javascript/stylesheet/test_appendchild.js index d1deadba6..aa1294cb3 100644 --- a/tests/unit/javascript/stylesheet/test_appendchild.js +++ b/tests/unit/javascript/stylesheet/test_appendchild.js @@ -9,37 +9,37 @@ var iframe, object; // svg iframe iframe = document.createElement('iframe'); iframe.onload = function () { kungFuDeathGrip.title += '1' }; -iframe.src = "svg.xml"; +// iframe.src = "svg.xml"; kungFuDeathGrip.appendChild(iframe); // object iframe object = document.createElement('object'); object.onload = function () { kungFuDeathGrip.title += '2' }; -object.data = "svg.xml"; +// object.data = "svg.xml"; kungFuDeathGrip.appendChild(object); // xml iframe iframe = document.createElement('iframe'); iframe.onload = function () { kungFuDeathGrip.title += '3' }; -iframe.src = "empty.xml"; +// iframe.src = "empty.xml"; kungFuDeathGrip.appendChild(iframe); // html iframe iframe = document.createElement('iframe'); iframe.onload = function () { kungFuDeathGrip.title += '4' }; -iframe.src = "empty.html"; +// iframe.src = "empty.html"; kungFuDeathGrip.appendChild(iframe); // html iframe iframe = document.createElement('iframe'); iframe.onload = function () { kungFuDeathGrip.title += '5' }; -iframe.src = "xhtml.1"; +// iframe.src = "xhtml.1"; kungFuDeathGrip.appendChild(iframe); // html iframe iframe = document.createElement('iframe'); iframe.onload = function () { kungFuDeathGrip.title += '6' }; -iframe.src = "xhtml.2"; +// iframe.src = "xhtml.2"; kungFuDeathGrip.appendChild(iframe); // html iframe iframe = document.createElement('iframe'); iframe.onload = function () { kungFuDeathGrip.title += '7' }; -iframe.src = "xhtml.3"; +// iframe.src = "xhtml.3"; kungFuDeathGrip.appendChild(iframe); // add the lot to the document diff --git a/tests/unit/misc/test_checkpyver.py b/tests/unit/misc/test_checkpyver.py index 5202efd07..2d4da12e8 100644 --- a/tests/unit/misc/test_checkpyver.py +++ b/tests/unit/misc/test_checkpyver.py @@ -28,21 +28,22 @@ import pytest from qutebrowser.misc import checkpyver -TEXT = (r"At least Python 3.5.2 is required to run qutebrowser, but it's " +TEXT = (r"At least Python 3.6 is required to run qutebrowser, but it's " r"running with \d+\.\d+\.\d+.") @pytest.mark.not_frozen -def test_python2(): - """Run checkpyver with python 2.""" +@pytest.mark.parametrize('python', ['python2', 'python3.5']) +def test_old_python(python): + """Run checkpyver with old python versions.""" try: proc = subprocess.run( - ['python2', checkpyver.__file__, '--no-err-windows'], + [python, checkpyver.__file__, '--no-err-windows'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False) except FileNotFoundError: - pytest.skip("python2 not found") + pytest.skip(f"{python} not found") assert not proc.stdout stderr = proc.stderr.decode('utf-8').rstrip() assert re.fullmatch(TEXT, stderr), stderr diff --git a/tests/unit/misc/test_editor.py b/tests/unit/misc/test_editor.py index 323ac1b21..694e6d204 100644 --- a/tests/unit/misc/test_editor.py +++ b/tests/unit/misc/test_editor.py @@ -137,17 +137,6 @@ class TestFileHandling: msg = message_mock.getmsg(usertypes.MessageLevel.error) assert msg.text.startswith("Failed to read back edited file: ") - @pytest.fixture - def unwritable_tmp_path(self, tmp_path): - tmp_path.chmod(0) - if os.access(str(tmp_path), os.W_OK): - # Docker container or similar - pytest.skip("File was still writable") - - yield tmp_path - - tmp_path.chmod(0o755) - def test_unwritable(self, monkeypatch, message_mock, editor, unwritable_tmp_path, caplog): """Test file handling when the initial file is not writable.""" diff --git a/tests/unit/utils/test_log.py b/tests/unit/utils/test_log.py index 3ae4c3cfc..8e8fa47a4 100644 --- a/tests/unit/utils/test_log.py +++ b/tests/unit/utils/test_log.py @@ -384,9 +384,9 @@ def test_stub(caplog, suffix, expected): assert caplog.messages == [expected] -def test_ignore_py_warnings(caplog): +def test_py_warning_filter(caplog): logging.captureWarnings(True) - with log.ignore_py_warnings(category=UserWarning): + with log.py_warning_filter(category=UserWarning): warnings.warn("hidden", UserWarning) with caplog.at_level(logging.WARNING): warnings.warn("not hidden", UserWarning) @@ -395,6 +395,21 @@ def test_ignore_py_warnings(caplog): assert msg.endswith("UserWarning: not hidden") +def test_py_warning_filter_error(caplog): + warnings.simplefilter('ignore') + warnings.warn("hidden", UserWarning) + + with log.py_warning_filter('error'): + with pytest.raises(UserWarning): + warnings.warn("error", UserWarning) + + +def test_warning_still_errors(): + # Mainly a sanity check after the tests messing with warnings above. + with pytest.raises(UserWarning): + warnings.warn("error", UserWarning) + + class TestQtMessageHandler: @attr.s diff --git a/tests/unit/utils/test_standarddir.py b/tests/unit/utils/test_standarddir.py index 064c51b30..ea65b7cc4 100644 --- a/tests/unit/utils/test_standarddir.py +++ b/tests/unit/utils/test_standarddir.py @@ -308,14 +308,12 @@ class TestInitCacheDirTag: # http://www.brynosaurus.com/cachedir/ """).lstrip() - def test_open_oserror(self, caplog, tmpdir, mocker, monkeypatch): + def test_open_oserror(self, caplog, unwritable_tmp_path, monkeypatch): """Test creating a new CACHEDIR.TAG.""" - monkeypatch.setattr(standarddir, 'cache', lambda: str(tmpdir)) - mocker.patch('builtins.open', side_effect=OSError) + monkeypatch.setattr(standarddir, 'cache', lambda: str(unwritable_tmp_path)) with caplog.at_level(logging.ERROR, 'init'): standarddir._init_cachedir_tag() assert caplog.messages == ['Failed to create CACHEDIR.TAG'] - assert not tmpdir.listdir() class TestCreatingDir: diff --git a/tests/unit/utils/test_urlmatch.py b/tests/unit/utils/test_urlmatch.py index 8292a09ad..c38794c40 100644 --- a/tests/unit/utils/test_urlmatch.py +++ b/tests/unit/utils/test_urlmatch.py @@ -28,7 +28,6 @@ Currently not tested: - Any other features we don't need, such as .GetAsString() or set operations. """ -import sys import string import pytest @@ -89,11 +88,7 @@ from qutebrowser.utils import urlmatch ("http://foo:/", "Invalid port: Port is empty"), ("http://*.foo:/", "Invalid port: Port is empty"), ("http://foo:com/", "Invalid port: .* 'com'"), - pytest.param("http://foo:123456/", - "Invalid port: Port out of range 0-65535", - marks=pytest.mark.skipif( - sys.hexversion < 0x03060000, - reason="Doesn't show an error on Python 3.5")), + ("http://foo:123456/", "Invalid port: Port out of range 0-65535"), ("http://foo:80:80/monkey", "Invalid port: .* '80:80'"), ("chrome://foo:1234/bar", "Ports are unsupported with chrome scheme"), # No port specified, but port separator. diff --git a/tests/unit/utils/test_urlutils.py b/tests/unit/utils/test_urlutils.py index 7fd52152c..df9d8b510 100644 --- a/tests/unit/utils/test_urlutils.py +++ b/tests/unit/utils/test_urlutils.py @@ -745,9 +745,6 @@ class TestProxyFromUrl: def test_proxy_from_url_valid(self, url, expected): assert urlutils.proxy_from_url(QUrl(url)) == expected - @pytest.mark.qt_log_ignore( - r'^QHttpNetworkConnectionPrivate::_q_hostLookupFinished could not ' - r'de-queue request, failed to report HostNotFoundError') @pytest.mark.parametrize('scheme', ['pac+http', 'pac+https']) def test_proxy_from_url_pac(self, scheme, qapp): fetcher = urlutils.proxy_from_url(QUrl('{}://foo'.format(scheme))) diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index 3eda4234f..8a07e3411 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -37,6 +37,7 @@ from PyQt5.QtGui import QColor, QClipboard import pytest import hypothesis from hypothesis import strategies +import yaml import qutebrowser import qutebrowser.utils # for test_qualname @@ -560,8 +561,12 @@ class TestIsEnum: def test_enum(self): """Test is_enum with an enum.""" - e = enum.Enum('Foo', 'bar, baz') - assert utils.is_enum(e) + class Foo(enum.Enum): + + bar = enum.auto() + baz = enum.auto() + + assert utils.is_enum(Foo) def test_class(self): """Test is_enum with a non-enum class.""" @@ -852,6 +857,10 @@ class TestYaml: def test_load(self): assert utils.yaml_load("[1, 2]") == [1, 2] + def test_load_float_bug(self): + with pytest.raises(yaml.YAMLError): + utils.yaml_load("._") + def test_load_file(self, tmpdir): tmpfile = tmpdir / 'foo.yml' tmpfile.write('[1, 2]') diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py index 868c4920f..c76a22e56 100644 --- a/tests/unit/utils/test_version.py +++ b/tests/unit/utils/test_version.py @@ -41,7 +41,7 @@ import hypothesis.strategies import qutebrowser from qutebrowser.config import config from qutebrowser.utils import version, usertypes, utils, standarddir -from qutebrowser.misc import pastebin +from qutebrowser.misc import pastebin, objects from qutebrowser.browser import pdfjs @@ -871,39 +871,49 @@ _QTWE_USER_AGENT = ("Mozilla/5.0 (X11; Linux x86_64) " "QtWebEngine/5.14.0 Chrome/{} Safari/537.36") -def test_chromium_version(monkeypatch, caplog): - pytest.importorskip('PyQt5.QtWebEngineWidgets') +class TestChromiumVersion: - ver = '77.0.3865.98' - version.webenginesettings._init_user_agent_str( - _QTWE_USER_AGENT.format(ver)) + @pytest.fixture(autouse=True) + def clear_parsed_ua(self, monkeypatch): + if version.webenginesettings is not None: + # Not available with QtWebKit + monkeypatch.setattr(version.webenginesettings, 'parsed_user_agent', None) - assert version._chromium_version() == ver + def test_fake_ua(self, monkeypatch, caplog): + pytest.importorskip('PyQt5.QtWebEngineWidgets') + ver = '77.0.3865.98' + version.webenginesettings._init_user_agent_str( + _QTWE_USER_AGENT.format(ver)) -def test_chromium_version_no_webengine(monkeypatch): - monkeypatch.setattr(version, 'webenginesettings', None) - assert version._chromium_version() == 'unavailable' + assert version._chromium_version() == ver + def test_no_webengine(self, monkeypatch): + monkeypatch.setattr(version, 'webenginesettings', None) + assert version._chromium_version() == 'unavailable' -def test_chromium_version_prefers_saved_user_agent(monkeypatch): - pytest.importorskip('PyQt5.QtWebEngineWidgets') - version.webenginesettings._init_user_agent_str(_QTWE_USER_AGENT) + def test_prefers_saved_user_agent(self, monkeypatch): + pytest.importorskip('PyQt5.QtWebEngineWidgets') + version.webenginesettings._init_user_agent_str(_QTWE_USER_AGENT) - class FakeProfile: - def defaultProfile(self): - raise AssertionError("Should not be called") + class FakeProfile: + def defaultProfile(self): + raise AssertionError("Should not be called") - monkeypatch.setattr(version.webenginesettings, 'QWebEngineProfile', - FakeProfile()) + monkeypatch.setattr(version.webenginesettings, 'QWebEngineProfile', + FakeProfile()) - version._chromium_version() + version._chromium_version() + def test_unpatched(self, qapp, cache_tmpdir, data_tmpdir, config_stub): + pytest.importorskip('PyQt5.QtWebEngineWidgets') + unexpected = ['', 'unknown', 'unavailable', 'avoided'] + assert version._chromium_version() not in unexpected -def test_chromium_version_unpatched(qapp, cache_tmpdir, data_tmpdir, - config_stub): - pytest.importorskip('PyQt5.QtWebEngineWidgets') - assert version._chromium_version() not in ['', 'unknown', 'unavailable'] + def test_avoided(self, monkeypatch): + pytest.importorskip('PyQt5.QtWebEngineWidgets') + monkeypatch.setattr(objects, 'debug_flags', ['avoid-chromium-init']) + assert version._chromium_version() == 'avoided' @attr.s @@ -18,7 +18,6 @@ setenv = passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER CI XDG_* QUTE_* DOCKER QT_QUICK_BACKEND PY_COLORS basepython = py3: {env:PYTHON:python3} - py35: {env:PYTHON:python3.5} py36: {env:PYTHON:python3.6} py37: {env:PYTHON:python3.7} py38: {env:PYTHON:python3.8} |