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] diff --git a/.flake8 b/.flake8 index 6f49baf99..7709eacaf 100644 --- a/.flake8 +++ b/.flake8 @@ -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 diff --git a/.mypy.ini b/.mypy.ini index 1972e5040..dfef7d3e5 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -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: <> 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: <> 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: <> 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: <> 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 -<>. +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 +<>. 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 +<> 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 +<> 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 <> 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 <> 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 <> 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 profile qutebrowser /usr/{local/,}bin/qutebrowser { - #include + #include + #include + #include + #include + #include + #include + #include + #include + + #include #include #include #include - #include - #include - #include + + #include #include - #include + #include - 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 @@ + 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; + } @@ -68,13 +97,24 @@ const HEADER = ` `; 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 + 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) -> typing.Sequence[str]: + 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) +# +# 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 . + +"""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 @@ Note this warning will only appear once. Use :open qute://warning/sessions to show it again at a later time. -

You're using qutebrowser with Qt 5.15.

+

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.

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

-

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

+

At the time of writing (October 2020), a new session format which stores part of the needed binary data in saved sessions is in development and is expected to be released with qutebrowser v2.0.0 (planned to be released at the end of the year or early 2021).

As a stop-gap measure:

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 . -"""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) -# -# 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 . - -"""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 = "

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

" 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.*)\(.*\)', - sig.signal) # type: ignore[attr-defined] + m = re.fullmatch(r'[0-9]+(?P.*)\(.*\)', 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 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): - def __init__(self, items: typing.Sequence[_T] = None, - default: typing.Union[_T, Unset] = UNSET, + """Behavior for the 'mode' argument.""" + + edge = enum.auto() + exception = enum.auto() + + 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() -# Backend of a tab -Backend = enum.Enum('Backend', ['QtWebKit', 'QtWebEngine']) +class Backend(enum.Enum): + + """The backend being used (usertypes.backend).""" + + 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. + + 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() -MessageLevel = enum.Enum('MessageLevel', ['error', 'warning', 'info']) +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' - ]) + class Type(enum.Enum): - 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 + """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" "$@" & diff --git a/setup.py b/setup.py index 0c0bf73b4..1169eae81 100755 --- a/setup.py +++ b/setup.py @@ -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) + +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see . + +import 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 + expected = ('--blink-settings=forceDarkModeEnabled=true,' + 'forceDarkModeImagePolicy=2') - -class TestDarkMode: - - 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 diff --git a/tox.ini b/tox.ini index a25374672..bfc1f2b49 100644 --- a/tox.ini +++ b/tox.ini @@ -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}