diff options
author | Ander Punnar <ander@kvlt.ee> | 2021-05-20 20:52:42 +0300 |
---|---|---|
committer | Ander Punnar <ander@kvlt.ee> | 2021-05-20 20:52:42 +0300 |
commit | 4e47af21586aad3846c2458dc0c8243e9d6eb792 (patch) | |
tree | 11b2073411b7decc68ba049bdfd4d97e8ab48343 | |
parent | 2fc588358b27153721cc83e728e0d15b0e4d0d22 (diff) | |
parent | 90fbe1e181463379f38b8f0b01d488d1f7a586e0 (diff) | |
download | qutebrowser-4e47af21586aad3846c2458dc0c8243e9d6eb792.tar.gz qutebrowser-4e47af21586aad3846c2458dc0c8243e9d6eb792.zip |
Merge remote-tracking branch 'origin/master' into 4nd3r/hostblock_subdomains
62 files changed, 385 insertions, 162 deletions
diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 6b05fd3bc..c0e16391b 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.2.1 +current_version = 2.2.2 commit = True message = Release v{new_version} tag = True diff --git a/.github/workflows/bleeding.yml b/.github/workflows/bleeding.yml index 5d464e3ac..746810cb8 100644 --- a/.github/workflows/bleeding.yml +++ b/.github/workflows/bleeding.yml @@ -78,6 +78,9 @@ jobs: sed -i '' '/.-d., .--debug.,/s/$/ default=True,/' qutebrowser/qutebrowser.py - name: Run tox run: "tox -e build-release -- --asciidoc ../asciidoc/asciidoc.py --gh-token ${{ secrets.GITHUB_TOKEN }} ${{ matrix.args }}" + - name: Wait 90s to avoid upload errors + if: "contains(matrix.args, '--32bit')" + run: "sleep 90" - name: Upload artifacts uses: actions/upload-artifact@v2 with: @@ -3,7 +3,7 @@ python_version = 3.6 ### --strict warn_unused_configs = True -# disallow_any_generics = True +disallow_any_generics = True disallow_subclassing_any = True # disallow_untyped_calls = True # disallow_untyped_defs = True @@ -83,6 +83,10 @@ disallow_untyped_defs = True [mypy-qutebrowser.config.*] disallow_untyped_defs = True +[mypy-qutebrowser.config.configtypes] +# Needs some major work to use specific generics +disallow_any_generics = False + [mypy-qutebrowser.api.*] disallow_untyped_defs = True diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 149b4f9ee..428cf22f8 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -15,8 +15,18 @@ 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. +[[v2.3.0]] +v2.3.0 (unreleased) +------------------- + +Changed +~~~~~~~ + +The `fonts.web.*` settings now support URL patterns. + + [[v2.2.2]] -v2.2.2 (unreleased) +v2.2.2 (2021-05-20) ------------------- Fixed @@ -25,6 +35,14 @@ Fixed - When awesomewm's "naughty" notification daemon was used with a development version of AwesomeWM and an unknown version number, qutebrowser would crash when trying to parse the version string. This is now fixed. +- Due to a bug with QtWebEngine 5.15.4, old Service Worker data could cause + renderer process crashes. This is now worked around by qutebrowser. +- When an (broken) binding to `set-cmd-text` without any argument existed, + using `:` would crash, which is now fixed. +- New site-specific quirk (again) working around not being able to type + accented/composed characters on Google Docs. +- When running with `python -OO` (which is not recommended), a notification + being shown would result in a crash, which is now fixed. [[v2.2.1]] v2.2.1 (2021-04-29) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 42eacd175..fb208e48c 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -2692,6 +2692,7 @@ Valid values: * +ua-whatsapp+ * +ua-google+ * +ua-slack+ + * +ua-googledocs+ * +js-whatsapp-web+ * +js-discord+ * +js-string-replaceall+ @@ -3088,6 +3089,8 @@ Default: +pass:[default_size default_family]+ === fonts.web.family.cursive Font family for cursive fonts. +This setting supports URL patterns. + Type: <<types,FontFamily>> Default: empty @@ -3096,6 +3099,8 @@ Default: empty === fonts.web.family.fantasy Font family for fantasy fonts. +This setting supports URL patterns. + Type: <<types,FontFamily>> Default: empty @@ -3104,6 +3109,8 @@ Default: empty === fonts.web.family.fixed Font family for fixed fonts. +This setting supports URL patterns. + Type: <<types,FontFamily>> Default: empty @@ -3112,6 +3119,8 @@ Default: empty === fonts.web.family.sans_serif Font family for sans-serif fonts. +This setting supports URL patterns. + Type: <<types,FontFamily>> Default: empty @@ -3120,6 +3129,8 @@ Default: empty === fonts.web.family.serif Font family for serif fonts. +This setting supports URL patterns. + Type: <<types,FontFamily>> Default: empty @@ -3128,6 +3139,8 @@ Default: empty === fonts.web.family.standard Font family for standard fonts. +This setting supports URL patterns. + Type: <<types,FontFamily>> Default: empty @@ -3136,6 +3149,8 @@ Default: empty === fonts.web.size.default Default font size (in pixels) for regular text. +This setting supports URL patterns. + Type: <<types,Int>> Default: +pass:[16]+ @@ -3144,6 +3159,8 @@ Default: +pass:[16]+ === fonts.web.size.default_fixed Default font size (in pixels) for fixed-pitch text. +This setting supports URL patterns. + Type: <<types,Int>> Default: +pass:[13]+ @@ -3152,6 +3169,8 @@ Default: +pass:[13]+ === fonts.web.size.minimum Hard minimum font size (in pixels). +This setting supports URL patterns. + Type: <<types,Int>> Default: +pass:[0]+ @@ -3160,6 +3179,8 @@ Default: +pass:[0]+ === fonts.web.size.minimum_logical Minimum logical font size (in pixels) that is applied when zooming out. +This setting supports URL patterns. + Type: <<types,Int>> Default: +pass:[6]+ diff --git a/misc/org.qutebrowser.qutebrowser.appdata.xml b/misc/org.qutebrowser.qutebrowser.appdata.xml index 6950c45eb..95602b476 100644 --- a/misc/org.qutebrowser.qutebrowser.appdata.xml +++ b/misc/org.qutebrowser.qutebrowser.appdata.xml @@ -44,6 +44,7 @@ </content_rating> <releases> <!-- Add new releases here --> +<release version="2.2.2" date="2021-05-20"/> <release version="2.2.1" date="2021-04-29"/> <release version="2.2.0" date="2021-04-13"/> <release version="2.1.1" date="2021-04-01"/> diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt index 48417717b..2621579f6 100644 --- a/misc/requirements/requirements-dev.txt +++ b/misc/requirements/requirements-dev.txt @@ -6,7 +6,7 @@ cffi==1.14.5 chardet==4.0.0 cryptography==3.4.7 github3.py==2.0.0 -hunter==3.3.2 +hunter==3.3.3 idna==2.10 jwcrypto==0.8 manhole==1.8.0 @@ -14,11 +14,11 @@ packaging==20.9 pycparser==2.20 Pympler==0.9 pyparsing==2.4.7 -PyQt-builder==1.9.1 +PyQt-builder==1.10.0 python-dateutil==2.8.1 requests==2.25.1 -sip==6.0.3 -six==1.15.0 +sip==6.1.0 +six==1.16.0 toml==0.10.2 uritemplate==3.0.1 # urllib3==1.26.4 diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt index 7f6c5feaf..429d04dfe 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.3.0 -flake8==3.9.1 +attrs==21.2.0 +flake8==3.9.2 flake8-bugbear==21.4.3 flake8-builtins==1.5.3 -flake8-comprehensions==3.4.0 +flake8-comprehensions==3.5.0 flake8-copyright==0.2.2 flake8-debugger==4.0.0 flake8-deprecated==1.3 @@ -13,12 +13,12 @@ flake8-future-import==0.4.6 flake8-mock==0.3 flake8-polyfill==1.0.2 flake8-string-format==0.3.0 -flake8-tidy-imports==4.2.1 +flake8-tidy-imports==4.3.0 flake8-tuple==0.4.1 mccabe==0.6.1 pep8-naming==0.11.1 pycodestyle==2.7.0 pydocstyle==6.0.0 pyflakes==2.3.1 -six==1.15.0 +six==1.16.0 snowballstemmer==2.1.0 diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt index f43386414..7d3d29e63 100644 --- a/misc/requirements/requirements-mypy.txt +++ b/misc/requirements/requirements-mypy.txt @@ -1,18 +1,18 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py chardet==4.0.0 -diff-cover==5.0.1 +diff-cover==5.1.1 importlib-metadata==4.0.1 -importlib-resources==5.1.2 +importlib-resources==5.1.3 inflect==5.3.0 -Jinja2==2.11.3 +Jinja2==3.0.0 jinja2-pluralize==0.3.0 lxml==4.6.3 -MarkupSafe==1.1.1 +MarkupSafe==2.0.0 mypy==0.812 mypy-extensions==0.4.3 pluggy==0.13.1 -Pygments==2.8.1 +Pygments==2.9.0 PyQt5-stubs==5.15.2.0 typed-ast==1.4.3 typing-extensions==3.10.0.0 diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index ac3215255..13358f6d5 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -12,13 +12,13 @@ isort==4.3.21 jwcrypto==0.8 lazy-object-proxy==1.4.3 mccabe==0.6.1 -pefile==2019.4.18 +pefile==2021.5.13 pycparser==2.20 pylint==2.4.4 # rq.filter: < 2.5 python-dateutil==2.8.1 ./scripts/dev/pylint_checkers requests==2.25.1 -six==1.15.0 +six==1.16.0 typed-ast==1.4.3 ; python_version<"3.8" uritemplate==3.0.1 # urllib3==1.26.4 diff --git a/misc/requirements/requirements-pyqt-5.12.txt b/misc/requirements/requirements-pyqt-5.12.txt index 80a700f09..890306127 100644 --- a/misc/requirements/requirements-pyqt-5.12.txt +++ b/misc/requirements/requirements-pyqt-5.12.txt @@ -1,5 +1,5 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py PyQt5==5.12.3 # rq.filter: < 5.13 -PyQt5-sip==12.8.1 +PyQt5-sip==12.9.0 PyQtWebEngine==5.12.1 # rq.filter: < 5.13 diff --git a/misc/requirements/requirements-pyqt-5.13.txt b/misc/requirements/requirements-pyqt-5.13.txt index 438c600da..5f4da4758 100644 --- a/misc/requirements/requirements-pyqt-5.13.txt +++ b/misc/requirements/requirements-pyqt-5.13.txt @@ -1,5 +1,5 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py PyQt5==5.13.2 # rq.filter: < 5.14 -PyQt5-sip==12.8.1 +PyQt5-sip==12.9.0 PyQtWebEngine==5.13.2 # rq.filter: < 5.14 diff --git a/misc/requirements/requirements-pyqt-5.14.txt b/misc/requirements/requirements-pyqt-5.14.txt index d515e717f..9ce643666 100644 --- a/misc/requirements/requirements-pyqt-5.14.txt +++ b/misc/requirements/requirements-pyqt-5.14.txt @@ -1,5 +1,5 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py PyQt5==5.14.2 # rq.filter: < 5.15 -PyQt5-sip==12.8.1 +PyQt5-sip==12.9.0 PyQtWebEngine==5.14.0 # rq.filter: < 5.15 diff --git a/misc/requirements/requirements-pyqt-5.15.0.txt b/misc/requirements/requirements-pyqt-5.15.0.txt index b9ee53f65..b111a93f3 100644 --- a/misc/requirements/requirements-pyqt-5.15.0.txt +++ b/misc/requirements/requirements-pyqt-5.15.0.txt @@ -1,5 +1,5 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py PyQt5==5.15.0 # rq.filter: == 5.15.0 -PyQt5-sip==12.8.1 +PyQt5-sip==12.9.0 PyQtWebEngine==5.15.0 # rq.filter: == 5.15.0 diff --git a/misc/requirements/requirements-pyqt-5.15.txt b/misc/requirements/requirements-pyqt-5.15.txt index a5b3a5787..8b7a53c44 100644 --- a/misc/requirements/requirements-pyqt-5.15.txt +++ b/misc/requirements/requirements-pyqt-5.15.txt @@ -2,6 +2,6 @@ PyQt5==5.15.4 # rq.filter: < 5.16 PyQt5-Qt5==5.15.2 -PyQt5-sip==12.8.1 +PyQt5-sip==12.9.0 PyQtWebEngine==5.15.4 # rq.filter: < 5.16 PyQtWebEngine-Qt5==5.15.2 diff --git a/misc/requirements/requirements-pyqt-pyinstaller.txt b/misc/requirements/requirements-pyqt-pyinstaller.txt index 31ecefad5..678a1d7ea 100644 --- a/misc/requirements/requirements-pyqt-pyinstaller.txt +++ b/misc/requirements/requirements-pyqt-pyinstaller.txt @@ -2,6 +2,6 @@ PyQt5==5.15.3 PyQt5-Qt==5.15.2 -PyQt5-sip==12.8.1 +PyQt5-sip==12.9.0 PyQtWebEngine==5.15.3 PyQtWebEngine-Qt==5.15.2 diff --git a/misc/requirements/requirements-pyqt.txt b/misc/requirements/requirements-pyqt.txt index 7e28f7dc2..75ef27bf4 100644 --- a/misc/requirements/requirements-pyqt.txt +++ b/misc/requirements/requirements-pyqt.txt @@ -2,6 +2,6 @@ PyQt5==5.15.4 PyQt5-Qt5==5.15.2 -PyQt5-sip==12.8.1 +PyQt5-sip==12.9.0 PyQtWebEngine==5.15.4 PyQtWebEngine-Qt5==5.15.2 diff --git a/misc/requirements/requirements-pyroma.txt b/misc/requirements/requirements-pyroma.txt index 16d92bd0e..feceac972 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.17.1 -Pygments==2.8.1 +Pygments==2.9.0 pyroma==3.1 diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt index 403fc2903..2aa55e0a3 100644 --- a/misc/requirements/requirements-sphinx.txt +++ b/misc/requirements/requirements-sphinx.txt @@ -4,18 +4,18 @@ alabaster==0.7.12 Babel==2.9.1 certifi==2020.12.5 chardet==4.0.0 -docutils==0.16 +docutils==0.17.1 idna==2.10 imagesize==1.2.0 Jinja2==2.11.3 MarkupSafe==1.1.1 packaging==20.9 -Pygments==2.8.1 +Pygments==2.9.0 pyparsing==2.4.7 pytz==2021.1 requests==2.25.1 snowballstemmer==2.1.0 -Sphinx==3.5.4 +Sphinx==4.0.1 sphinxcontrib-applehelp==1.0.2 sphinxcontrib-devhelp==1.0.2 sphinxcontrib-htmlhelp==1.0.3 diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 5adf6a324..6c83c4fb7 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -1,29 +1,29 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py apipkg==1.5 -attrs==20.3.0 +attrs==21.2.0 beautifulsoup4==4.9.3 certifi==2020.12.5 chardet==4.0.0 cheroot==8.5.2 -click==7.1.2 +click==8.0.0 coverage==5.5 EasyProcess==0.3 execnet==1.8.0 filelock==3.0.12 -Flask==1.1.2 +Flask==2.0.0 glob2==0.7 -hunter==3.3.2 -hypothesis==6.10.1 +hunter==3.3.3 +hypothesis==6.12.0 icdiff==1.9.1 idna==2.10 iniconfig==1.1.1 -itsdangerous==1.1.0 +itsdangerous==2.0.0 jaraco.functools==3.3.0 -# Jinja2==2.11.3 +# Jinja2==3.0.0 Mako==1.1.4 manhole==1.8.0 -# MarkupSafe==1.1.1 +# MarkupSafe==2.0.0 more-itertools==8.7.0 packaging==20.9 parse==1.19.0 @@ -32,16 +32,16 @@ pluggy==0.13.1 pprintpp==0.4.0 py==1.10.0 py-cpuinfo==8.0.0 -Pygments==2.8.1 +Pygments==2.9.0 pyparsing==2.4.7 -pytest==6.2.3 +pytest==6.2.4 pytest-bdd==4.0.2 pytest-benchmark==3.4.1 -pytest-cov==2.11.1 +pytest-cov==2.12.0 pytest-forked==1.3.0 pytest-icdiff==0.5 pytest-instafail==0.4.2 -pytest-mock==3.6.0 +pytest-mock==3.6.1 pytest-qt==3.3.0 pytest-repeat==0.9.1 pytest-rerunfailures==9.1.1 @@ -50,11 +50,11 @@ pytest-xvfb==2.0.0 PyVirtualDisplay==2.1 requests==2.25.1 requests-file==1.5.1 -six==1.15.0 -sortedcontainers==2.3.0 +six==1.16.0 +sortedcontainers==2.4.0 soupsieve==2.2.1 tldextract==3.1.0 toml==0.10.2 urllib3==1.26.4 vulture==2.3 -Werkzeug==1.0.1 +Werkzeug==2.0.0 diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt index 6361d4f14..e8fd3f633 100644 --- a/misc/requirements/requirements-tox.txt +++ b/misc/requirements/requirements-tox.txt @@ -8,9 +8,9 @@ pip==21.1.1 pluggy==0.13.1 py==1.10.0 pyparsing==2.4.7 -setuptools==56.0.0 -six==1.15.0 +setuptools==56.2.0 +six==1.16.0 toml==0.10.2 -tox==3.23.0 -virtualenv==20.4.4 +tox==3.23.1 +virtualenv==20.4.6 wheel==0.36.2 diff --git a/pytest.ini b/pytest.ini index 8aa49b446..72ad24002 100644 --- a/pytest.ini +++ b/pytest.ini @@ -84,4 +84,6 @@ filterwarnings = ignore:_SixMetaPathImporter\.exec_module\(\) not found; falling back to load_module\(\):ImportWarning ignore:VendorImporter\.find_spec\(\) not found; falling back to find_module\(\):ImportWarning ignore:_SixMetaPathImporter\.find_spec\(\) not found; falling back to find_module\(\):ImportWarning + # https://github.com/ionelmc/python-hunter/issues/97 + ignore:The distutils\.sysconfig module is deprecated, use sysconfig instead:DeprecationWarning faulthandler_timeout = 90 diff --git a/qutebrowser/__init__.py b/qutebrowser/__init__.py index 91a49d5b5..96062d9bd 100644 --- a/qutebrowser/__init__.py +++ b/qutebrowser/__init__.py @@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2021 Florian Bruhin (The Compiler)" __license__ = "GPL" __maintainer__ = __author__ __email__ = "mail@qutebrowser.org" -__version__ = "2.2.1" +__version__ = "2.2.2" __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 85a700c43..73c6a1bc5 100644 --- a/qutebrowser/api/cmdutils.py +++ b/qutebrowser/api/cmdutils.py @@ -105,6 +105,9 @@ def check_exclusive(flags: Iterable[bool], names: Iterable[str]) -> None: raise CommandError("Only one of {} can be given!".format(argstr)) +_CmdHandlerType = Callable[..., Any] + + class register: # noqa: N801,N806 pylint: disable=invalid-name """Decorator to register a new command handler.""" @@ -130,7 +133,7 @@ class register: # noqa: N801,N806 pylint: disable=invalid-name # The arguments to pass to Command. self._kwargs = kwargs - def __call__(self, func: Callable) -> Callable: + def __call__(self, func: _CmdHandlerType) -> _CmdHandlerType: """Register the command before running the function. Gets called when a function should be decorated. @@ -222,7 +225,7 @@ class argument: # noqa: N801,N806 pylint: disable=invalid-name self._argname = argname # The name of the argument to handle. self._kwargs = kwargs # Valid ArgInfo members. - def __call__(self, func: Callable) -> Callable: + def __call__(self, func: _CmdHandlerType) -> _CmdHandlerType: funcname = func.__name__ if self._argname not in inspect.signature(func).parameters: diff --git a/qutebrowser/api/hook.py b/qutebrowser/api/hook.py index eadc310f3..884d6c67f 100644 --- a/qutebrowser/api/hook.py +++ b/qutebrowser/api/hook.py @@ -22,13 +22,13 @@ """Hooks for extensions.""" import importlib -from typing import Callable +from typing import Callable, Any from qutebrowser.extensions import loader -def _add_module_info(func: Callable) -> loader.ModuleInfo: +def _add_module_info(func: Callable[..., Any]) -> 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: Callable) -> Callable: + def __call__(self, func: loader.InitHookType) -> loader.InitHookType: info = _add_module_info(func) if info.init_hook is not None: raise ValueError("init hook is already registered!") @@ -86,7 +86,10 @@ class config_changed: def __init__(self, option_filter: str = None) -> None: self._filter = option_filter - def __call__(self, func: Callable) -> Callable: + def __call__( + self, + func: loader.ConfigChangedHookType, + ) -> loader.ConfigChangedHookType: info = _add_module_info(func) info.config_changed_hooks.append((self._filter, func)) return func diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index cbe698009..b3d1e85f7 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -34,9 +34,10 @@ from PyQt5.QtPrintSupport import QPrintDialog, QPrinter from PyQt5.QtNetwork import QNetworkAccessManager if TYPE_CHECKING: - from PyQt5.QtWebKit import QWebHistory + from PyQt5.QtWebKit import QWebHistory, QWebHistoryItem from PyQt5.QtWebKitWidgets import QWebPage - from PyQt5.QtWebEngineWidgets import QWebEngineHistory, QWebEnginePage + from PyQt5.QtWebEngineWidgets import ( + QWebEngineHistory, QWebEngineHistoryItem, QWebEnginePage) from qutebrowser.keyinput import modeman from qutebrowser.config import config @@ -634,8 +635,8 @@ class AbstractHistoryPrivate: """Deserialize from a format produced by self.serialize.""" raise NotImplementedError - def load_items(self, items: Sequence) -> None: - """Deserialize from a list of WebHistoryItems.""" + def load_items(self, items: Sequence[sessions.TabHistoryItem]) -> None: + """Deserialize from a list of TabHistoryItems.""" raise NotImplementedError @@ -651,7 +652,7 @@ class AbstractHistory: def __len__(self) -> int: raise NotImplementedError - def __iter__(self) -> Iterable: + def __iter__(self) -> Iterable[Union['QWebHistoryItem', 'QWebEngineHistoryItem']]: raise NotImplementedError def _check_count(self, count: int) -> None: diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index e9653ae19..8cd73ae4f 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -602,7 +602,7 @@ class CommandDispatcher: widget = self._current_widget() url = self._current_url() - handlers: Dict[str, Callable] = { + handlers: Dict[str, Callable[..., QUrl]] = { 'prev': functools.partial(navigate.prevnext, prev=True), 'next': functools.partial(navigate.prevnext, prev=False), 'up': navigate.path_up, diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index cfb238188..031b6fe06 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -92,7 +92,8 @@ class Redirect(Exception): # Return value: (mimetype, data) (encoded as utf-8 if a str is returned) _HandlerRet = Tuple[str, Union[str, bytes]] -_Handler = TypeVar('_Handler', bound=Callable[[QUrl], _HandlerRet]) +_HandlerCallable = Callable[[QUrl], _HandlerRet] +_Handler = TypeVar('_Handler', bound=_HandlerCallable) class add_handler: # noqa: N801,N806 pylint: disable=invalid-name @@ -105,7 +106,7 @@ class add_handler: # noqa: N801,N806 pylint: disable=invalid-name def __init__(self, name: str) -> None: self._name = name - self._function: Optional[Callable] = None + self._function: Optional[_HandlerCallable] = None def __call__(self, function: _Handler) -> _Handler: self._function = function diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py index f53ad1afb..9ec29ce07 100644 --- a/qutebrowser/browser/webelem.py +++ b/qutebrowser/browser/webelem.py @@ -58,7 +58,8 @@ def css_selector(group: str, url: QUrl) -> str: return ','.join(selectors[group]) -class AbstractWebElement(collections.abc.MutableMapping): +# MutableMapping is only generic in Python 3.9+ +class AbstractWebElement(collections.abc.MutableMapping): # type: ignore[type-arg] """A wrapper around QtWebKit/QtWebEngine web element.""" diff --git a/qutebrowser/browser/webengine/notification.py b/qutebrowser/browser/webengine/notification.py index 50f73fca4..d8387e6d4 100644 --- a/qutebrowser/browser/webengine/notification.py +++ b/qutebrowser/browser/webengine/notification.py @@ -669,7 +669,8 @@ class _ServerCapabilities: def _as_uint32(x: int) -> QVariant: """Convert the given int to an uint32 for DBus.""" variant = QVariant(x) - assert variant.convert(QVariant.UInt) + successful = variant.convert(QVariant.UInt) + assert successful return variant diff --git a/qutebrowser/browser/webengine/tabhistory.py b/qutebrowser/browser/webengine/tabhistory.py index d6c795fb2..ab4b05fe9 100644 --- a/qutebrowser/browser/webengine/tabhistory.py +++ b/qutebrowser/browser/webengine/tabhistory.py @@ -35,10 +35,10 @@ HISTORY_STREAM_VERSION = 3 def _serialize_item(item, stream): - """Serialize a single WebHistoryItem into a QDataStream. + """Serialize a single TabHistoryItem into a QDataStream. Args: - item: The WebHistoryItem to write. + item: The TabHistoryItem to write. stream: The QDataStream to write to. """ # Thanks to Otter Browser: @@ -108,10 +108,10 @@ def _serialize_item(item, stream): def serialize(items): - """Serialize a list of WebHistoryItems to a data stream. + """Serialize a list of TabHistoryItems to a data stream. Args: - items: An iterable of WebHistoryItems. + items: An iterable of TabHistoryItems. Return: A (stream, data, user_data) tuple. diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 15784d6bf..c793a1929 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -983,6 +983,11 @@ class _Quirk: QWebEngineScript.DocumentCreation) world: QWebEngineScript.ScriptWorldId = QWebEngineScript.MainWorld predicate: bool = True + name: Optional[str] = None + + def __post_init__(self): + if self.name is None: + self.name = f"js-{self.filename.replace('_', '-')}" class _WebEngineScripts(QObject): @@ -1154,6 +1159,11 @@ class _WebEngineScripts(QObject): ), _Quirk('discord'), _Quirk( + 'googledocs', + # will be an UA quirk once we set the JS UA as well + name='ua-googledocs', + ), + _Quirk( 'string_replaceall', predicate=versions.webengine < utils.VersionNumber(5, 15, 3), ), @@ -1171,8 +1181,7 @@ class _WebEngineScripts(QObject): if not quirk.predicate: continue src = resources.read_file(f'javascript/quirks/{quirk.filename}.user.js') - name = f"js-{quirk.filename.replace('_', '-')}" - if name not in config.val.content.site_specific_quirks.skip: + if quirk.name not in config.val.content.site_specific_quirks.skip: self._inject_js( f'quirk_{quirk.filename}', src, diff --git a/qutebrowser/browser/webkit/tabhistory.py b/qutebrowser/browser/webkit/tabhistory.py index c38968b62..a707030d1 100644 --- a/qutebrowser/browser/webkit/tabhistory.py +++ b/qutebrowser/browser/webkit/tabhistory.py @@ -64,10 +64,10 @@ def _serialize_item(item): def serialize(items): - """Serialize a list of WebHistoryItems to a data stream. + """Serialize a list of TabHistoryItems to a data stream. Args: - items: An iterable of WebHistoryItems. + items: An iterable of TabHistoryItems. Return: A (stream, data, user_data) tuple. diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 374019677..437a54a33 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -97,7 +97,10 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name else: return False - def __call__(self, func: Callable) -> Callable: + def __call__( + self, + func: Callable[..., None], + ) -> Callable[..., None]: """Filter calls to the decorated function. Gets called when a function should be decorated. @@ -105,7 +108,9 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name Adds a filter which returns if we're not interested in the change-event and calls the wrapped function if we are. - We assume the function passed doesn't take any parameters. + We assume the function passed doesn't take any parameters. However, it + could take a "self" argument, so we can't cleary express this in the + type above. Args: func: The function to be decorated. @@ -173,6 +178,8 @@ class KeyConfig: result = results[0] if result.cmd.name != "set-cmd-text": return cmdline + if not result.args: + return None # doesn't look like this sets a command *flags, cmd = result.args if "-a" in flags or "--append" in flags or not cmd.startswith(":"): return None # doesn't look like this sets a command @@ -307,7 +314,7 @@ class Config(QObject): def _init_values(self) -> None: """Populate the self._values dict.""" - self._values: Mapping = {} + self._values: Mapping[str, configutils.Values] = {} for name, opt in configdata.DATA.items(): self._values[name] = configutils.Values(opt) diff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py index 2084556da..143b02fca 100644 --- a/qutebrowser/config/configcommands.py +++ b/qutebrowser/config/configcommands.py @@ -21,7 +21,7 @@ import os.path import contextlib -from typing import TYPE_CHECKING, Iterator, List, Optional +from typing import TYPE_CHECKING, Iterator, List, Optional, Any, Tuple from PyQt5.QtCore import QUrl @@ -475,7 +475,7 @@ class ConfigCommands: raise cmdutils.CommandError("{} already exists - use --force to " "overwrite!".format(filename)) - options: List = [] + options: List[Tuple[Optional[urlmatch.UrlPattern], configdata.Option, Any]] = [] if defaults: options = [(None, opt, opt.default) for _name, opt in sorted(configdata.DATA.items())] diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 6cb277e5c..80732d43d 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -545,6 +545,7 @@ content.site_specific_quirks.skip: - ua-whatsapp - ua-google - ua-slack + - ua-googledocs - js-whatsapp-web - js-discord - js-string-replaceall @@ -3269,6 +3270,7 @@ fonts.tabs.unselected: fonts.web.family.standard: default: '' + supports_pattern: true type: name: FontFamily none_ok: true @@ -3276,6 +3278,7 @@ fonts.web.family.standard: fonts.web.family.fixed: default: '' + supports_pattern: true type: name: FontFamily none_ok: true @@ -3283,6 +3286,7 @@ fonts.web.family.fixed: fonts.web.family.serif: default: '' + supports_pattern: true type: name: FontFamily none_ok: true @@ -3290,6 +3294,7 @@ fonts.web.family.serif: fonts.web.family.sans_serif: default: '' + supports_pattern: true type: name: FontFamily none_ok: true @@ -3297,6 +3302,7 @@ fonts.web.family.sans_serif: fonts.web.family.cursive: default: '' + supports_pattern: true type: name: FontFamily none_ok: true @@ -3304,6 +3310,7 @@ fonts.web.family.cursive: fonts.web.family.fantasy: default: '' + supports_pattern: true type: name: FontFamily none_ok: true @@ -3316,6 +3323,7 @@ fonts.web.family.fantasy: fonts.web.size.default: default: 16 + supports_pattern: true type: name: Int minval: 1 @@ -3324,6 +3332,7 @@ fonts.web.size.default: fonts.web.size.default_fixed: default: 13 + supports_pattern: true type: name: Int minval: 1 @@ -3332,6 +3341,7 @@ fonts.web.size.default_fixed: fonts.web.size.minimum: default: 0 + supports_pattern: true type: name: Int minval: 0 @@ -3342,6 +3352,7 @@ fonts.web.size.minimum_logical: # This is 0 as default on QtWebKit, and 6 on QtWebEngine - so let's # just go for 6 here. default: 6 + supports_pattern: true type: name: Int minval: 0 diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index f8566e2d0..6f0d0b13c 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -30,7 +30,7 @@ import configparser import contextlib import re from typing import (TYPE_CHECKING, Any, Dict, Iterable, Iterator, List, Mapping, - MutableMapping, Optional, cast) + MutableMapping, Optional, Tuple, cast) import yaml from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QSettings, qVersion @@ -39,7 +39,7 @@ import qutebrowser from qutebrowser.config import (configexc, config, configdata, configutils, configtypes) from qutebrowser.keyinput import keyutils -from qutebrowser.utils import standarddir, utils, qtutils, log, urlmatch +from qutebrowser.utils import standarddir, utils, qtutils, log, urlmatch, version if TYPE_CHECKING: from qutebrowser.misc import savemanager @@ -89,6 +89,7 @@ class StateConfig(configparser.ConfigParser): self.read(self._filename, encoding='utf-8') self.qt_version_changed = False + self.qtwe_version_changed = False self.qutebrowser_version_changed = VersionChange.unknown self._set_changed_attributes() @@ -108,8 +109,20 @@ class StateConfig(configparser.ConfigParser): self[sect].pop(key, None) self['general']['qt_version'] = qVersion() + self['general']['qtwe_version'] = self._qtwe_version_str() self['general']['version'] = qutebrowser.__version__ + def _qtwe_version_str(self) -> str: + """Get the QtWebEngine version string. + + Note that it's too early to use objects.backend here... + """ + try: + import PyQt5.QtWebEngineWidgets # pylint: disable=unused-import + except ImportError: + return 'no' + return str(version.qtwebengine_versions(avoid_init=True).webengine) + def _set_changed_attributes(self) -> None: """Set qt_version_changed/qutebrowser_version_changed attributes. @@ -123,6 +136,9 @@ class StateConfig(configparser.ConfigParser): old_qt_version = self['general'].get('qt_version', None) self.qt_version_changed = old_qt_version != qVersion() + old_qtwe_version = self['general'].get('qtwe_version', None) + self.qtwe_version_changed = old_qtwe_version != self._qtwe_version_str() + old_qutebrowser_version = self['general'].get('version', None) if old_qutebrowser_version is None: # https://github.com/python/typeshed/issues/2093 @@ -286,18 +302,18 @@ class YamlConfig(QObject): self._validate_names(settings) self._build_values(settings) - def _load_settings_object(self, yaml_data: Any) -> '_SettingsType': + def _load_settings_object(self, yaml_data: Any) -> _SettingsType: """Load the settings from the settings: key.""" return self._pop_object(yaml_data, 'settings', dict) - def _load_legacy_settings_object(self, yaml_data: Any) -> '_SettingsType': + def _load_legacy_settings_object(self, yaml_data: Any) -> _SettingsType: data = self._pop_object(yaml_data, 'global', dict) settings = {} for name, value in data.items(): settings[name] = {'global': value} return settings - def _build_values(self, settings: Mapping) -> None: + def _build_values(self, settings: Mapping[str, Any]) -> None: """Build up self._values from the values in the given dict.""" errors = [] for name, yaml_values in settings.items(): @@ -724,9 +740,17 @@ class ConfigPyWriter: def __init__( self, - options: List, + options: List[ + Tuple[ + Optional[urlmatch.UrlPattern], + configdata.Option, + Any + ] + ], bindings: MutableMapping[str, Mapping[str, Optional[str]]], - *, commented: bool) -> None: + *, + commented: bool, + ) -> None: self._options = options self._bindings = bindings self._commented = commented diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py index 1b07baab7..7556d2b6d 100644 --- a/qutebrowser/config/websettings.py +++ b/qutebrowser/config/websettings.py @@ -23,7 +23,7 @@ import re import argparse import functools import dataclasses -from typing import Any, Callable, Dict, Optional +from typing import Any, Callable, Dict, Optional, Union from PyQt5.QtCore import QUrl, pyqtSlot, qVersion from PyQt5.QtGui import QFont @@ -85,7 +85,11 @@ class AttributeInfo: """Info about a settings attribute.""" - def __init__(self, *attributes: Any, converter: Callable = None) -> None: + def __init__( + self, + *attributes: Any, + converter: Callable[[Any], bool] = None, + ) -> None: self.attributes = attributes if converter is None: self.converter = lambda val: val @@ -105,9 +109,6 @@ class AbstractSettings: def __init__(self, settings: Any) -> None: self._settings = settings - def _assert_not_unset(self, value: Any) -> None: - assert value is not usertypes.UNSET - def set_attribute(self, name: str, value: Any) -> None: """Set the given QWebSettings/QWebEngineSettings attribute. @@ -129,30 +130,38 @@ class AbstractSettings: info = self._ATTRIBUTES[name] return self._settings.testAttribute(info.attributes[0]) - def set_font_size(self, name: str, value: int) -> None: + def set_font_size(self, name: str, value: Union[int, usertypes.Unset]) -> None: """Set the given QWebSettings/QWebEngineSettings font size.""" - self._assert_not_unset(value) family = self._FONT_SIZES[name] - self._settings.setFontSize(family, value) + if value is usertypes.UNSET: + self._settings.resetFontSize(family) + else: + self._settings.setFontSize(family, value) - def set_font_family(self, name: str, value: Optional[str]) -> None: + def set_font_family( + self, + name: str, + value: Union[str, None, usertypes.Unset], + ) -> None: """Set the given QWebSettings/QWebEngineSettings font family. With None (the default), QFont is used to get the default font for the family. """ - self._assert_not_unset(value) family = self._FONT_FAMILIES[name] - if value is None: + if value is usertypes.UNSET: + self._settings.resetFontFamily(family) + elif value is None: font = QFont() font.setStyleHint(self._FONT_TO_QFONT[family]) value = font.defaultFamily() + self._settings.setFontFamily(family, value) + else: + self._settings.setFontFamily(family, value) - self._settings.setFontFamily(family, value) - - def set_default_text_encoding(self, encoding: str) -> None: + def set_default_text_encoding(self, encoding: Union[str, usertypes.Unset]) -> None: """Set the default text encoding to use.""" - self._assert_not_unset(encoding) + assert encoding is not usertypes.UNSET # unclear how to reset self._settings.setDefaultTextEncoding(encoding) def _update_setting(self, setting: str, value: Any) -> bool: diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py index 9b704c94d..7ae45023b 100644 --- a/qutebrowser/extensions/loader.py +++ b/qutebrowser/extensions/loader.py @@ -39,6 +39,9 @@ from qutebrowser.misc import objects # ModuleInfo objects for all loaded plugins _module_infos = [] +InitHookType = Callable[['InitContext'], None] +ConfigChangedHookType = Callable[[], None] + @dataclasses.dataclass class InitContext: @@ -59,9 +62,13 @@ class ModuleInfo: """ skip_hooks: bool = False - init_hook: Optional[Callable] = None - config_changed_hooks: List[Tuple[Optional[str], Callable]] = dataclasses.field( - default_factory=list) + init_hook: Optional[InitHookType] = None + config_changed_hooks: List[ + Tuple[ + Optional[str], + ConfigChangedHookType, + ] + ] = dataclasses.field(default_factory=list) @dataclasses.dataclass diff --git a/qutebrowser/javascript/quirks/googledocs.user.js b/qutebrowser/javascript/quirks/googledocs.user.js new file mode 100644 index 000000000..7ec47f70d --- /dev/null +++ b/qutebrowser/javascript/quirks/googledocs.user.js @@ -0,0 +1,14 @@ +// ==UserScript== +// @include https://docs.google.com/* +// ==/UserScript== + +// Workaround for typing dead keys on Google Docs +// See https://bugreports.qt.io/browse/QTBUG-69652 + +"use strict"; + +Object.defineProperty(navigator, "userAgent", { + get() { + return "Mozilla/5.0 (X11; Linux x86_64; rv:88.0) Gecko/20100101 Firefox/88.0"; + }, +}); diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index d1828791c..7e688dab1 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -112,7 +112,7 @@ class BindingTrie: return lines - def update(self, mapping: Mapping) -> None: + def update(self, mapping: Mapping[keyutils.KeySequence, str]) -> None: """Add data from the given mapping to the trie.""" for key in mapping: self[key] = mapping[key] diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index ddf818708..6bd8c99b8 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -458,7 +458,7 @@ class KeySequence: assert self self._validate() - def _convert_key(self, key: Qt.Key) -> int: + def _convert_key(self, key: Union[int, Qt.KeyboardModifiers]) -> int: """Convert a single key for QKeySequence.""" assert isinstance(key, (int, Qt.KeyboardModifiers)), key return int(key) diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py index 001aa3047..3e14719e0 100644 --- a/qutebrowser/misc/backendproblem.py +++ b/qutebrowser/misc/backendproblem.py @@ -389,6 +389,7 @@ class _BackendProblemChecker: WORKAROUND for: https://bugreports.qt.io/browse/QTBUG-72532 https://bugreports.qt.io/browse/QTBUG-82105 + https://bugreports.qt.io/browse/QTBUG-93744 """ if ('serviceworker_workaround' not in configfiles.state['general'] and qtutils.version_check('5.14', compiled=False)): @@ -398,6 +399,8 @@ class _BackendProblemChecker: reason = 'Qt 5.14' elif configfiles.state.qt_version_changed: reason = 'Qt version changed' + elif configfiles.state.qtwe_version_changed: + reason = 'QtWebEngine version changed' elif config.val.qt.workarounds.remove_service_workers: reason = 'Explicitly enabled' else: diff --git a/qutebrowser/misc/debugcachestats.py b/qutebrowser/misc/debugcachestats.py index f172f0854..2004ad7ab 100644 --- a/qutebrowser/misc/debugcachestats.py +++ b/qutebrowser/misc/debugcachestats.py @@ -30,7 +30,7 @@ from typing import Any, Callable, List, Optional, Tuple, TypeVar _CACHE_FUNCTIONS: List[Tuple[str, Any]] = [] -_T = TypeVar('_T', bound=Callable) +_T = TypeVar('_T', bound=Callable[..., Any]) def register(name: Optional[str] = None) -> Callable[[_T], _T]: diff --git a/qutebrowser/misc/elf.py b/qutebrowser/misc/elf.py index 307d3b4ac..bf824880a 100644 --- a/qutebrowser/misc/elf.py +++ b/qutebrowser/misc/elf.py @@ -65,7 +65,7 @@ import re import dataclasses import mmap import pathlib -from typing import IO, ClassVar, Dict, Optional, Tuple, cast +from typing import Any, IO, ClassVar, Dict, Optional, Tuple, cast from PyQt5.QtCore import QLibraryInfo @@ -93,7 +93,7 @@ class Endianness(enum.Enum): big = 2 -def _unpack(fmt: str, fobj: IO[bytes]) -> Tuple: +def _unpack(fmt: str, fobj: IO[bytes]) -> Tuple[Any, ...]: """Unpack the given struct format from the given file.""" size = struct.calcsize(fmt) data = _safe_read(fobj, size) diff --git a/qutebrowser/misc/throttle.py b/qutebrowser/misc/throttle.py index 1beebe1aa..ac565b68d 100644 --- a/qutebrowser/misc/throttle.py +++ b/qutebrowser/misc/throttle.py @@ -45,7 +45,7 @@ class Throttle(QObject): """ def __init__(self, - func: Callable, + func: Callable[..., None], delay_ms: int, parent: QObject = None) -> None: """Constructor. diff --git a/qutebrowser/utils/debug.py b/qutebrowser/utils/debug.py index 54fcd5aa9..7d069909a 100644 --- a/qutebrowser/utils/debug.py +++ b/qutebrowser/utils/debug.py @@ -35,7 +35,7 @@ from qutebrowser.misc import objects from qutebrowser.qt import sip -def log_events(klass: Type) -> Type: +def log_events(klass: Type[QObject]) -> Type[QObject]: """Class decorator to log Qt events.""" old_event = klass.event @@ -46,7 +46,7 @@ def log_events(klass: Type) -> Type: qenum_key(QEvent, e.type()))) return old_event(self, e) - klass.event = new_event + klass.event = new_event # type: ignore[assignment] return klass @@ -96,10 +96,13 @@ def log_signals(obj: QObject) -> QObject: return obj -def qenum_key(base: Type, - value: Union[int, sip.simplewrapper], +_EnumValueType = Union[sip.simplewrapper, int] + + +def qenum_key(base: Type[_EnumValueType], + value: _EnumValueType, add_base: bool = False, - klass: Type = None) -> str: + klass: Type[_EnumValueType] = None) -> str: """Convert a Qt Enum value to its key as a string. Args: @@ -119,8 +122,9 @@ def qenum_key(base: Type, raise TypeError("Can't guess enum class of an int!") try: - idx = base.staticMetaObject.indexOfEnumerator(klass.__name__) - meta_enum = base.staticMetaObject.enumerator(idx) + meta_obj = base.staticMetaObject # type: ignore[union-attr] + idx = meta_obj.indexOfEnumerator(klass.__name__) + meta_enum = meta_obj.enumerator(idx) ret = meta_enum.valueToKey(int(value)) # type: ignore[arg-type] except AttributeError: ret = None @@ -139,10 +143,10 @@ def qenum_key(base: Type, return ret -def qflags_key(base: Type, - value: Union[int, sip.simplewrapper], +def qflags_key(base: Type[_EnumValueType], + value: _EnumValueType, add_base: bool = False, - klass: Type = None) -> str: + klass: Type[_EnumValueType] = 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 @@ -220,7 +224,7 @@ def signal_name(sig: pyqtBoundSignal) -> str: return m.group('name') -def format_args(args: Sequence = None, kwargs: Mapping = None) -> str: +def format_args(args: Sequence[Any] = None, kwargs: Mapping[str, Any] = 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] @@ -245,9 +249,9 @@ def dbg_signal(sig: pyqtBoundSignal, args: Any) -> str: return '{}({})'.format(signal_name(sig), format_args(args)) -def format_call(func: Callable, - args: Sequence = None, - kwargs: Mapping = None, +def format_call(func: Callable[..., Any], + args: Sequence[Any] = None, + kwargs: Mapping[str, Any] = None, full: bool = True) -> str: """Get a string representation of a function calls with the given args. @@ -302,7 +306,7 @@ class log_time: # noqa: N801,N806 pylint: disable=invalid-name self._logger.debug("{} took {} seconds.".format( self._action.capitalize(), delta)) - def __call__(self, func: Callable) -> Callable: + def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]: @functools.wraps(func) def wrapped(*args: Any, **kwargs: Any) -> Any: """Call the original function.""" diff --git a/qutebrowser/utils/docutils.py b/qutebrowser/utils/docutils.py index 202fcba95..89e799c89 100644 --- a/qutebrowser/utils/docutils.py +++ b/qutebrowser/utils/docutils.py @@ -25,7 +25,7 @@ import inspect import os.path import collections import enum -from typing import Callable, MutableMapping, Optional, List, Union +from typing import Any, Callable, MutableMapping, Optional, List, Union import qutebrowser from qutebrowser.utils import log, utils @@ -88,7 +88,7 @@ class DocstringParser: arg_inside = enum.auto() misc = enum.auto() - def __init__(self, func: Callable) -> None: + def __init__(self, func: Callable[..., Any]) -> None: """Constructor. Args: diff --git a/qutebrowser/utils/jinja.py b/qutebrowser/utils/jinja.py index 61d8ccdad..a44a0235e 100644 --- a/qutebrowser/utils/jinja.py +++ b/qutebrowser/utils/jinja.py @@ -145,7 +145,7 @@ js_environment = jinja2.Environment(loader=Loader('javascript')) @functools.lru_cache() def template_config_variables(template: str) -> FrozenSet[str]: """Return the config variables used in the template.""" - unvisted_nodes = [environment.parse(template)] + unvisted_nodes: List[jinja2.nodes.Node] = [environment.parse(template)] result: Set[str] = set() while unvisted_nodes: node = unvisted_nodes.pop() @@ -157,11 +157,11 @@ def template_config_variables(template: str) -> FrozenSet[str]: # For example it's ['ab', 'c', 'd'] for 'conf.d.c.ab'. attrlist: List[str] = [] while isinstance(node, jinja2.nodes.Getattr): - attrlist.append(node.attr) # type: ignore[attr-defined] - node = node.node # type: ignore[attr-defined] + attrlist.append(node.attr) + node = node.node if isinstance(node, jinja2.nodes.Name): - if node.name == 'conf': # type: ignore[attr-defined] + if node.name == 'conf': result.add('.'.join(reversed(attrlist))) # otherwise, the node is a Name node so it doesn't have any # child nodes diff --git a/qutebrowser/utils/objreg.py b/qutebrowser/utils/objreg.py index 99d8a0936..0819a5d0a 100644 --- a/qutebrowser/utils/objreg.py +++ b/qutebrowser/utils/objreg.py @@ -55,7 +55,8 @@ class CommandOnlyError(Exception): _IndexType = Union[str, int] -class ObjectRegistry(collections.UserDict): +# UserDict is only generic in Python 3.9+ +class ObjectRegistry(collections.UserDict): # type: ignore[type-arg] """A registry of long-living objects in qutebrowser. diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 01234a42b..ff8983c50 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -31,7 +31,8 @@ Module attributes: import io import operator import contextlib -from typing import TYPE_CHECKING, BinaryIO, IO, Iterator, Optional, Union, Tuple, cast +from typing import (Any, AnyStr, TYPE_CHECKING, BinaryIO, IO, Iterator, + Optional, Union, Tuple, cast) from PyQt5.QtCore import (qVersion, QEventLoop, QDataStream, QByteArray, QIODevice, QFileDevice, QSaveFile, QT_VERSION_STR, @@ -227,7 +228,7 @@ def savefile_open( filename: str, binary: bool = False, encoding: str = 'utf-8' -) -> Iterator[IO]: +) -> Iterator[IO[AnyStr]]: """Context manager to easily use a QSaveFile.""" f = QSaveFile(filename) cancelled = False @@ -239,7 +240,7 @@ def savefile_open( dev = cast(BinaryIO, PyQIODevice(f)) if binary: - new_f: IO = dev + new_f: IO[Any] = dev # FIXME:mypy Why doesn't AnyStr work? else: new_f = io.TextIOWrapper(dev, encoding=encoding) @@ -298,7 +299,11 @@ class PyQIODevice(io.BufferedIOBase): if not self.writable(): raise OSError("Trying to write to unwritable file!") - def open(self, mode: QIODevice.OpenMode) -> contextlib.closing: + # contextlib.closing is only generic in Python 3.9+ + def open( + self, + mode: QIODevice.OpenMode, + ) -> contextlib.closing: # type: ignore[type-arg] """Open the underlying device and ensure opening succeeded. Raises OSError if opening failed. diff --git a/qutebrowser/utils/urlmatch.py b/qutebrowser/utils/urlmatch.py index 8dfd6d273..f14c2083d 100644 --- a/qutebrowser/utils/urlmatch.py +++ b/qutebrowser/utils/urlmatch.py @@ -104,7 +104,14 @@ class UrlPattern: self._init_path(parsed) self._init_port(parsed) - def _to_tuple(self) -> Tuple: + def _to_tuple(self) -> Tuple[ + bool, # _match_all + bool, # _match_subdomains + Optional[str], # _scheme + Optional[str], # host + Optional[str], # _path + Optional[int], # _port + ]: """Get a pattern with information used for __eq__/__hash__.""" return (self._match_all, self._match_subdomains, self._scheme, self.host, self._path, self._port) diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 56ebe45c4..fb0165de2 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -341,7 +341,7 @@ class prevent_exceptions: # noqa: N801,N806 pylint: disable=invalid-name self._retval = retval self._predicate = predicate - def __call__(self, func: Callable) -> Callable: + def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]: """Called when a function should be decorated. Args: @@ -447,7 +447,7 @@ def qualname(obj: Any) -> str: _ExceptionType = Union[Type[BaseException], Tuple[Type[BaseException]]] -def raises(exc: _ExceptionType, func: Callable, *args: Any) -> bool: +def raises(exc: _ExceptionType, func: Callable[..., Any], *args: Any) -> bool: """Check if a function raises a given exception. Args: @@ -725,7 +725,10 @@ def yaml_dump(data: Any, f: IO[str] = None) -> Optional[str]: return yaml_data.decode('utf-8') -def chunk(elems: Sequence, n: int) -> Iterator[Sequence]: +_T = TypeVar('_T') + + +def chunk(elems: Sequence[_T], n: int) -> Iterator[Sequence[_T]]: """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 f5bc5eab2..8cd244fca 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -591,7 +591,7 @@ class WebEngineVersions: def __str__(self) -> str: s = f'QtWebEngine {self.webengine}' if self.chromium is not None: - s += f', Chromium {self.chromium}' + s += f', based on Chromium {self.chromium}' if self.source != 'UA': s += f' (from {self.source})' return s diff --git a/requirements.txt b/requirements.txt index 9fbd22da3..88033d3e7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,10 +4,10 @@ adblock==0.4.4 colorama==0.4.4 dataclasses==0.6 ; python_version<"3.7" importlib-metadata==4.0.1 ; python_version<"3.8" -importlib-resources==5.1.2 ; python_version<"3.9" -Jinja2==2.11.3 -MarkupSafe==1.1.1 -Pygments==2.8.1 +importlib-resources==5.1.3 ; python_version<"3.9" +Jinja2==3.0.0 +MarkupSafe==2.0.0 +Pygments==2.9.0 PyYAML==5.4.1 typing-extensions==3.10.0.0 zipp==3.4.1 diff --git a/scripts/dev/recompile_requirements.py b/scripts/dev/recompile_requirements.py index b545dbc5f..8dd9e4c09 100644 --- a/scripts/dev/recompile_requirements.py +++ b/scripts/dev/recompile_requirements.py @@ -55,13 +55,13 @@ CHANGELOG_URLS = { 'pytest-repeat': 'https://github.com/pytest-dev/pytest-repeat/blob/master/CHANGES.rst', 'requests': 'https://github.com/psf/requests/blob/master/HISTORY.md', 'requests-file': 'https://github.com/dashea/requests-file/blob/master/CHANGES.rst', - 'Werkzeug': 'https://github.com/pallets/werkzeug/blob/master/CHANGES.rst', - 'click': 'https://click.palletsprojects.com/en/7.x/changelog/', - 'itsdangerous': 'https://itsdangerous.palletsprojects.com/en/1.1.x/changes/', + 'Werkzeug': 'https://werkzeug.palletsprojects.com/en/latest/changes/', + 'click': 'https://click.palletsprojects.com/en/latest/changes/', + 'itsdangerous': 'https://itsdangerous.palletsprojects.com/en/latest/changes/', 'parse-type': 'https://github.com/jenisys/parse_type/blob/master/CHANGES.txt', 'sortedcontainers': 'https://github.com/grantjenks/python-sortedcontainers/blob/master/HISTORY.rst', 'soupsieve': 'https://facelessuser.github.io/soupsieve/about/changelog/', - 'Flask': 'https://flask.palletsprojects.com/en/1.1.x/changelog/', + 'Flask': 'https://flask.palletsprojects.com/en/latest/changes/', 'Mako': 'https://docs.makotemplates.org/en/latest/changelog.html', 'glob2': 'https://github.com/miracle2k/python-glob2/blob/master/CHANGES', 'hypothesis': 'https://hypothesis.readthedocs.io/en/latest/changes.html', @@ -76,8 +76,8 @@ CHANGELOG_URLS = { 'packaging': 'https://packaging.pypa.io/en/latest/changelog.html', 'build': 'https://github.com/pypa/build/blob/main/CHANGELOG.rst', 'attrs': 'https://www.attrs.org/en/stable/changelog.html', - 'Jinja2': 'https://github.com/pallets/jinja/blob/master/CHANGES.rst', - 'MarkupSafe': 'https://markupsafe.palletsprojects.com/en/1.1.x/changes/', + 'Jinja2': 'https://jinja.palletsprojects.com/en/latest/changes/', + 'MarkupSafe': 'https://markupsafe.palletsprojects.com/en/latest/changes/', 'flake8': 'https://gitlab.com/pycqa/flake8/tree/master/docs/source/release-notes', 'flake8-docstrings': 'https://pypi.org/project/flake8-docstrings/', 'flake8-debugger': 'https://github.com/JBKahn/flake8-debugger/', diff --git a/scripts/mkvenv.py b/scripts/mkvenv.py index 31b185fe1..58131bc05 100755 --- a/scripts/mkvenv.py +++ b/scripts/mkvenv.py @@ -391,7 +391,13 @@ def install_dev_requirements(venv_dir: pathlib.Path) -> None: utils.print_title("Installing dev dependencies") pip_install(venv_dir, '-r', str(requirements_file('dev')), - '-r', requirements_file('tests')) + '-r', str(requirements_file('check-manifest')), + '-r', str(requirements_file('flake8')), + '-r', str(requirements_file('mypy')), + '-r', str(requirements_file('pyroma')), + '-r', str(requirements_file('vulture')), + '-r', str(requirements_file('yamllint')), + '-r', str(requirements_file('tests'))) def install_qutebrowser(venv_dir: pathlib.Path) -> None: diff --git a/tests/end2end/fixtures/webserver_sub_ssl.py b/tests/end2end/fixtures/webserver_sub_ssl.py index 241261194..4c54d767b 100644 --- a/tests/end2end/fixtures/webserver_sub_ssl.py +++ b/tests/end2end/fixtures/webserver_sub_ssl.py @@ -64,7 +64,7 @@ def turn_off_logging(): def main(): end2end_dir = pathlib.Path(__file__).resolve().parents[1] ssl_dir = end2end_dir / 'data' / 'ssl' - context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) context.load_cert_chain((ssl_dir / 'cert.pem'), (ssl_dir / 'key.pem')) app.run(port=int(sys.argv[1]), debug=False, ssl_context=context) diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index dd6ef54fa..b88bc2f8d 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -211,6 +211,7 @@ class TestKeyConfig: "a": "set-cmd-text no_leading_colon", "b": "set-cmd-text -s -a :skip_cuz_append", "c": "set-cmd-text --append :skip_cuz_append", + "x": "set-cmd-text", }, { "open": ["o"], diff --git a/tests/unit/config/test_configfiles.py b/tests/unit/config/test_configfiles.py index e0d64bffc..65952ddb4 100644 --- a/tests/unit/config/test_configfiles.py +++ b/tests/unit/config/test_configfiles.py @@ -29,7 +29,7 @@ from PyQt5.QtCore import QSettings from qutebrowser.config import (config, configfiles, configexc, configdata, configtypes) -from qutebrowser.utils import utils, usertypes, urlmatch, standarddir +from qutebrowser.utils import utils, usertypes, urlmatch, standarddir, version from qutebrowser.keyinput import keyutils @@ -81,6 +81,7 @@ def autoconfig(config_tmpdir): False, '[general]\n' 'qt_version = 5.6.7\n' + 'qtwe_version = 7.8.9\n' 'version = 1.2.3\n' '\n' '[geometry]\n' @@ -92,6 +93,7 @@ def autoconfig(config_tmpdir): False, '[general]\n' 'qt_version = 5.6.7\n' + 'qtwe_version = 7.8.9\n' 'version = 1.2.3\n' '\n' '[geometry]\n' @@ -104,6 +106,7 @@ def autoconfig(config_tmpdir): '[general]\n' 'foobar = 42\n' 'qt_version = 5.6.7\n' + 'qtwe_version = 7.8.9\n' 'version = 1.2.3\n' '\n' '[geometry]\n' @@ -114,6 +117,7 @@ def autoconfig(config_tmpdir): True, '[general]\n' 'qt_version = 5.6.7\n' + 'qtwe_version = 7.8.9\n' 'version = 1.2.3\n' 'newval = 23\n' '\n' @@ -122,10 +126,13 @@ def autoconfig(config_tmpdir): '[inspector]\n' '\n'), ]) -def test_state_config(fake_save_manager, data_tmpdir, monkeypatch, - old_data, insert, new_data): +def test_state_config( + fake_save_manager, data_tmpdir, monkeypatch, qtwe_version_patcher, + old_data, insert, new_data +): monkeypatch.setattr(configfiles.qutebrowser, '__version__', '1.2.3') monkeypatch.setattr(configfiles, 'qVersion', lambda: '5.6.7') + qtwe_version_patcher('7.8.9') statefile = data_tmpdir / 'state' if old_data is not None: @@ -157,6 +164,28 @@ def state_writer(data_tmpdir): return _write +@pytest.fixture +def qtwe_version_patcher(monkeypatch): + try: + from PyQt5 import QtWebEngineWidgets # pylint: disable=unused-import + except ImportError: + pytest.skip("QtWebEngine not available") + + def patch(ver): + monkeypatch.setattr( + configfiles.version, + 'qtwebengine_versions', + lambda avoid_init=False: + version.WebEngineVersions( + webengine=utils.VersionNumber.parse(ver), + chromium=None, + source='test', + ) + ) + + return patch + + @pytest.mark.parametrize('old_version, new_version, changed', [ (None, '5.12.1', False), ('5.12.1', '5.12.1', False), @@ -176,6 +205,32 @@ def test_qt_version_changed(state_writer, monkeypatch, assert state.qt_version_changed == changed +@pytest.mark.parametrize('old_version, new_version, changed', [ + (None, '5.15.1', False), + ('5.15.1', '5.15.1', False), + ('5.15.1', '5.15.2', True), + ('5.14.0', '5.15.2', True), +]) +def test_qtwe_version_changed(state_writer, qtwe_version_patcher, + old_version, new_version, changed): + qtwe_version_patcher(new_version) + + if old_version is not None: + state_writer('qtwe_version', old_version) + + state = configfiles.StateConfig() + assert state.qtwe_version_changed == changed + + +def test_qtwe_version_changed_webkit(stubs, monkeypatch, state_writer): + fake = stubs.ImportFake({'PyQt5.QtWebEngineWidgets': False}, monkeypatch) + fake.patch() + + state_writer('qtwe_version', 'no') + state = configfiles.StateConfig() + assert not state.qtwe_version_changed + + @pytest.mark.parametrize('old_version, new_version, expected', [ (None, '2.0.0', configfiles.VersionChange.unknown), ('1.14.1', '1.14.1', configfiles.VersionChange.equal), diff --git a/tests/unit/scripts/test_check_coverage.py b/tests/unit/scripts/test_check_coverage.py index d2a0b409f..efd35ce82 100644 --- a/tests/unit/scripts/test_check_coverage.py +++ b/tests/unit/scripts/test_check_coverage.py @@ -176,9 +176,6 @@ def test_untested_floats(covtest): assert covtest.check() == [expected] -@pytest.mark.skipif( - sys.version_info[:4] == (3, 10, 0, 'alpha'), - reason='Different results, see https://github.com/nedbat/coveragepy/issues/1106') def test_untested_branches(covtest): covtest.makefile(""" def func2(arg): @@ -191,10 +188,11 @@ def test_untested_branches(covtest): func2(True) """) covtest.run() + line_coverage = "83.33%" if sys.version_info[:2] >= (3, 10) else "100.00%" expected = check_coverage.Message( check_coverage.MsgType.insufficient_coverage, 'module.py', - 'module.py has 100.00% line and 50.00% branch coverage!') + f'module.py has {line_coverage} line and 50.00% branch coverage!') assert covtest.check() == [expected] diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py index 734b70468..6c57cb3d3 100644 --- a/tests/unit/utils/test_version.py +++ b/tests/unit/utils/test_version.py @@ -917,14 +917,14 @@ class TestWebEngineVersions: webengine=utils.VersionNumber(5, 15, 2), chromium='87.0.4280.144', source='UA'), - "QtWebEngine 5.15.2, Chromium 87.0.4280.144", + "QtWebEngine 5.15.2, based on Chromium 87.0.4280.144", ), ( version.WebEngineVersions( webengine=utils.VersionNumber(5, 15, 2), chromium='87.0.4280.144', source='faked'), - "QtWebEngine 5.15.2, Chromium 87.0.4280.144 (from faked)", + "QtWebEngine 5.15.2, based on Chromium 87.0.4280.144 (from faked)", ), ]) def test_str(self, version, expected): |