diff options
author | Florian Bruhin <me@the-compiler.org> | 2021-01-14 11:47:04 +0100 |
---|---|---|
committer | Florian Bruhin <me@the-compiler.org> | 2021-01-14 11:47:04 +0100 |
commit | 482cf9ae047b8a08ee7ad6a1ff64feff39fe6e3c (patch) | |
tree | bd9a1784d271b57bd3571f2839100306366eb94d | |
parent | 4d893e75a175d7f05c5627a1dca4e176ce1e70e6 (diff) | |
parent | e66968737d433ccc4a5664491e50fad38d5655c3 (diff) | |
download | qutebrowser-482cf9ae047b8a08ee7ad6a1ff64feff39fe6e3c.tar.gz qutebrowser-482cf9ae047b8a08ee7ad6a1ff64feff39fe6e3c.zip |
Merge branch 'master' into history-cleanup
# Conflicts:
# qutebrowser/app.py
110 files changed, 722 insertions, 629 deletions
@@ -50,7 +50,7 @@ ignore = A003, W503, W504 FI15 -min-version = 3.6.0 +min-version = 3.6.1 max-complexity = 12 per-file-ignores = qutebrowser/api/hook.py : N801 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4ec4b1184..481ddc18d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,7 +102,7 @@ jobs: include: ### PyQt 5.12 (Python 3.6) - testenv: py36-pyqt512 - os: ubuntu-20.04 + os: ubuntu-18.04 python: 3.6 ### PyQt 5.13 (Python 3.7) - testenv: py37-pyqt513 @@ -126,10 +126,10 @@ jobs: python: 3.7 args: "tests/unit" # Only run unit tests on macOS ### macOS Big Sur - - testenv: py37-pyqt515 - os: macos-11.0 - python: 3.7 - args: "tests/unit" # Only run unit tests on macOS + # - testenv: py37-pyqt515 + # os: macos-11.0 + # python: 3.7 + # args: "tests/unit" # Only run unit tests on macOS ### Windows: PyQt 5.15 (Python 3.7 to match PyInstaller env) - testenv: py37-pyqt515 os: windows-2019 diff --git a/README.asciidoc b/README.asciidoc index 42013368c..a4fc38e7d 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -74,11 +74,11 @@ Requirements The following software and libraries are required to run qutebrowser: -* https://www.python.org/[Python] 3.6 or newer +* https://www.python.org/[Python] 3.6.1 or newer * https://www.qt.io/[Qt] 5.12.0 or newer (5.12 LTS or 5.15 recommended) with the following modules: - QtCore / qtbase - - QtQuick (part of qtbase in some distributions) + - QtQuick (part of qtbase or qtdeclarative in some distributions) - QtSQL (part of qtbase in some distributions) - QtOpenGL - QtWebEngine, or @@ -96,9 +96,11 @@ The following software and libraries are required to run qutebrowser: * http://jinja.pocoo.org/[jinja2] * http://pygments.org/[pygments] * https://github.com/yaml/pyyaml[PyYAML] -* https://www.attrs.org/[attrs] -* https://importlib-resources.readthedocs.io/[importlib_resources] (on Python - 3.8 or older) + +On older Python versions (3.6/3.7/3.8), the following backports are also required: + +* https://importlib-resources.readthedocs.io/[importlib_resources] (Python 3.8 or older) +* https://github.com/ericvsmith/dataclasses[dataclasses] (Python 3.6 only) The following libraries are optional: diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index ceb5faa46..58fbecb6f 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -21,8 +21,8 @@ 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 +- At least Python 3.6.1 is now required to run qutebrowser, support for Python + 3.5 (and 3.6.0) is dropped. Note that Python 3.5 is https://www.python.org/downloads/release/python-3510/[no longer supported upstream] since September 2020. - At least Qt/PyQt 5.12 is now required to run qutebrowser, support for 5.7 to @@ -44,12 +44,18 @@ Major changes still relying on it. The `cssutils` project is also dead upstream, with its repository being gone after Bitbucket https://bitbucket.org/blog/sunsetting-mercurial-support-in-bitbucket[removed Mercurial support]. +- The (formerly required) `pygments` dependency is now optional. It is only + used when using `:view-source` with QtWebKit, or when forcing it via + `:view-source --pygments` on QtWebEngine. If it is unavailable, an + unhighlighted fallback version of the page's source is shown. - TODO: The former dependency on the `pkg_resources` module (part of the `setuptools` project) got dropped. - A new dependency on the `importlib_resources` module got introduced for Python versions up to and including 3.8. Note that the stdlib `importlib.resources` module for Python 3.7 and 3.8 is missing the needed APIs, thus requiring the backports for those versions as well. +- The former dependency on the `attrs`/`attr` package is now dropped. +- On Python 3.6, a new dependency on the `dataclasses` backport is now required. Removed ~~~~~~~ diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index eb8e4925d..3564c9766 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -1515,6 +1515,7 @@ Show the source of the current page in a new tab. * +*-p*+, +*--pygments*+: Use pygments to generate the view. This is always the case for QtWebKit. For QtWebEngine it may display slightly different source. Some JavaScript processing may be applied. + Needs the optional Pygments dependency for highlighting. [[window-only]] diff --git a/doc/install.asciidoc b/doc/install.asciidoc index 74b18a6cf..03f079cad 100644 --- a/doc/install.asciidoc +++ b/doc/install.asciidoc @@ -330,7 +330,7 @@ This binary is also available through the https://caskroom.github.io/[Homebrew Cask] package manager: ---- -$ brew cask install qutebrowser +$ brew install qutebrowser --cask ---- Manual Install diff --git a/misc/requirements/requirements-check-manifest.txt b/misc/requirements/requirements-check-manifest.txt index f2a5f00dd..1fe41314d 100644 --- a/misc/requirements/requirements-check-manifest.txt +++ b/misc/requirements/requirements-check-manifest.txt @@ -1,7 +1,7 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py build==0.1.0 -check-manifest==0.45 +check-manifest==0.46 packaging==20.8 pep517==0.9.1 pyparsing==2.4.7 diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt index 2dd1e96b9..3089e2843 100644 --- a/misc/requirements/requirements-dev.txt +++ b/misc/requirements/requirements-dev.txt @@ -15,10 +15,10 @@ packaging==20.8 pycparser==2.20 Pympler==0.9 pyparsing==2.4.7 -PyQt-builder==1.6.0 +PyQt-builder==1.7.0 python-dateutil==2.8.1 requests==2.25.1 -sip==5.5.0 +sip==6.0.0 six==1.15.0 toml==0.10.2 uritemplate==3.0.1 diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt index 4e370dfb7..99907efa2 100644 --- a/misc/requirements/requirements-mypy.txt +++ b/misc/requirements/requirements-mypy.txt @@ -1,7 +1,7 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -diff-cover==4.0.1 -importlib-resources==4.1.1 +diff-cover==4.1.1 +importlib-resources==5.0.0 inflect==5.0.2 Jinja2==2.11.2 jinja2-pluralize==0.3.0 @@ -11,6 +11,6 @@ mypy==0.790 mypy-extensions==0.4.3 pluggy==0.13.1 Pygments==2.7.3 --e git+https://github.com/stlehmann/PyQt5-stubs.git@998632b9d6771137f9665732b03eba25c8b4e920#egg=PyQt5_stubs +-e git+https://github.com/stlehmann/PyQt5-stubs.git@307eb693f63bd91ac67631ea57c4620e2c363435#egg=PyQt5_stubs typed-ast==1.4.2 typing-extensions==3.7.4.3 diff --git a/misc/requirements/requirements-qutebrowser.txt-raw b/misc/requirements/requirements-qutebrowser.txt-raw index 2d527aeef..1781775f4 100644 --- a/misc/requirements/requirements-qutebrowser.txt-raw +++ b/misc/requirements/requirements-qutebrowser.txt-raw @@ -1,10 +1,16 @@ Jinja2 -Pygments pyPEG2 PyYAML -colorama attrs -adblock # Optional, for improved adblocking + +## stdlib backports importlib-resources +dataclasses + +## Optional dependencies +Pygments # For :view-source --pygments or on QtWebKit +colorama # Colored log output on Windows +adblock # Improved adblocking #@ markers: importlib-resources python_version<"3.9" +#@ markers: dataclasses python_version<"3.7" diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt index 54eb185bc..f8a1b773e 100644 --- a/misc/requirements/requirements-sphinx.txt +++ b/misc/requirements/requirements-sphinx.txt @@ -15,7 +15,7 @@ pyparsing==2.4.7 pytz==2020.5 requests==2.25.1 snowballstemmer==2.0.0 -Sphinx==3.4.1 +Sphinx==3.4.3 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 dd5daf9ef..9141378cd 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -15,7 +15,7 @@ filelock==3.0.12 Flask==1.1.2 glob2==0.7 hunter==3.3.1 -hypothesis==5.46.0 +hypothesis==6.0.0 icdiff==1.9.1 idna==2.10 iniconfig==1.1.1 @@ -43,13 +43,13 @@ pytest-cov==2.10.1 pytest-forked==1.3.0 pytest-icdiff==0.5 pytest-instafail==0.4.2 -pytest-mock==3.4.0 +pytest-mock==3.5.1 pytest-qt==3.3.0 pytest-repeat==0.9.1 pytest-rerunfailures==9.1.1 pytest-xdist==2.2.0 pytest-xvfb==2.0.0 -PyVirtualDisplay==1.3.2 +PyVirtualDisplay==2.0 requests==2.25.1 requests-file==1.5.1 six==1.15.0 diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt index c72393868..f7161c3f6 100644 --- a/misc/requirements/requirements-tox.txt +++ b/misc/requirements/requirements-tox.txt @@ -9,5 +9,5 @@ py==1.10.0 pyparsing==2.4.7 six==1.15.0 toml==0.10.2 -tox==3.20.1 -virtualenv==20.2.2 +tox==3.21.0 +virtualenv==20.3.0 diff --git a/misc/userscripts/readability-js b/misc/userscripts/readability-js index 310d1c081..5f5c85a78 100755 --- a/misc/userscripts/readability-js +++ b/misc/userscripts/readability-js @@ -76,6 +76,10 @@ const HEADER = ` margin: 0; background-color: #dddddd; } + pre > code { + padding-right: 0; + padding-left: 0; + } blockquote { border-inline-start: 2px solid grey !important; padding: 0; diff --git a/pytest.ini b/pytest.ini index 3705a17ef..73c0d7adf 100644 --- a/pytest.ini +++ b/pytest.ini @@ -79,7 +79,5 @@ qt_log_ignore = xfail_strict = true filterwarnings = error - # See https://github.com/HypothesisWorks/hypothesis/issues/2370 - ignore:.*which is reset between function calls but not between test cases generated by:hypothesis.errors.HypothesisDeprecationWarning default:Test process .* failed to terminate!:UserWarning faulthandler_timeout = 90 diff --git a/qutebrowser/app.py b/qutebrowser/app.py index c596fab55..1d9b593c0 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -92,20 +92,20 @@ def run(args): configinit.early_init(args) log.init.debug("Initializing application...") - global q_app - q_app = Application(args) - q_app.setOrganizationName("qutebrowser") - q_app.setApplicationName("qutebrowser") + app = Application(args) + objects.qapp = app + app.setOrganizationName("qutebrowser") + app.setApplicationName("qutebrowser") # Default DesktopFileName is org.qutebrowser.qutebrowser, set in `get_argparser()` - q_app.setDesktopFileName(args.desktop_file_name) - q_app.setApplicationVersion(qutebrowser.__version__) + app.setDesktopFileName(args.desktop_file_name) + app.setApplicationVersion(qutebrowser.__version__) if args.version: print(version.version_info()) sys.exit(usertypes.Exit.ok) quitter.init(args) - crashsignal.init(q_app=q_app, args=args, quitter=quitter.instance) + crashsignal.init(q_app=app, args=args, quitter=quitter.instance) try: server = ipc.send_or_listen(args) @@ -136,7 +136,7 @@ def qt_mainloop(): WARNING: misc/crashdialog.py checks the stacktrace for this function name, so if this is changed, it should be changed there as well! """ - return q_app.exec_() + return objects.qapp.exec() def init(*, args: argparse.Namespace) -> None: @@ -145,7 +145,7 @@ def init(*, args: argparse.Namespace) -> None: crashsignal.crash_handler.init_faulthandler() - q_app.setQuitOnLastWindowClosed(False) + objects.qapp.setQuitOnLastWindowClosed(False) quitter.instance.shutting_down.connect(QApplication.closeAllWindows) _init_icon() @@ -165,7 +165,7 @@ def init(*, args: argparse.Namespace) -> None: eventfilter.init() log.init.debug("Connecting signals...") - q_app.focusChanged.connect(on_focus_changed) + objects.qapp.focusChanged.connect(on_focus_changed) _process_args(args) @@ -191,7 +191,7 @@ def _init_icon(): if icon.isNull(): log.init.warning("Failed to load icon") else: - q_app.setWindowIcon(icon) + objects.qapp.setWindowIcon(icon) def _init_pulseaudio(): @@ -228,7 +228,7 @@ def _process_args(args): window = mainwindow.MainWindow(private=private) if not args.nowindow: window.show() - q_app.setActiveWindow(window) + objects.qapp.setActiveWindow(window) process_pos_args(args.command) _open_startpage() @@ -423,7 +423,7 @@ def _init_modules(*, args): config.instance.changed.connect(_on_config_changed) log.init.debug("Initializing save manager...") - save_manager = savemanager.SaveManager(q_app) + save_manager = savemanager.SaveManager(objects.qapp) objreg.register('save-manager', save_manager) quitter.instance.shutting_down.connect(save_manager.shutdown) configinit.late_init(save_manager) @@ -451,17 +451,17 @@ def _init_modules(*, args): sql.init(os.path.join(standarddir.data(), 'history.sqlite')) log.init.debug("Initializing web history...") - history.init(q_app) + history.init(objects.qapp) except sql.KnownError as e: error.handle_fatal_exc(e, 'Error initializing SQL', - pre_text='Error initializing SQL', - no_err_windows=args.no_err_windows) + pre_text='Error initializing SQL', + no_err_windows=args.no_err_windows) sys.exit(usertypes.Exit.err_init) log.init.debug("Initializing command history...") cmdhistory.init() log.init.debug("Initializing sessions...") - sessions.init(q_app) + sessions.init(objects.qapp) log.init.debug("Initializing websettings...") websettings.init(args) @@ -471,18 +471,18 @@ def _init_modules(*, args): crashsignal.crash_handler.display_faulthandler() log.init.debug("Initializing quickmarks...") - quickmark_manager = urlmarks.QuickmarkManager(q_app) + quickmark_manager = urlmarks.QuickmarkManager(objects.qapp) objreg.register('quickmark-manager', quickmark_manager) log.init.debug("Initializing bookmarks...") - bookmark_manager = urlmarks.BookmarkManager(q_app) + bookmark_manager = urlmarks.BookmarkManager(objects.qapp) objreg.register('bookmark-manager', bookmark_manager) log.init.debug("Initializing cookies...") - cookies.init(q_app) + cookies.init(objects.qapp) log.init.debug("Initializing cache...") - cache.init(q_app) + cache.init(objects.qapp) log.init.debug("Initializing downloads...") qtnetworkdownloads.init() diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 42ad89e7c..26d5ed65d 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -22,10 +22,10 @@ import enum import itertools import functools +import dataclasses 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, QEvent, QPoint) from PyQt5.QtGui import QKeyEvent, QIcon @@ -38,14 +38,10 @@ if TYPE_CHECKING: from PyQt5.QtWebKitWidgets import QWebPage from PyQt5.QtWebEngineWidgets import QWebEngineHistory, QWebEnginePage -import pygments -import pygments.lexers -import pygments.formatters - from qutebrowser.keyinput import modeman from qutebrowser.config import config from qutebrowser.utils import (utils, objreg, usertypes, log, qtutils, - urlutils, message) + urlutils, message, jinja) from qutebrowser.misc import miscwidgets, objects, sessions from qutebrowser.browser import eventfilter, inspector from qutebrowser.qt import sip @@ -112,7 +108,7 @@ class TerminationStatus(enum.Enum): killed = 3 -@attr.s +@dataclasses.dataclass class TabData: """A simple namespace with a fixed set of attributes. @@ -134,17 +130,17 @@ class TabData: splitter: InspectorSplitter used to show inspector inside the tab. """ - 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) + keep_icon: bool = False + viewing_source: bool = False + inspector: Optional['AbstractWebInspector'] = None + open_target: usertypes.ClickTarget = usertypes.ClickTarget.normal + override_target: Optional[usertypes.ClickTarget] = None + pinned: bool = False + fullscreen: bool = False + netrc_used: bool = False + input_mode: usertypes.KeyMode = usertypes.KeyMode.normal + last_navigation: Optional[usertypes.NavigationRequest] = None + splitter: Optional[miscwidgets.InspectorSplitter] = None def should_show_icon(self) -> bool: return (config.val.tabs.favicons.show == 'always' or @@ -177,30 +173,52 @@ class AbstractAction: raise WebTabError("{} is not a valid web action!".format(name)) self._widget.triggerPageAction(member) - def show_source( - self, - pygments: bool = False # pylint: disable=redefined-outer-name - ) -> None: + def show_source(self, pygments: bool = False) -> None: """Show the source of the current page in a new tab.""" raise NotImplementedError + def _show_html_source(self, html: str) -> None: + """Show the given HTML as source page.""" + tb = objreg.get('tabbed-browser', scope='window', window=self._tab.win_id) + new_tab = tb.tabopen(background=False, related=True) + new_tab.set_html(html, self._tab.url()) + new_tab.data.viewing_source = True + + def _show_source_fallback(self, source: str) -> None: + """Show source with pygments unavailable.""" + html = jinja.render( + 'pre.html', + title='Source', + content=source, + preamble="Note: The optional Pygments dependency wasn't found - " + "showing unhighlighted source.", + ) + self._show_html_source(html) + def _show_source_pygments(self) -> None: def show_source_cb(source: str) -> None: """Show source as soon as it's ready.""" - # WORKAROUND for https://github.com/PyCQA/pylint/issues/491 - # pylint: disable=no-member - lexer = pygments.lexers.HtmlLexer() - formatter = pygments.formatters.HtmlFormatter( - full=True, linenos='table') - # pylint: enable=no-member - highlighted = pygments.highlight(source, lexer, formatter) - - tb = objreg.get('tabbed-browser', scope='window', - window=self._tab.win_id) - new_tab = tb.tabopen(background=False, related=True) - new_tab.set_html(highlighted, self._tab.url()) - new_tab.data.viewing_source = True + try: + import pygments + import pygments.lexers + import pygments.formatters + except ImportError: + # Pygments is an optional dependency + self._show_source_fallback(source) + return + + try: + lexer = pygments.lexers.HtmlLexer() + formatter = pygments.formatters.HtmlFormatter( + full=True, linenos='table') + except AttributeError: + # Remaining namespace package from Pygments + self._show_source_fallback(source) + return + + html = pygments.highlight(source, lexer, formatter) + self._show_html_source(html) self._tab.dump_async(show_source_cb) @@ -259,7 +277,7 @@ class AbstractPrinting: diag = QPrintDialog(self._tab) if utils.is_mac: # For some reason we get a segfault when using open() on macOS - ret = diag.exec_() + ret = diag.exec() if ret == QDialog.Accepted: do_print() else: @@ -851,6 +869,7 @@ class AbstractTabPrivate: """Show/hide (and if needed, create) the web inspector for this tab.""" tabdata = self._tab.data if tabdata.inspector is None: + assert tabdata.splitter is not None tabdata.inspector = inspector.create( splitter=tabdata.splitter, win_id=self._tab.win_id) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 18777e250..ec43a3210 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -65,7 +65,7 @@ class CommandDispatcher: def _new_tabbed_browser(self, private): """Get a tabbed-browser from a new window.""" - args = QApplication.instance().arguments() + args = objects.qapp.arguments() if private and '--single-process' in args: raise cmdutils.CommandError("Private windows are unavailable with " "the single-process process model.") @@ -1343,6 +1343,7 @@ class CommandDispatcher: the case for QtWebKit. For QtWebEngine it may display slightly different source. Some JavaScript processing may be applied. + Needs the optional Pygments dependency for highlighting. """ tab = self._current_widget() try: diff --git a/qutebrowser/browser/greasemonkey.py b/qutebrowser/browser/greasemonkey.py index 9e25e49bd..6e66f9cbc 100644 --- a/qutebrowser/browser/greasemonkey.py +++ b/qutebrowser/browser/greasemonkey.py @@ -26,9 +26,9 @@ import fnmatch import functools import glob import textwrap +import dataclasses from typing import cast, List, Sequence -import attr from PyQt5.QtCore import pyqtSignal, QObject, QUrl from qutebrowser.utils import (log, standarddir, jinja, objreg, utils, @@ -198,15 +198,15 @@ class GreasemonkeyScript: self._code = "\n".join([textwrap.indent(source, " "), self._code]) -@attr.s +@dataclasses.dataclass class MatchingScripts: """All userscripts registered to run on a particular url.""" - url = attr.ib() - start = attr.ib(default=attr.Factory(list)) - end = attr.ib(default=attr.Factory(list)) - idle = attr.ib(default=attr.Factory(list)) + url: QUrl + start: List[GreasemonkeyScript] = dataclasses.field(default_factory=list) + end: List[GreasemonkeyScript] = dataclasses.field(default_factory=list) + idle: List[GreasemonkeyScript] = dataclasses.field(default_factory=list) class GreasemonkeyMatcher: diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index f914f3085..0812eda7d 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -25,11 +25,11 @@ import os import re import html import enum +import dataclasses 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 pyqtSignal, pyqtSlot, QObject, Qt, QUrl from PyQt5.QtWidgets import QLabel @@ -150,7 +150,7 @@ class HintLabel(QLabel): self.deleteLater() -@attr.s +@dataclasses.dataclass class HintContext: """Context namespace used for hinting. @@ -181,20 +181,21 @@ class HintContext: group: The group of web elements to hint. """ - 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) + tab: 'browsertab.AbstractTab' + target: Target + rapid: bool + hint_mode: str + add_history: bool + first: bool + baseurl: QUrl + args: List[str] + group: str + + all_labels: List[HintLabel] = dataclasses.field(default_factory=list) + labels: Dict[str, HintLabel] = dataclasses.field(default_factory=dict) + to_follow: Optional[str] = None + first_run: bool = True + filterstr: Optional[str] = None def get_args(self, urlstr: str) -> Sequence[str]: """Get the arguments, with {hint-url} replaced by the given URL.""" @@ -590,7 +591,7 @@ class HintManager(QObject): raise cmdutils.CommandError( "'args' is only allowed with target userscript/spawn.") - def _filter_matches(self, filterstr: str, elemstr: str) -> bool: + def _filter_matches(self, filterstr: Optional[str], elemstr: str) -> bool: """Return True if `filterstr` matches `elemstr`.""" # Empty string and None always match if not filterstr: @@ -758,19 +759,23 @@ class HintManager(QObject): "with target {}!".format(name)) self._check_args(target, *args) - self._context = HintContext() - self._context.tab = tab - self._context.target = target - self._context.rapid = rapid - self._context.hint_mode = self._get_hint_mode(mode) - self._context.add_history = add_history - self._context.first = first + try: - self._context.baseurl = tabbed_browser.current_url() + baseurl = tabbed_browser.current_url() except qtutils.QtValueError: raise cmdutils.CommandError("No URL set for this page yet!") - self._context.args = list(args) - self._context.group = group + + self._context = HintContext( + tab=tab, + target=target, + rapid=rapid, + hint_mode=self._get_hint_mode(mode), + add_history=add_history, + first=first, + baseurl=baseurl, + args=list(args), + group=group, + ) try: selector = webelem.css_selector(self._context.group, diff --git a/qutebrowser/browser/network/pac.py b/qutebrowser/browser/network/pac.py index 3b5686a03..891c5687b 100644 --- a/qutebrowser/browser/network/pac.py +++ b/qutebrowser/browser/network/pac.py @@ -301,7 +301,7 @@ class PACFetcher(QObject): if self._manager is not None: loop = qtutils.EventLoop() self.finished.connect(loop.quit) - loop.exec_() + loop.exec() def fetch_error(self): """Check if PAC script is successfully fetched. diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py index 3aebbb6dd..f2dd01372 100644 --- a/qutebrowser/browser/qtnetworkdownloads.py +++ b/qutebrowser/browser/qtnetworkdownloads.py @@ -23,12 +23,12 @@ import io import os.path import shutil import functools +import dataclasses from typing import Dict, IO, Optional -import attr from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, QUrl from PyQt5.QtWidgets import QApplication -from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply +from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager from qutebrowser.config import config, websettings from qutebrowser.utils import message, usertypes, log, urlutils, utils, debug, objreg @@ -38,11 +38,11 @@ from qutebrowser.browser.webkit import http from qutebrowser.browser.webkit.network import networkmanager -@attr.s +@dataclasses.dataclass class _RetryInfo: - request = attr.ib() - manager = attr.ib() + request: QNetworkRequest + manager: QNetworkAccessManager class DownloadItem(downloads.AbstractDownloadItem): diff --git a/qutebrowser/browser/webengine/interceptor.py b/qutebrowser/browser/webengine/interceptor.py index 8804bea6e..fae142d56 100644 --- a/qutebrowser/browser/webengine/interceptor.py +++ b/qutebrowser/browser/webengine/interceptor.py @@ -19,8 +19,6 @@ """A request interceptor taking care of adblocking and custom headers.""" -import attr - from PyQt5.QtCore import QUrl, QByteArray from PyQt5.QtWebEngineCore import (QWebEngineUrlRequestInterceptor, QWebEngineUrlRequestInfo) @@ -32,26 +30,27 @@ from qutebrowser.extensions import interceptors from qutebrowser.misc import objects -@attr.s class WebEngineRequest(interceptors.Request): """QtWebEngine-specific request interceptor functionality.""" _WHITELISTED_REQUEST_METHODS = {QByteArray(b'GET'), QByteArray(b'HEAD')} - _webengine_info: QWebEngineUrlRequestInfo = attr.ib(default=None) - #: If this request has been redirected already - _redirected: bool = attr.ib(init=False, default=False) + def __init__(self, *args, webengine_info, **kwargs): + super().__init__(*args, **kwargs) + self._webengine_info = webengine_info + self._redirected = False def redirect(self, url: QUrl) -> None: if self._redirected: raise interceptors.RedirectException("Request already redirected.") if self._webengine_info is None: raise interceptors.RedirectException("Request improperly initialized.") + # Redirecting a request that contains payload data is not allowed. # To be safe, abort on any request not in a whitelist. - if (self._webengine_info.requestMethod() - not in self._WHITELISTED_REQUEST_METHODS): + verb = self._webengine_info.requestMethod() + if verb not in self._WHITELISTED_REQUEST_METHODS: raise interceptors.RedirectException( "Request method does not support redirection.") self._webengine_info.redirect(url) diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index 05e7b4b68..4664f1040 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -422,7 +422,7 @@ def _init_site_specific_quirks(): user_agents = { # Needed to avoid a ""WhatsApp works with Google Chrome 36+" error # page which doesn't allow to use WhatsApp Web at all. Also see the - # additional JS quirk: qutebrowser/javascript/whatsapp_web_quirk.user.js + # additional JS quirk: qutebrowser/javascript/quirks/whatsapp_web.user.js # https://github.com/qutebrowser/qutebrowser/issues/4445 'https://web.whatsapp.com/': no_qtwe_ua, diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 955be8c22..efdc8a59e 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -1140,16 +1140,16 @@ class _WebEngineScripts(QObject): page_scripts = self._widget.page().scripts() quirks = [ ( - 'whatsapp_web_quirk', + 'whatsapp_web', QWebEngineScript.DocumentReady, QWebEngineScript.ApplicationWorld, ), ] if not qtutils.version_check('5.13'): - quirks.append(('globalthis_quirk', + quirks.append(('globalthis', QWebEngineScript.DocumentCreation, QWebEngineScript.MainWorld)) - quirks.append(('object_fromentries_quirk', + quirks.append(('object_fromentries', QWebEngineScript.DocumentCreation, QWebEngineScript.MainWorld)) @@ -1158,7 +1158,7 @@ class _WebEngineScripts(QObject): script.setName(filename) script.setWorldId(world) script.setInjectionPoint(injection_point) - src = utils.read_file("javascript/{}.user.js".format(filename)) + src = utils.read_file("javascript/quirks/{}.user.js".format(filename)) script.setSourceCode(src) page_scripts.insert(script) @@ -1529,6 +1529,7 @@ class WebEngineTab(browsertab.AbstractTab): # However, self.url() is not available yet and the requested URL # might not match the URL we get from the error - so we just apply a # heuristic here. + assert self.data.last_navigation is not None if (show_non_overr_cert_error and url.matches(self.data.last_navigation.url, QUrl.RemoveScheme)): self._show_error_page(url, str(error)) diff --git a/qutebrowser/browser/webkit/mhtml.py b/qutebrowser/browser/webkit/mhtml.py index 794e1b73b..bf008d009 100644 --- a/qutebrowser/browser/webkit/mhtml.py +++ b/qutebrowser/browser/webkit/mhtml.py @@ -33,9 +33,9 @@ import email.encoders import email.mime.multipart import email.message import quopri -from typing import MutableMapping, Set, Tuple +import dataclasses +from typing import MutableMapping, Set, Tuple, Callable -import attr from PyQt5.QtCore import QUrl from qutebrowser.browser import downloads @@ -44,13 +44,13 @@ from qutebrowser.utils import log, objreg, message, usertypes, utils, urlutils from qutebrowser.extensions import interceptors -@attr.s +@dataclasses.dataclass class _File: - content = attr.ib() - content_type = attr.ib() - content_location = attr.ib() - transfer_encoding = attr.ib() + content: bytes + content_type: str + content_location: str + transfer_encoding: Callable[[email.message.Message], None] _CSS_URL_PATTERNS = [re.compile(x) for x in [ diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index 3e393680c..40b700c71 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -21,13 +21,12 @@ import collections import html +import dataclasses from typing import TYPE_CHECKING, Dict, MutableMapping, Optional, Sequence -import attr -from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QCoreApplication, QUrl, - QByteArray) +from PyQt5.QtCore import pyqtSlot, pyqtSignal, QUrl, QByteArray from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslSocket, - QSslError) + QSslError, QNetworkProxy) from qutebrowser.config import config from qutebrowser.utils import (message, log, usertypes, utils, objreg, @@ -48,14 +47,14 @@ HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%' _proxy_auth_cache: Dict['ProxyId', 'prompt.AuthInfo'] = {} -@attr.s(frozen=True) +@dataclasses.dataclass(frozen=True) class ProxyId: """Information identifying a proxy server.""" - type = attr.ib() - hostname = attr.ib() - port = attr.ib() + type: QNetworkProxy.ProxyType + hostname: str + port: int def _is_secure_cipher(cipher): @@ -155,7 +154,12 @@ class NetworkManager(QNetworkAccessManager): def __init__(self, *, win_id, tab_id, private, parent=None): log.init.debug("Initializing NetworkManager") - super().__init__(parent) + with log.disable_qt_msghandler(): + # WORKAROUND for a hang when a message is printed - See: + # http://www.riverbankcomputing.com/pipermail/pyqt/2014-November/035045.html + # + # Still needed on Qt/PyQt 5.15.2 according to #6010. + super().__init__(parent) log.init.debug("NetworkManager init done") self.adopted_downloads = 0 self._win_id = win_id @@ -188,8 +192,7 @@ class NetworkManager(QNetworkAccessManager): # We have a shared cookie jar - we restore its parent so we don't # take ownership of it. self.setCookieJar(cookie_jar) - app = QCoreApplication.instance() - cookie_jar.setParent(app) + cookie_jar.setParent(objects.qapp) def _set_cache(self): """Set the cache of the NetworkManager correctly.""" @@ -197,9 +200,8 @@ class NetworkManager(QNetworkAccessManager): return # We have a shared cache - we restore its parent so we don't take # ownership of it. - app = QCoreApplication.instance() self.setCache(cache.diskcache) - cache.diskcache.setParent(app) + cache.diskcache.setParent(objects.qapp) def _get_abort_signals(self, owner=None): """Get a list of signals which should abort a question.""" diff --git a/qutebrowser/browser/webkit/rfc6266.py b/qutebrowser/browser/webkit/rfc6266.py index b43bfb808..10c337ee1 100644 --- a/qutebrowser/browser/webkit/rfc6266.py +++ b/qutebrowser/browser/webkit/rfc6266.py @@ -22,8 +22,9 @@ import urllib.parse import string import re +import dataclasses +from typing import Optional -import attr import pypeg2 as peg from qutebrowser.utils import utils @@ -210,13 +211,13 @@ class ContentDispositionValue: peg.optional(';')) -@attr.s +@dataclasses.dataclass class LangTagged: """A string with an associated language.""" - string = attr.ib() - langtag = attr.ib() + string: str + langtag: Optional[str] class Error(Exception): diff --git a/qutebrowser/browser/webkit/webview.py b/qutebrowser/browser/webkit/webview.py index 428e66744..f2c610d32 100644 --- a/qutebrowser/browser/webkit/webview.py +++ b/qutebrowser/browser/webkit/webview.py @@ -179,7 +179,7 @@ class WebView(QWebView): self.shutting_down.connect(menu.close) mm = modeman.instance(self.win_id) mm.entered.connect(menu.close) - menu.exec_(e.globalPos()) + menu.exec(e.globalPos()) def showEvent(self, e): """Extend showEvent to set the page visibility state to visible. diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 61b44d555..388fcfb81 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -23,28 +23,29 @@ import inspect import collections import traceback import typing -from typing import Any, MutableMapping, MutableSequence, Tuple, Union - -import attr +import dataclasses +from typing import (Any, MutableMapping, MutableSequence, Tuple, Union, List, Optional, + Callable) from qutebrowser.api import cmdutils from qutebrowser.commands import cmdexc, argparser from qutebrowser.utils import log, message, docutils, objreg, usertypes, utils from qutebrowser.utils import debug as debug_utils from qutebrowser.misc import objects +from qutebrowser.completion.models import completionmodel -@attr.s +@dataclasses.dataclass class ArgInfo: """Information about an argument.""" - value = attr.ib(None) - hide = attr.ib(False) - metavar = attr.ib(None) - flag = attr.ib(None) - completion = attr.ib(None) - choices = attr.ib(None) + value: Optional[usertypes.CommandValue] = None + hide: bool = False + metavar: Optional[str] = None + flag: Optional[str] = None + completion: Optional[Callable[..., completionmodel.CompletionModel]] = None + choices: Optional[List[str]] = None class Command: diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index c195a8be9..2a30344cd 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -22,14 +22,15 @@ import traceback import re import contextlib -from typing import TYPE_CHECKING, Callable, Dict, Iterator, Mapping, MutableMapping +import dataclasses +from typing import (TYPE_CHECKING, Callable, Dict, Iterator, Mapping, MutableMapping, + List, Optional) -import attr from PyQt5.QtCore import pyqtSlot, QUrl, QObject from qutebrowser.api import cmdutils from qutebrowser.config import config -from qutebrowser.commands import cmdexc +from qutebrowser.commands import cmdexc, command from qutebrowser.utils import message, objreg, qtutils, usertypes, utils from qutebrowser.misc import split, objects from qutebrowser.keyinput import macros, modeman @@ -42,14 +43,14 @@ _ReplacementFunction = Callable[['tabbedbrowser.TabbedBrowser'], str] last_command = {} -@attr.s +@dataclasses.dataclass class ParseResult: """The result of parsing a commandline.""" - cmd = attr.ib() - args = attr.ib() - cmdline = attr.ib() + cmd: Optional[command.Command] + args: Optional[List[str]] + cmdline: List[str] def _url(tabbed_browser): diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py index d66e3ee40..1819f4694 100644 --- a/qutebrowser/completion/completer.py +++ b/qutebrowser/completion/completer.py @@ -19,7 +19,9 @@ """Completer attached to a CompletionView.""" -import attr +import dataclasses +from typing import TYPE_CHECKING + from PyQt5.QtCore import pyqtSlot, QObject, QTimer from qutebrowser.config import config @@ -27,17 +29,19 @@ from qutebrowser.commands import runners from qutebrowser.misc import objects from qutebrowser.utils import log, utils, debug, objreg from qutebrowser.completion.models import miscmodels +if TYPE_CHECKING: + from qutebrowser.browser import browsertab -@attr.s +@dataclasses.dataclass class CompletionInfo: """Context passed into all completion functions.""" - config = attr.ib() - keyconf = attr.ib() - win_id = attr.ib() - cur_tab = attr.ib() + config: config.Config + keyconf: config.KeyConfig + win_id: int + cur_tab: 'browsertab.AbstractTab' class Completer(QObject): diff --git a/qutebrowser/components/misccommands.py b/qutebrowser/components/misccommands.py index f553fce3b..b8906b7b0 100644 --- a/qutebrowser/components/misccommands.py +++ b/qutebrowser/components/misccommands.py @@ -80,7 +80,7 @@ def _print_preview(tab: apitypes.Tab) -> None: Qt.WindowMinimizeButtonHint) diag.paintRequested.connect(functools.partial( tab.printing.to_printer, callback=print_callback)) - diag.exec_() + diag.exec() def _print_pdf(tab: apitypes.Tab, filename: str) -> None: @@ -339,6 +339,7 @@ def devtools(tab: apitypes.Tab, @cmdutils.argument('tab', value=cmdutils.Value.cur_tab) def devtools_focus(tab: apitypes.Tab) -> None: """Toggle focus between the devtools/tab.""" + assert tab.data.splitter is not None try: tab.data.splitter.cycle_focus() except apitypes.InspectorError as e: diff --git a/qutebrowser/components/readlinecommands.py b/qutebrowser/components/readlinecommands.py index ea8f12edf..44772f10f 100644 --- a/qutebrowser/components/readlinecommands.py +++ b/qutebrowser/components/readlinecommands.py @@ -39,7 +39,11 @@ class _ReadlineBridge: def _widget(self) -> Optional[QLineEdit]: """Get the currently active QLineEdit.""" - w = QApplication.instance().focusWidget() + # FIXME add this to api.utils or so + qapp = QApplication.instance() + assert qapp is not None + w = qapp.focusWidget() + if isinstance(w, QLineEdit): return w else: diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 065527bb9..bb7ac2c5b 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -27,8 +27,8 @@ DATA: A dict of Option objects after init() has been called. from typing import (Any, Dict, Iterable, List, Mapping, MutableMapping, Optional, Sequence, Tuple, Union, cast) import functools +import dataclasses -import attr from qutebrowser.config import configtypes from qutebrowser.utils import usertypes, qtutils, utils from qutebrowser.misc import debugcachestats @@ -39,7 +39,7 @@ MIGRATIONS = cast('Migrations', None) _BackendDict = Mapping[str, Union[str, bool]] -@attr.s +@dataclasses.dataclass(order=True) class Option: """Description of an Option in the config. @@ -47,18 +47,18 @@ class Option: Note that this is just an option which exists, with no value associated. """ - name: str = attr.ib() - typ: configtypes.BaseType = attr.ib() - default: Any = attr.ib() - backends: Iterable[usertypes.Backend] = attr.ib() - raw_backends: Optional[Mapping[str, bool]] = attr.ib() - description: str = attr.ib() - supports_pattern: bool = attr.ib(default=False) - restart: bool = attr.ib(default=False) - no_autoconfig: bool = attr.ib(default=False) + name: str + typ: configtypes.BaseType + default: Any + backends: Iterable[usertypes.Backend] + raw_backends: Optional[Mapping[str, bool]] + description: str + supports_pattern: bool = False + restart: bool = False + no_autoconfig: bool = False -@attr.s +@dataclasses.dataclass class Migrations: """Migrated options in configdata.yml. @@ -68,8 +68,8 @@ class Migrations: deleted: A list of option names which have been removed. """ - renamed: Dict[str, str] = attr.ib(default=attr.Factory(dict)) - deleted: List[str] = attr.ib(default=attr.Factory(list)) + renamed: Dict[str, str] = dataclasses.field(default_factory=dict) + deleted: List[str] = dataclasses.field(default_factory=list) def _raise_invalid_node(name: str, what: str, node: Any) -> None: diff --git a/qutebrowser/config/configexc.py b/qutebrowser/config/configexc.py index 872f777ff..5bf7cafb3 100644 --- a/qutebrowser/config/configexc.py +++ b/qutebrowser/config/configexc.py @@ -19,9 +19,9 @@ """Exceptions related to config parsing.""" +import dataclasses from typing import Any, Mapping, Optional, Sequence, Union -import attr from qutebrowser.utils import usertypes, log @@ -105,7 +105,7 @@ class NoOptionError(Error): self.option = option -@attr.s +@dataclasses.dataclass class ConfigErrorDesc: """A description of an error happening while reading the config. @@ -116,9 +116,9 @@ class ConfigErrorDesc: traceback: The formatted traceback of the exception. """ - text: str = attr.ib() - exception: Union[str, Exception] = attr.ib() - traceback: str = attr.ib(None) + text: str + exception: Union[str, Exception] + traceback: Optional[str] = None def __str__(self) -> str: if self.traceback: diff --git a/qutebrowser/config/configinit.py b/qutebrowser/config/configinit.py index 2951b5292..f0feb8af1 100644 --- a/qutebrowser/config/configinit.py +++ b/qutebrowser/config/configinit.py @@ -130,7 +130,7 @@ def late_init(save_manager: savemanager.SaveManager) -> None: text=_init_errors.to_html(), icon=QMessageBox.Warning, plain_text=False) - errbox.exec_() + errbox.exec() if _init_errors.fatal: sys.exit(usertypes.Exit.err_init) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 6328c3140..3e25fdfd0 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -50,10 +50,10 @@ import itertools import functools import operator import json +import dataclasses from typing import (Any, Callable, Dict as DictType, Iterable, Iterator, List as ListType, Optional, Pattern, Sequence, Tuple, Union) -import attr import yaml from PyQt5.QtCore import QUrl, Qt from PyQt5.QtGui import QColor @@ -1645,15 +1645,15 @@ class FuzzyUrl(BaseType): raise configexc.ValidationError(value, str(e)) -@attr.s +@dataclasses.dataclass class PaddingValues: """Four padding values.""" - top: int = attr.ib() - bottom: int = attr.ib() - left: int = attr.ib() - right: int = attr.ib() + top: int + bottom: int + left: int + right: int class Padding(Dict): diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py index e1cb393dc..b5f07fec0 100644 --- a/qutebrowser/config/websettings.py +++ b/qutebrowser/config/websettings.py @@ -22,9 +22,9 @@ import re import argparse import functools +import dataclasses from typing import Any, Callable, Dict, Optional -import attr from PyQt5.QtCore import QUrl, pyqtSlot, qVersion from PyQt5.QtGui import QFont @@ -36,16 +36,16 @@ from qutebrowser.misc import objects, debugcachestats UNSET = object() -@attr.s +@dataclasses.dataclass class UserAgent: """A parsed user agent.""" - os_info: str = attr.ib() - webkit_version: str = attr.ib() - upstream_browser_key: str = attr.ib() - upstream_browser_version: str = attr.ib() - qt_key: str = attr.ib() + os_info: str + webkit_version: str + upstream_browser_key: str + upstream_browser_version: str + qt_key: str @classmethod def parse(cls, ua: str) -> 'UserAgent': diff --git a/qutebrowser/extensions/interceptors.py b/qutebrowser/extensions/interceptors.py index 9c50346d7..eb4af42a9 100644 --- a/qutebrowser/extensions/interceptors.py +++ b/qutebrowser/extensions/interceptors.py @@ -20,10 +20,9 @@ """Infrastructure for intercepting requests.""" import enum +import dataclasses from typing import Callable, List, Optional -import attr - from PyQt5.QtCore import QUrl @@ -62,21 +61,21 @@ class RedirectException(Exception): """Raised when the request was invalid, or a request was already made.""" -@attr.s +@dataclasses.dataclass class Request: """A request which can be intercepted/blocked.""" #: The URL of the page being shown. - first_party_url: Optional[QUrl] = attr.ib() + first_party_url: Optional[QUrl] #: The URL of the file being requested. - request_url: QUrl = attr.ib() + request_url: QUrl - is_blocked: bool = attr.ib(False) + is_blocked: bool = False #: The resource type of the request. None if not supported on this backend. - resource_type: Optional[ResourceType] = attr.ib(None) + resource_type: Optional[ResourceType] = None def block(self) -> None: """Block this request.""" diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py index b6d86f517..61ede5022 100644 --- a/qutebrowser/extensions/loader.py +++ b/qutebrowser/extensions/loader.py @@ -25,10 +25,9 @@ import sys import pathlib import importlib import argparse +import dataclasses from typing import Callable, Iterator, List, Optional, Set, Tuple -import attr - from PyQt5.QtCore import pyqtSlot from qutebrowser import components @@ -41,17 +40,17 @@ from qutebrowser.misc import objects _module_infos = [] -@attr.s +@dataclasses.dataclass class InitContext: """Context an extension gets in its init hook.""" - data_dir: pathlib.Path = attr.ib() - config_dir: pathlib.Path = attr.ib() - args: argparse.Namespace = attr.ib() + data_dir: pathlib.Path + config_dir: pathlib.Path + args: argparse.Namespace -@attr.s +@dataclasses.dataclass class ModuleInfo: """Information attached to an extension module. @@ -59,19 +58,18 @@ class ModuleInfo: This gets used by qutebrowser.api.hook. """ - _ConfigChangedHooksType = List[Tuple[Optional[str], Callable]] - - skip_hooks: bool = attr.ib(False) - init_hook: Optional[Callable] = attr.ib(None) - config_changed_hooks: _ConfigChangedHooksType = attr.ib(attr.Factory(list)) + skip_hooks: bool = False + init_hook: Optional[Callable] = None + config_changed_hooks: List[Tuple[Optional[str], Callable]] = dataclasses.field( + default_factory=list) -@attr.s +@dataclasses.dataclass class ExtensionInfo: """Information about a qutebrowser extension.""" - name: str = attr.ib() + name: str def add_module_info(module: types.ModuleType) -> ModuleInfo: diff --git a/qutebrowser/html/pre.html b/qutebrowser/html/pre.html index cfcfad359..5fb9c7f40 100644 --- a/qutebrowser/html/pre.html +++ b/qutebrowser/html/pre.html @@ -1,6 +1,9 @@ {% extends "base.html" %} {% block content %} {{ super() }} +{% if preamble is defined %} + <p>{{ preamble }}</p> +{% endif %} <pre> {{ content }} </pre> diff --git a/qutebrowser/javascript/globalthis_quirk.user.js b/qutebrowser/javascript/quirks/globalthis.user.js index 03e74de3c..03e74de3c 100644 --- a/qutebrowser/javascript/globalthis_quirk.user.js +++ b/qutebrowser/javascript/quirks/globalthis.user.js diff --git a/qutebrowser/javascript/object_fromentries_quirk.user.js b/qutebrowser/javascript/quirks/object_fromentries.user.js index 6f6ad8b31..6f6ad8b31 100644 --- a/qutebrowser/javascript/object_fromentries_quirk.user.js +++ b/qutebrowser/javascript/quirks/object_fromentries.user.js diff --git a/qutebrowser/javascript/whatsapp_web_quirk.user.js b/qutebrowser/javascript/quirks/whatsapp_web.user.js index 801d300e1..801d300e1 100644 --- a/qutebrowser/javascript/whatsapp_web_quirk.user.js +++ b/qutebrowser/javascript/quirks/whatsapp_web.user.js diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 23b77cba1..8fe0e3ef8 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -21,9 +21,9 @@ import string import types +import dataclasses from typing import Mapping, MutableMapping, Optional, Sequence -import attr from PyQt5.QtCore import pyqtSignal, QObject, Qt from PyQt5.QtGui import QKeySequence, QKeyEvent @@ -32,16 +32,16 @@ from qutebrowser.utils import usertypes, log, utils from qutebrowser.keyinput import keyutils -@attr.s(frozen=True) +@dataclasses.dataclass(frozen=True) class MatchResult: """The result of matching a keybinding.""" - match_type: QKeySequence.SequenceMatch = attr.ib() - command: Optional[str] = attr.ib() - sequence: keyutils.KeySequence = attr.ib() + match_type: QKeySequence.SequenceMatch + command: Optional[str] + sequence: keyutils.KeySequence - def __attrs_post_init__(self) -> None: + def __post_init__(self) -> None: if self.match_type == QKeySequence.ExactMatch: assert self.command is not None else: diff --git a/qutebrowser/keyinput/eventfilter.py b/qutebrowser/keyinput/eventfilter.py index d77c8702d..2202ebd2d 100644 --- a/qutebrowser/keyinput/eventfilter.py +++ b/qutebrowser/keyinput/eventfilter.py @@ -23,10 +23,9 @@ from typing import cast from PyQt5.QtCore import pyqtSlot, QObject, QEvent from PyQt5.QtGui import QKeyEvent, QWindow -from PyQt5.QtWidgets import QApplication from qutebrowser.keyinput import modeman -from qutebrowser.misc import quitter +from qutebrowser.misc import quitter, objects from qutebrowser.utils import objreg @@ -50,11 +49,11 @@ class EventFilter(QObject): } def install(self) -> None: - QApplication.instance().installEventFilter(self) + objects.qapp.installEventFilter(self) @pyqtSlot() def shutdown(self) -> None: - QApplication.instance().removeEventFilter(self) + objects.qapp.removeEventFilter(self) def _handle_key_event(self, event: QKeyEvent) -> bool: """Handle a key press/release event. @@ -65,7 +64,7 @@ class EventFilter(QObject): Return: True if the event should be filtered, False if it's passed through. """ - active_window = QApplication.instance().activeWindow() + active_window = objects.qapp.activeWindow() if active_window not in objreg.window_registry.values(): # Some other window (print dialog, etc.) is focused so we pass the # event through. @@ -112,6 +111,6 @@ class EventFilter(QObject): def init() -> None: """Initialize the global EventFilter instance.""" - event_filter = EventFilter(parent=QApplication.instance()) + event_filter = EventFilter(parent=objects.qapp) event_filter.install() quitter.instance.shutting_down.connect(event_filter.shutdown) diff --git a/qutebrowser/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index bc8c4a5fe..ca41ce6e8 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -32,9 +32,9 @@ handle what we actually think we do. """ import itertools +import dataclasses from typing import cast, overload, Iterable, Iterator, List, Mapping, Optional, Union -import attr from PyQt5.QtCore import Qt, QEvent from PyQt5.QtGui import QKeySequence, QKeyEvent @@ -336,7 +336,7 @@ def _parse_single_key(keystr: str) -> str: return 'Shift+' + keystr if keystr.isupper() else keystr -@attr.s(frozen=True) +@dataclasses.dataclass(frozen=True, order=True) class KeyInfo: """A key with optional modifiers. @@ -346,8 +346,8 @@ class KeyInfo: modifiers: A Qt::KeyboardModifiers enum value. """ - key: Qt.Key = attr.ib() - modifiers: _ModifierType = attr.ib() + key: Qt.Key + modifiers: _ModifierType @classmethod def from_event(cls, e: QKeyEvent) -> 'KeyInfo': diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index dcc6fa949..4832e5769 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -20,12 +20,11 @@ """Mode manager (per window) which handles the current keyboard mode.""" import functools +import dataclasses from typing import Mapping, Callable, MutableMapping, Union, Set, cast -import attr from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QObject, QEvent from PyQt5.QtGui import QKeyEvent -from PyQt5.QtWidgets import QApplication from qutebrowser.commands import runners from qutebrowser.keyinput import modeparsers, basekeyparser @@ -33,6 +32,7 @@ from qutebrowser.config import config from qutebrowser.api import cmdutils from qutebrowser.utils import usertypes, log, objreg, utils from qutebrowser.browser import hints +from qutebrowser.misc import objects INPUT_MODES = [usertypes.KeyMode.insert, usertypes.KeyMode.passthrough] PROMPT_MODES = [usertypes.KeyMode.prompt, usertypes.KeyMode.yesno] @@ -40,7 +40,7 @@ PROMPT_MODES = [usertypes.KeyMode.prompt, usertypes.KeyMode.yesno] ParserDictType = MutableMapping[usertypes.KeyMode, basekeyparser.BaseKeyParser] -@attr.s(frozen=True) +@dataclasses.dataclass(frozen=True) class KeyEvent: """A small wrapper over a QKeyEvent storing its data. @@ -54,8 +54,8 @@ class KeyEvent: text: A string (QKeyEvent::text). """ - key: Qt.Key = attr.ib() - text: str = attr.ib() + key: Qt.Key + text: str @classmethod def from_event(cls, event: QKeyEvent) -> 'KeyEvent': @@ -307,7 +307,7 @@ class ModeManager(QObject): self._releaseevents_to_pass.add(KeyEvent.from_event(event)) if curmode != usertypes.KeyMode.insert: - focus_widget = QApplication.instance().focusWidget() + focus_widget = objects.qapp.focusWidget() log.modes.debug("match: {}, forward_unbound_keys: {}, " "passthrough: {}, is_non_alnum: {}, dry_run: {} " "--> filter: {} (focused: {!r})".format( diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 6273b3382..21fb704e1 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -27,7 +27,7 @@ from typing import List, MutableSequence, Optional, Tuple, cast from PyQt5.QtCore import (pyqtBoundSignal, pyqtSlot, QRect, QPoint, QTimer, Qt, QCoreApplication, QEventLoop, QByteArray) -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QSizePolicy +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QSizePolicy from PyQt5.QtGui import QPalette from qutebrowser.commands import runners @@ -39,7 +39,7 @@ from qutebrowser.mainwindow import messageview, prompt from qutebrowser.completion import completionwidget, completer from qutebrowser.keyinput import modeman from qutebrowser.browser import commands, downloadview, hints, downloads -from qutebrowser.misc import crashsignal, keyhintwidget, sessions +from qutebrowser.misc import crashsignal, keyhintwidget, sessions, objects from qutebrowser.qt import sip @@ -100,7 +100,7 @@ def raise_window(window, alert=True): window.activateWindow() if alert: - QApplication.instance().alert(window) + objects.qapp.alert(window) def get_target_window(): @@ -275,7 +275,7 @@ class MainWindow(QWidget): QTimer.singleShot(0, self._connect_overlay_signals) config.instance.changed.connect(self._on_config_changed) - QApplication.instance().new_window.emit(self) + objects.qapp.new_window.emit(self) self._set_decoration(config.val.window.hide_decoration) self.state_before_fullscreen = self.windowState() diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 42b6c3d97..9ca07b9ab 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -23,9 +23,9 @@ import os.path import html import collections import functools +import dataclasses from typing import Deque, MutableSequence, Optional, cast -import attr from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelIndex, QItemSelectionModel, QObject, QEventLoop) from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit, @@ -43,13 +43,13 @@ from qutebrowser.utils import urlmatch prompt_queue = cast('PromptQueue', None) -@attr.s +@dataclasses.dataclass class AuthInfo: """Authentication info returned by a prompt.""" - user = attr.ib() - password = attr.ib() + user: str + password: str class Error(Exception): @@ -194,11 +194,11 @@ class PromptQueue(QObject): loop.destroyed.connect(lambda: self._loops.remove(loop)) question.completed.connect(loop.quit) question.completed.connect(loop.deleteLater) - log.prompt.debug("Starting loop.exec_() for {}".format(question)) + log.prompt.debug("Starting loop.exec() for {}".format(question)) flags = cast(QEventLoop.ProcessEventsFlags, QEventLoop.ExcludeSocketNotifiers) - loop.exec_(flags) - log.prompt.debug("Ending loop.exec_() for {}".format(question)) + loop.exec(flags) + log.prompt.debug("Ending loop.exec() for {}".format(question)) log.prompt.debug("Restoring old question {}".format(old_question)) self._question = old_question diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 821ea030b..ae9653d80 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -20,7 +20,8 @@ """The main statusbar widget.""" import enum -import attr +import dataclasses + from PyQt5.QtCore import (pyqtSignal, pyqtSlot, # type: ignore[attr-defined] pyqtProperty, Qt, QSize, QTimer) from PyQt5.QtWidgets import QWidget, QHBoxLayout, QStackedLayout, QSizePolicy @@ -34,7 +35,7 @@ from qutebrowser.mainwindow.statusbar import (backforward, command, progress, tabindex, textbase) -@attr.s +@dataclasses.dataclass class ColorFlags: """Flags which change the appearance of the statusbar. @@ -56,12 +57,12 @@ class ColorFlags: on = enum.auto() selection = enum.auto() - prompt = attr.ib(False) - insert = attr.ib(False) - command = attr.ib(False) - caret = attr.ib(CaretMode.off) - private = attr.ib(False) - passthrough = attr.ib(False) + prompt: bool = False + insert: bool = False + command: bool = False + caret: CaretMode = CaretMode.off + private: bool = False + passthrough: bool = False def to_stringlist(self): """Get a string list of set flags used in the stylesheet. diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 78eb864a6..cc52bffcf 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -23,10 +23,10 @@ import collections import functools import weakref import datetime +import dataclasses from typing import ( Any, Deque, List, Mapping, MutableMapping, MutableSequence, Optional, Tuple) -import attr from PyQt5.QtWidgets import QSizePolicy, QWidget, QApplication from PyQt5.QtCore import pyqtSignal, pyqtSlot, QTimer, QUrl @@ -39,16 +39,20 @@ from qutebrowser.utils import (log, usertypes, utils, qtutils, objreg, from qutebrowser.misc import quitter -@attr.s +@dataclasses.dataclass class _UndoEntry: """Information needed for :undo.""" - url = attr.ib() - history = attr.ib() - index = attr.ib() - pinned = attr.ib() - created_at = attr.ib(attr.Factory(datetime.datetime.now)) + url: QUrl + history: bytes + index: int + pinned: bool + created_at: datetime.datetime = dataclasses.field( + default_factory=datetime.datetime.now) + + +UndoStackType = MutableSequence[MutableSequence[_UndoEntry]] class TabDeque: @@ -221,8 +225,7 @@ class TabbedBrowser(QWidget): # This init is never used, it is immediately thrown away in the next # line. - self.undo_stack: MutableSequence[MutableSequence[_UndoEntry]] = ( - collections.deque()) + self.undo_stack: UndoStackType = collections.deque() self._update_stack_size() self._filter = signalfilter.SignalFilter(win_id, self) self._now_focused = None diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index 5263ecff9..deefac80d 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -21,9 +21,9 @@ import functools import contextlib +import dataclasses from typing import Optional, cast -import attr from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QSize, QRect, QPoint, QTimer, QUrl) from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle, @@ -712,7 +712,7 @@ class TabBar(QTabBar): tabbed_browser.wheelEvent(e) -@attr.s +@dataclasses.dataclass class Layouts: """Layout information for tab. @@ -720,9 +720,9 @@ class Layouts: Used by TabBarStyle._tab_layout(). """ - text = attr.ib() - icon = attr.ib() - indicator = attr.ib() + text: QRect + icon: QRect + indicator: QRect class TabBarStyle(QCommonStyle): diff --git a/qutebrowser/mainwindow/windowundo.py b/qutebrowser/mainwindow/windowundo.py index af7b2766a..6ca8fb30d 100644 --- a/qutebrowser/mainwindow/windowundo.py +++ b/qutebrowser/mainwindow/windowundo.py @@ -20,26 +20,28 @@ """Code for :undo --window.""" import collections -from typing import MutableSequence, cast +import dataclasses +from typing import MutableSequence, cast, TYPE_CHECKING -import attr -from PyQt5.QtCore import QObject -from PyQt5.QtWidgets import QApplication +from PyQt5.QtCore import QObject, QByteArray from qutebrowser.config import config from qutebrowser.mainwindow import mainwindow +from qutebrowser.misc import objects +if TYPE_CHECKING: + from qutebrowser.mainwindow import tabbedbrowser instance = cast('WindowUndoManager', None) -@attr.s +@dataclasses.dataclass class _WindowUndoEntry: """Information needed for :undo -w.""" - geometry = attr.ib() - tab_stack = attr.ib() + geometry: QByteArray + tab_stack: 'tabbedbrowser.UndoStackType' class WindowUndoManager(QObject): @@ -49,7 +51,7 @@ class WindowUndoManager(QObject): def __init__(self, parent=None): super().__init__(parent) self._undos: MutableSequence[_WindowUndoEntry] = collections.deque() - QApplication.instance().window_closing.connect(self._on_window_closing) + objects.qapp.window_closing.connect(self._on_window_closing) config.instance.changed.connect(self._on_config_changed) @config.change_filter('tabs.undo_stack_size') @@ -88,4 +90,4 @@ class WindowUndoManager(QObject): def init(): global instance - instance = WindowUndoManager(parent=QApplication.instance()) + instance = WindowUndoManager(parent=objects.qapp) diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py index e459d81c1..bac3fabc0 100644 --- a/qutebrowser/misc/backendproblem.py +++ b/qutebrowser/misc/backendproblem.py @@ -26,12 +26,12 @@ import html import enum import shutil import argparse -from typing import Any, List, Sequence, Tuple +import dataclasses +from typing import Any, List, Sequence, Tuple, Optional -import attr from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import (QApplication, QDialog, QPushButton, QHBoxLayout, - QVBoxLayout, QLabel, QMessageBox, QWidget) +from PyQt5.QtWidgets import (QDialog, QPushButton, QHBoxLayout, QVBoxLayout, QLabel, + QMessageBox, QWidget) from PyQt5.QtNetwork import QSslSocket from qutebrowser.config import config, configfiles @@ -50,15 +50,15 @@ class _Result(enum.IntEnum): restart_webengine = QDialog.Accepted + 4 -@attr.s +@dataclasses.dataclass class _Button: """A button passed to BackendProblemDialog.""" - text: str = attr.ib() - setting: str = attr.ib() - value: Any = attr.ib() - default: bool = attr.ib(default=False) + text: str + setting: str + value: Any + default: bool = False def _other_backend(backend: usertypes.Backend) -> Tuple[usertypes.Backend, str]: @@ -150,15 +150,13 @@ class _Dialog(QDialog): self.done(_Result.restart) -@attr.s +@dataclasses.dataclass class _BackendImports: """Whether backend modules could be imported.""" - 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) + webkit_error: Optional[str] = None + webengine_error: Optional[str] = None class _BackendProblemChecker: @@ -180,7 +178,7 @@ class _BackendProblemChecker: dialog = _Dialog(*args, **kwargs) - status = dialog.exec_() + status = dialog.exec() self._save_manager.save_all(is_exit=True) if status in [_Result.quit, QDialog.Rejected]: @@ -236,7 +234,7 @@ class _BackendProblemChecker: if os.environ.get('QUTE_SKIP_WAYLAND_WEBGL_CHECK'): return - platform = QApplication.instance().platformName() + platform = objects.qapp.platformName() if platform not in ['wayland', 'wayland-egl']: return @@ -284,29 +282,15 @@ class _BackendProblemChecker: from PyQt5.QtWebKit import qWebKitVersion from PyQt5 import QtWebKitWidgets except (ImportError, ValueError) as e: - results.webkit_available = False results.webkit_error = str(e) else: - if qtutils.is_new_qtwebkit(): - results.webkit_available = True - else: - results.webkit_available = False + if not qtutils.is_new_qtwebkit(): results.webkit_error = "Unsupported legacy QtWebKit found" try: from PyQt5 import QtWebEngineWidgets except (ImportError, ValueError) as e: - results.webengine_available = False results.webengine_error = str(e) - else: - results.webengine_available = True - - assert results.webkit_available is not None - assert results.webengine_available is not None - if not results.webkit_available: - assert results.webkit_error is not None - if not results.webengine_available: - assert results.webengine_error is not None return results @@ -338,7 +322,7 @@ class _BackendProblemChecker: text="Could not initialize SSL support.", icon=QMessageBox.Critical, plain_text=False) - errbox.exec_() + errbox.exec() sys.exit(usertypes.Exit.err_init) assert not fatal @@ -348,9 +332,9 @@ class _BackendProblemChecker: """Check for the modules needed for QtWebKit/QtWebEngine.""" imports = self._try_import_backends() - if imports.webkit_available and imports.webengine_available: + if not imports.webkit_error and not imports.webengine_error: return - elif not imports.webkit_available and not imports.webengine_available: + elif imports.webkit_error and imports.webengine_error: text = ("<p>qutebrowser needs QtWebKit or QtWebEngine, but " "neither could be imported!</p>" "<p>The errors encountered were:<ul>" @@ -364,12 +348,11 @@ class _BackendProblemChecker: text=text, icon=QMessageBox.Critical, plain_text=False) - errbox.exec_() + errbox.exec() sys.exit(usertypes.Exit.err_init) elif objects.backend == usertypes.Backend.QtWebKit: - if imports.webkit_available: + if not imports.webkit_error: return - assert imports.webengine_available self._show_dialog( backend=usertypes.Backend.QtWebKit, because="QtWebKit could not be imported", @@ -377,9 +360,8 @@ class _BackendProblemChecker: html.escape(imports.webkit_error)) ) elif objects.backend == usertypes.Backend.QtWebEngine: - if imports.webengine_available: + if not imports.webengine_error: return - assert imports.webkit_available self._show_dialog( backend=usertypes.Backend.QtWebEngine, because="QtWebEngine could not be imported", diff --git a/qutebrowser/misc/checkpyver.py b/qutebrowser/misc/checkpyver.py index 6f6659a24..28ababcb9 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 < 0x03060000: + if sys.hexversion < 0x03060100: # 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.6 is required to run qutebrowser, but " + + text = ("At least Python 3.6.1 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/crashdialog.py b/qutebrowser/misc/crashdialog.py index 41160198a..76791ae18 100644 --- a/qutebrowser/misc/crashdialog.py +++ b/qutebrowser/misc/crashdialog.py @@ -33,12 +33,12 @@ from typing import List, Tuple from PyQt5.QtCore import pyqtSlot, Qt, QSize from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton, QVBoxLayout, QHBoxLayout, QCheckBox, - QDialogButtonBox, QApplication, QMessageBox) + QDialogButtonBox, QMessageBox) import qutebrowser from qutebrowser.utils import version, log, utils from qutebrowser.misc import (miscwidgets, autoupdate, msgbox, httpclient, - pastebin) + pastebin, objects) from qutebrowser.config import config, configfiles from qutebrowser.browser import history @@ -241,8 +241,7 @@ class _CrashDialog(QDialog): exc: An exception tuple (type, value, traceback) """ try: - application = QApplication.instance() - launch_time = application.launch_time.ctime() + launch_time = objects.qapp.launch_time.ctime() crash_time = datetime.datetime.now().ctime() text = 'Launch: {}\nCrash: {}'.format(launch_time, crash_time) self._crash_info.append(('Timestamps', text)) diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py index d07d8e49c..ab43dc795 100644 --- a/qutebrowser/misc/crashsignal.py +++ b/qutebrowser/misc/crashsignal.py @@ -29,9 +29,9 @@ import argparse import functools import threading import faulthandler -from typing import TYPE_CHECKING, Optional, MutableMapping, cast +import dataclasses +from typing import TYPE_CHECKING, Optional, MutableMapping, cast, List -import attr from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QObject, QSocketNotifier, QTimer, QUrl) from PyQt5.QtWidgets import QApplication @@ -44,14 +44,14 @@ if TYPE_CHECKING: from qutebrowser.misc import quitter -@attr.s +@dataclasses.dataclass class ExceptionInfo: """Information stored when there was an exception.""" - pages = attr.ib() - cmd_history = attr.ib() - objects = attr.ib() + pages: List[List[str]] + cmd_history: List[str] + objects: str crash_handler = cast('CrashHandler', None) @@ -295,7 +295,7 @@ class CrashHandler(QObject): self._crash_dialog = crashdialog.ExceptionCrashDialog( self._args.debug, info.pages, info.cmd_history, exc, info.objects) - ret = self._crash_dialog.exec_() + ret = self._crash_dialog.exec() if ret == crashdialog.Result.restore: self._quitter.restart(info.pages) diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py index d742a6706..f1afedca7 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.6 features available. +At this point we can be sure we have all python 3.6.1 features available. """ try: @@ -95,7 +95,7 @@ def _die(message, exception=None): message) msgbox.setTextFormat(Qt.RichText) msgbox.resize(msgbox.sizeHint()) - msgbox.exec_() + msgbox.exec() app.quit() sys.exit(1) @@ -228,9 +228,8 @@ def check_libraries(): 'pkg_resources': _missing_str("pkg_resources/setuptools"), 'pypeg2': _missing_str("pypeg2"), 'jinja2': _missing_str("jinja2"), - 'pygments': _missing_str("pygments"), 'yaml': _missing_str("PyYAML"), - 'attr': _missing_str("attrs"), + 'dataclasses': _missing_str("dataclasses"), 'PyQt5.QtQml': _missing_str("PyQt5.QtQml"), 'PyQt5.QtSql': _missing_str("PyQt5.QtSql"), 'PyQt5.QtOpenGL': _missing_str("PyQt5.QtOpenGL"), diff --git a/qutebrowser/misc/msgbox.py b/qutebrowser/misc/msgbox.py index f06eccd92..bf126da13 100644 --- a/qutebrowser/misc/msgbox.py +++ b/qutebrowser/misc/msgbox.py @@ -31,7 +31,7 @@ class DummyBox: """A dummy QMessageBox returned when --no-err-windows is used.""" - def exec_(self): + def exec(self): pass diff --git a/qutebrowser/misc/objects.py b/qutebrowser/misc/objects.py index c2e20e9ad..e8815d13a 100644 --- a/qutebrowser/misc/objects.py +++ b/qutebrowser/misc/objects.py @@ -26,6 +26,7 @@ import argparse from typing import TYPE_CHECKING, Any, Dict, Set, Union, cast if TYPE_CHECKING: + from PyQt5.QtWidgets import QApplication from qutebrowser.utils import usertypes from qutebrowser.commands import command @@ -46,3 +47,4 @@ backend: Union['usertypes.Backend', NoBackend] = NoBackend() commands: Dict[str, 'command.Command'] = {} debug_flags: Set[str] = set() args = cast(argparse.Namespace, None) +qapp = cast('QApplication', None) diff --git a/qutebrowser/misc/quitter.py b/qutebrowser/misc/quitter.py index 86342d57b..2aadac52c 100644 --- a/qutebrowser/misc/quitter.py +++ b/qutebrowser/misc/quitter.py @@ -32,7 +32,6 @@ import subprocess from typing import Iterable, Mapping, MutableSequence, Sequence, cast from PyQt5.QtCore import QObject, pyqtSignal, QTimer -from PyQt5.QtWidgets import QApplication try: import hunter except ImportError: @@ -267,7 +266,7 @@ class Quitter(QObject): else: print("Now logging late shutdown.", file=sys.stderr) hunter.trace() - QApplication.instance().exit(status) + objects.qapp.exit(status) @cmdutils.register(name='quit') @@ -311,7 +310,6 @@ def restart() -> None: def init(args: argparse.Namespace) -> None: """Initialize the global Quitter instance.""" global instance - qapp = QApplication.instance() - instance = Quitter(args=args, parent=qapp) + instance = Quitter(args=args, parent=objects.qapp) instance.shutting_down.connect(log.shutdown_log) - qapp.lastWindowClosed.connect(instance.on_last_window_closed) + objects.qapp.lastWindowClosed.connect(instance.on_last_window_closed) diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index b4aa72f32..d38b538d4 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -28,7 +28,6 @@ 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 import yaml from qutebrowser.utils import (standarddir, objreg, qtutils, log, message, @@ -38,6 +37,7 @@ from qutebrowser.config import config, configfiles from qutebrowser.completion.models import miscmodels from qutebrowser.mainwindow import mainwindow from qutebrowser.qt import sip +from qutebrowser.misc import objects _JsonType = MutableMapping[str, Any] @@ -283,7 +283,7 @@ class SessionManager(QObject): continue win_data: _JsonType = {} - active_window = QApplication.instance().activeWindow() + active_window = objects.qapp.activeWindow() if getattr(active_window, 'win_id', None) == win_id: win_data['active'] = True win_data['geometry'] = bytes(main_window.saveGeometry()) diff --git a/qutebrowser/misc/sql.py b/qutebrowser/misc/sql.py index 090c3a5d2..7016e60f6 100644 --- a/qutebrowser/misc/sql.py +++ b/qutebrowser/misc/sql.py @@ -273,7 +273,7 @@ class Query: self._bind_values(values) log.sql.debug('query bindings: {}'.format(self.bound_values())) - ok = self.query.exec_() + ok = self.query.exec() self._check_ok('exec', ok) return self diff --git a/qutebrowser/misc/throttle.py b/qutebrowser/misc/throttle.py index 3540d8824..646843e93 100644 --- a/qutebrowser/misc/throttle.py +++ b/qutebrowser/misc/throttle.py @@ -19,20 +19,20 @@ """A throttle for throttling function calls.""" +import dataclasses import time from typing import Any, Callable, Mapping, Optional, Sequence -import attr from PyQt5.QtCore import QObject from qutebrowser.utils import usertypes -@attr.s +@dataclasses.dataclass class _CallArgs: - args: Sequence[Any] = attr.ib() - kwargs: Mapping[str, Any] = attr.ib() + args: Sequence[Any] + kwargs: Mapping[str, Any] class Throttle(QObject): diff --git a/qutebrowser/utils/debug.py b/qutebrowser/utils/debug.py index 229a26ead..531996e03 100644 --- a/qutebrowser/utils/debug.py +++ b/qutebrowser/utils/debug.py @@ -29,9 +29,9 @@ from typing import ( Any, Callable, List, Mapping, MutableSequence, Optional, Sequence, Type, Union) from PyQt5.QtCore import Qt, QEvent, QMetaMethod, QObject, pyqtBoundSignal -from PyQt5.QtWidgets import QApplication from qutebrowser.utils import log, utils, qtutils, objreg +from qutebrowser.misc import objects from qutebrowser.qt import sip @@ -314,7 +314,7 @@ class log_time: # noqa: N801,N806 pylint: disable=invalid-name def _get_widgets() -> Sequence[str]: """Get a string list of all widgets.""" - widgets = QApplication.instance().allWidgets() + widgets = objects.qapp.allWidgets() widgets.sort(key=repr) return [repr(w) for w in widgets] @@ -338,7 +338,7 @@ def get_all_objects(start_obj: QObject = None) -> str: output += widget_lines if start_obj is None: - start_obj = QApplication.instance() + start_obj = objects.qapp pyqt_lines: List[str] = [] _get_pyqt_objects(pyqt_lines, start_obj) diff --git a/qutebrowser/utils/error.py b/qutebrowser/utils/error.py index 4cba06a10..a49798021 100644 --- a/qutebrowser/utils/error.py +++ b/qutebrowser/utils/error.py @@ -71,4 +71,4 @@ def handle_fatal_exc(exc: BaseException, if post_text: msg_text += '\n\n{}'.format(post_text) msgbox = QMessageBox(QMessageBox.Critical, title, msg_text) - msgbox.exec_() + msgbox.exec() diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 9aedb419f..fa6d9beaf 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -234,6 +234,16 @@ def _init_py_warnings() -> None: @contextlib.contextmanager +def disable_qt_msghandler() -> Iterator[None]: + """Contextmanager which temporarily disables the Qt message handler.""" + old_handler = QtCore.qInstallMessageHandler(None) + try: + yield + finally: + QtCore.qInstallMessageHandler(old_handler) + + +@contextlib.contextmanager def py_warning_filter(action: str = 'ignore', **kwargs: Any) -> Iterator[None]: """Contextmanager to temporarily disable certain Python warnings.""" warnings.filterwarnings(action, **kwargs) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index cd6ea2b32..a26438dd3 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -37,7 +37,6 @@ from PyQt5.QtCore import (qVersion, QEventLoop, QDataStream, QByteArray, QIODevice, QFileDevice, QSaveFile, QT_VERSION_STR, PYQT_VERSION_STR, QObject, QUrl) from PyQt5.QtGui import QColor -from PyQt5.QtWidgets import QApplication try: from PyQt5.QtWebKit import qWebKitVersion except ImportError: # pragma: no cover @@ -126,7 +125,7 @@ def is_single_process() -> bool: if objects.backend == usertypes.Backend.QtWebKit: return False assert objects.backend == usertypes.Backend.QtWebEngine, objects.backend - args = QApplication.instance().arguments() + args = objects.qapp.arguments() return '--single-process' in args @@ -450,14 +449,14 @@ class EventLoop(QEventLoop): """A thin wrapper around QEventLoop. - Raises an exception when doing exec_() multiple times. + Raises an exception when doing exec() multiple times. """ def __init__(self, parent: QObject = None) -> None: super().__init__(parent) self._executing = False - def exec_( + def exec( self, flags: QEventLoop.ProcessEventsFlags = cast(QEventLoop.ProcessEventsFlags, QEventLoop.AllEvents) @@ -466,7 +465,7 @@ class EventLoop(QEventLoop): if self._executing: raise AssertionError("Eventloop is already running!") self._executing = True - status = super().exec_(flags) + status = super().exec(flags) self._executing = False return status diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index 893dae877..cc102327e 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -21,9 +21,9 @@ import operator import enum +import dataclasses from typing import Any, Optional, Sequence, TypeVar, Union -import attr from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer from PyQt5.QtCore import QUrl @@ -496,7 +496,7 @@ class AbstractCertificateErrorWrapper: raise NotImplementedError -@attr.s +@dataclasses.dataclass class NavigationRequest: """A request to navigate to the given URL.""" @@ -526,7 +526,7 @@ class NavigationRequest: #: 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) + url: QUrl + navigation_type: Type + is_main_frame: bool + accepted: bool = True diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 175d0d715..74ad73833 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -31,9 +31,9 @@ import enum import datetime import getpass import functools +import dataclasses from typing import Mapping, Optional, Sequence, Tuple, cast -import attr from PyQt5.QtCore import PYQT_VERSION_STR, QLibraryInfo from PyQt5.QtNetwork import QSslSocket from PyQt5.QtGui import (QOpenGLContext, QOpenGLVersionProfile, @@ -76,15 +76,15 @@ _LOGO = r''' ''' -@attr.s +@dataclasses.dataclass class DistributionInfo: """Information about the running distribution.""" - id: Optional[str] = attr.ib() - parsed: 'Distribution' = attr.ib() - version: Optional[utils.VersionNumber] = attr.ib() - pretty: str = attr.ib() + id: Optional[str] + parsed: 'Distribution' + version: Optional[utils.VersionNumber] + pretty: str pastebin_url = None @@ -365,7 +365,7 @@ MODULE_INFO: Mapping[str, ModuleInfo] = collections.OrderedDict([ ('pygments', ['__version__']), ('yaml', ['__version__']), ('adblock', ['__version__'], "0.3.2"), - ('attr', ['__version__']), + ('dataclasses', []), ('importlib_resources', []), ('PyQt5.QtWebEngineWidgets', []), ('PyQt5.QtWebEngine', ['PYQT_WEBENGINE_VERSION_STR']), @@ -529,8 +529,7 @@ def _backend() -> str: def _uptime() -> datetime.timedelta: - launch_time = QApplication.instance().launch_time - time_delta = datetime.datetime.now() - launch_time + time_delta = datetime.datetime.now() - objects.qapp.launch_time # Round off microseconds time_delta -= datetime.timedelta(microseconds=time_delta.microseconds) return time_delta @@ -576,11 +575,10 @@ def version_info() -> str: if QSslSocket.supportsSsl() else 'no'), ] - qapp = QApplication.instance() - if qapp: - style = qapp.style() + if objects.qapp: + style = objects.qapp.style() lines.append('Style: {}'.format(style.metaObject().className())) - lines.append('Platform plugin: {}'.format(qapp.platformName())) + lines.append('Platform plugin: {}'.format(objects.qapp.platformName())) lines.append('OpenGL: {}'.format(opengl_info())) importpath = os.path.dirname(os.path.abspath(qutebrowser.__file__)) @@ -625,27 +623,27 @@ def version_info() -> str: return '\n'.join(lines) -@attr.s +@dataclasses.dataclass class OpenGLInfo: """Information about the OpenGL setup in use.""" # If we're using OpenGL ES. If so, no further information is available. - gles: bool = attr.ib(False) + gles: bool = False # The name of the vendor. Examples: # - nouveau # - "Intel Open Source Technology Center", "Intel", "Intel Inc." - vendor: Optional[str] = attr.ib(None) + vendor: Optional[str] = None # The OpenGL version as a string. See tests for examples. - version_str: Optional[str] = attr.ib(None) + version_str: Optional[str] = None # The parsed version as a (major, minor) tuple of ints - version: Optional[Tuple[int, ...]] = attr.ib(None) + version: Optional[Tuple[int, ...]] = None # The vendor specific information following the version number - vendor_specific: Optional[str] = attr.ib(None) + vendor_specific: Optional[str] = None def __str__(self) -> str: if self.gles: diff --git a/requirements.txt b/requirements.txt index 48c1991a3..b8fab9abf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,8 @@ adblock==0.4.0 attrs==20.3.0 colorama==0.4.4 -importlib-resources==4.1.1 ; python_version<"3.9" +dataclasses==0.6 ; python_version<"3.7" +importlib-resources==5.0.0 ; python_version<"3.9" Jinja2==2.11.2 MarkupSafe==1.1.1 Pygments==2.7.3 diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py index dae90d636..416628c45 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -25,10 +25,9 @@ import os.path import sys import enum import subprocess +import dataclasses from xml.etree import ElementTree -import attr - sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) @@ -36,14 +35,14 @@ from scripts import utils as scriptutils from qutebrowser.utils import utils -@attr.s +@dataclasses.dataclass class Message: """A message shown by coverage.py.""" - typ = attr.ib() - filename = attr.ib() - text = attr.ib() + typ: str + filename: str + text: str def show(self): """Print this message.""" diff --git a/scripts/dev/get_coredumpctl_traces.py b/scripts/dev/get_coredumpctl_traces.py index 31d70dd74..7bdc9d811 100644 --- a/scripts/dev/get_coredumpctl_traces.py +++ b/scripts/dev/get_coredumpctl_traces.py @@ -26,8 +26,7 @@ import sys import argparse import subprocess import tempfile - -import attr +import dataclasses sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir, os.pardir)) @@ -35,18 +34,18 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir, from scripts import utils -@attr.s +@dataclasses.dataclass class Line: """A line in "coredumpctl list".""" - time = attr.ib() - pid = attr.ib() - uid = attr.ib() - gid = attr.ib() - sig = attr.ib() - present = attr.ib() - exe = attr.ib() + time: str + pid: int + uid: int + gid: int + sig: int + present: bool + exe: str def _convert_present(data): diff --git a/scripts/dev/misc_checks.py b/scripts/dev/misc_checks.py index 4b5699086..5c654235c 100644 --- a/scripts/dev/misc_checks.py +++ b/scripts/dev/misc_checks.py @@ -198,6 +198,22 @@ def check_spelling(args: argparse.Namespace) -> Optional[bool]: "Don't use monkeypatch.setattr('obj.attr', value), use " "setattr(obj, 'attr', value) instead.", ), + ( + re.compile(r'(exec|print)_\('), + ".exec_()/.print_() are removed in PyQt 6, use .exec()/.print() instead.", + ), + ( + re.compile(r'qApp'), + "qApp is removed in PyQt 6, use QApplication.instance() instead.", + ), + ( + re.compile(r'PYQT_CONFIGURATION'), + "PYQT_CONFIGURATION is removed in PyQt 6", + ), + ( + re.compile(r'Q_(ENUM|FLAG)'), + "Q_ENUM and Q_FLAG are removed in PyQt 6", + ), ] # Files which should be ignored, e.g. because they come from another diff --git a/scripts/dev/recompile_requirements.py b/scripts/dev/recompile_requirements.py index e43a3111d..52ecf61f2 100644 --- a/scripts/dev/recompile_requirements.py +++ b/scripts/dev/recompile_requirements.py @@ -176,6 +176,7 @@ CHANGELOG_URLS = { 'adblock': 'https://github.com/ArniDagur/python-adblock/blob/master/CHANGELOG.md', 'pyPEG2': None, 'importlib-resources': 'https://importlib-resources.readthedocs.io/en/latest/history.html', + 'dataclasses': 'https://github.com/ericvsmith/dataclasses#release-history', } diff --git a/scripts/dictcli.py b/scripts/dictcli.py index 4e38727dd..7c575a641 100755 --- a/scripts/dictcli.py +++ b/scripts/dictcli.py @@ -31,8 +31,8 @@ import os import sys import re import urllib.request - -import attr +import dataclasses +from typing import Optional sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) from qutebrowser.browser.webengine import spell @@ -52,17 +52,17 @@ class InvalidLanguageError(Exception): super().__init__(msg) -@attr.s +@dataclasses.dataclass class Language: """Dictionary language specs.""" - code = attr.ib() - name = attr.ib() - remote_filename = attr.ib() - local_filename = attr.ib(default=None) + code: str + name: str + remote_filename: str + local_filename: Optional[str] = None - def __attrs_post_init__(self): + def __post_init__(self): if self.local_filename is None: self.local_filename = spell.local_filename(self.code) diff --git a/scripts/keytester.py b/scripts/keytester.py index 027dcbc59..7aa1fbe49 100644 --- a/scripts/keytester.py +++ b/scripts/keytester.py @@ -30,4 +30,4 @@ from qutebrowser.misc import miscwidgets app = QApplication([]) w = miscwidgets.KeyTesterWidget() w.show() -app.exec_() +app.exec() diff --git a/scripts/testbrowser/testbrowser_webengine.py b/scripts/testbrowser/testbrowser_webengine.py index 0a8dd0ac3..bfad90055 100755 --- a/scripts/testbrowser/testbrowser_webengine.py +++ b/scripts/testbrowser/testbrowser_webengine.py @@ -48,4 +48,4 @@ if __name__ == '__main__': wv.load(QUrl.fromUserInput(args.url)) wv.show() - app.exec_() + app.exec() diff --git a/scripts/testbrowser/testbrowser_webkit.py b/scripts/testbrowser/testbrowser_webkit.py index 39e2b991d..44112921a 100755 --- a/scripts/testbrowser/testbrowser_webkit.py +++ b/scripts/testbrowser/testbrowser_webkit.py @@ -53,4 +53,4 @@ if __name__ == '__main__': wv.load(QUrl.fromUserInput(args.url)) wv.show() - app.exec_() + app.exec() @@ -71,7 +71,8 @@ try: entry_points={'gui_scripts': ['qutebrowser = qutebrowser.qutebrowser:main']}, zip_safe=True, - install_requires=['pypeg2', 'jinja2', 'pygments', 'PyYAML', 'attrs', + install_requires=['pypeg2', 'jinja2', 'PyYAML', + 'dataclasses; python_version < "3.7"', 'importlib_resources>=1.1.0; python_version < "3.9"'], python_requires='>=3.6', name='qutebrowser', diff --git a/tests/conftest.py b/tests/conftest.py index fd317d6c4..524325595 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -49,11 +49,20 @@ _qute_scheme_handler = None # Set hypothesis settings hypothesis.settings.register_profile( - 'default', hypothesis.settings(deadline=600)) + 'default', hypothesis.settings( + deadline=600, + suppress_health_check=[hypothesis.HealthCheck.function_scoped_fixture], + ) +) hypothesis.settings.register_profile( 'ci', hypothesis.settings( deadline=None, - suppress_health_check=[hypothesis.HealthCheck.too_slow])) + suppress_health_check=[ + hypothesis.HealthCheck.function_scoped_fixture, + hypothesis.HealthCheck.too_slow, + ] + ) +) hypothesis.settings.load_profile('ci' if testutils.ON_CI else 'default') diff --git a/tests/end2end/fixtures/testprocess.py b/tests/end2end/fixtures/testprocess.py index 7a70e4de9..50679a255 100644 --- a/tests/end2end/fixtures/testprocess.py +++ b/tests/end2end/fixtures/testprocess.py @@ -22,8 +22,8 @@ import re import time import warnings +import dataclasses -import attr import pytest import pytestqt.plugin from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QProcess, QObject, @@ -55,7 +55,7 @@ class BlacklistedMessageError(Exception): """Raised when ensure_not_logged found a message.""" -@attr.s +@dataclasses.dataclass class Line: """Container for a line of data the process emits. @@ -65,8 +65,8 @@ class Line: waited_for: If Process.wait_for was used on this line already. """ - data = attr.ib() - waited_for = attr.ib(False) + data: str + waited_for: bool = False def _render_log(data, *, verbose, threshold=100): diff --git a/tests/end2end/fixtures/webserver.py b/tests/end2end/fixtures/webserver.py index d40739724..e368d050c 100644 --- a/tests/end2end/fixtures/webserver.py +++ b/tests/end2end/fixtures/webserver.py @@ -24,9 +24,9 @@ import sys import json import os.path import socket +import dataclasses from http import HTTPStatus -import attr import pytest from PyQt5.QtCore import pyqtSignal, QUrl @@ -100,13 +100,13 @@ class Request(testprocess.Line): return NotImplemented -@attr.s(frozen=True, eq=False, hash=True) +@dataclasses.dataclass(frozen=True) class ExpectedRequest: """Class to compare expected requests easily.""" - verb = attr.ib() - path = attr.ib() + verb: str + path: int @classmethod def from_request(cls, request): diff --git a/tests/end2end/test_dirbrowser.py b/tests/end2end/test_dirbrowser.py index 7b13c678e..8a91fe494 100644 --- a/tests/end2end/test_dirbrowser.py +++ b/tests/end2end/test_dirbrowser.py @@ -21,8 +21,9 @@ """Test the built-in directory browser.""" import os +import dataclasses +from typing import List -import attr import pytest import bs4 @@ -104,21 +105,21 @@ class DirLayout: return os.path.normpath(str(self.base)) -@attr.s +@dataclasses.dataclass class Parsed: - path = attr.ib() - parent = attr.ib() - folders = attr.ib() - files = attr.ib() + path: str + parent: str + folders: List[str] + files: List[str] -@attr.s +@dataclasses.dataclass class Item: - path = attr.ib() - link = attr.ib() - text = attr.ib() + path: str + link: str + text: str def parse(quteproc): diff --git a/tests/end2end/test_hints_html.py b/tests/end2end/test_hints_html.py index 6d3bed175..14868590c 100644 --- a/tests/end2end/test_hints_html.py +++ b/tests/end2end/test_hints_html.py @@ -22,8 +22,9 @@ import os import os.path import textwrap +import dataclasses +from typing import Optional -import attr import pytest import bs4 @@ -37,11 +38,11 @@ def collect_tests(): return files -@attr.s +@dataclasses.dataclass class ParsedFile: - target = attr.ib() - qtwebengine_todo = attr.ib() + target: str + qtwebengine_todo: Optional[str] class InvalidFile(Exception): diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py index 1d2f79a1a..a55f31b52 100644 --- a/tests/end2end/test_invocations.py +++ b/tests/end2end/test_invocations.py @@ -325,7 +325,7 @@ def test_launching_with_old_python(python): except FileNotFoundError: pytest.skip(f"{python} not found") assert proc.returncode == 1 - error = "At least Python 3.6 is required to run qutebrowser" + error = "At least Python 3.6.1 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 b814a6ea7..2d853df08 100644 --- a/tests/helpers/fixtures.py +++ b/tests/helpers/fixtures.py @@ -33,8 +33,8 @@ import unittest.mock import types import mimetypes import os.path +import dataclasses -import attr import pytest import py.path # pylint: disable=no-name-in-module from PyQt5.QtCore import QSize, Qt @@ -87,12 +87,12 @@ class WinRegistryHelper: """Helper class for win_registry.""" - @attr.s + @dataclasses.dataclass class FakeWindow: """A fake window object for the registry.""" - registry = attr.ib() + registry: objreg.ObjectRegistry def windowTitle(self): return 'window title - qutebrowser' @@ -276,11 +276,11 @@ def web_tab(request): def _generate_cmdline_tests(): """Generate testcases for test_split_binding.""" - @attr.s + @dataclasses.dataclass class TestCase: - cmd = attr.ib() - valid = attr.ib() + cmd: str + valid: bool separators = [';;', ' ;; ', ';; ', ' ;;'] invalid = ['foo', ''] diff --git a/tests/helpers/messagemock.py b/tests/helpers/messagemock.py index 03320a98f..8eae9129c 100644 --- a/tests/helpers/messagemock.py +++ b/tests/helpers/messagemock.py @@ -20,20 +20,20 @@ """pytest helper to monkeypatch the message module.""" import logging +import dataclasses -import attr import pytest from qutebrowser.utils import usertypes, message -@attr.s +@dataclasses.dataclass class Message: """Information about a shown message.""" - level = attr.ib() - text = attr.ib() + level: usertypes.MessageLevel + text: str class MessageMock: diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index a23d8fd3b..b8dc92540 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -21,11 +21,12 @@ """Fake objects/stubs.""" +from typing import Any, Callable, Tuple from unittest import mock import contextlib import shutil +import dataclasses -import attr from PyQt5.QtCore import pyqtSignal, QPoint, QProcess, QObject, QUrl from PyQt5.QtGui import QIcon from PyQt5.QtNetwork import (QNetworkRequest, QAbstractNetworkCache, @@ -116,13 +117,7 @@ class FakeQApplication: UNSET = object() def __init__(self, *, style=None, all_widgets=None, active_window=None, - instance=UNSET, arguments=None, platform_name=None): - - if instance is self.UNSET: - self.instance = mock.Mock(return_value=self) - else: - self.instance = mock.Mock(return_value=instance) - + arguments=None, platform_name=None): self.style = mock.Mock(spec=QCommonStyle) self.style().metaObject().className.return_value = style @@ -330,20 +325,20 @@ class FakeSignal: """ -@attr.s(frozen=True) +@dataclasses.dataclass(frozen=True) class FakeCommand: """A simple command stub which has a description.""" - name = attr.ib('') - desc = attr.ib('') - hide = attr.ib(False) - debug = attr.ib(False) - deprecated = attr.ib(False) - completion = attr.ib(None) - maxsplit = attr.ib(None) - takes_count = attr.ib(lambda: False) - modes = attr.ib((usertypes.KeyMode.normal, )) + name: str = '' + desc: str = '' + hide: bool = False + debug: bool = False + deprecated: bool = False + completion: Any = None + maxsplit: int = None + takes_count: Callable[[], bool] = lambda: False + modes: Tuple[usertypes.KeyMode] = (usertypes.KeyMode.normal, ) class FakeTimer(QObject): diff --git a/tests/unit/browser/test_signalfilter.py b/tests/unit/browser/test_signalfilter.py index 0e45026be..9b2cf881a 100644 --- a/tests/unit/browser/test_signalfilter.py +++ b/tests/unit/browser/test_signalfilter.py @@ -20,8 +20,8 @@ """Tests for browser.signalfilter.""" import logging +import dataclasses -import attr import pytest from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject @@ -46,11 +46,11 @@ class Signaller(QObject): self.filtered_signal_arg = s -@attr.s +@dataclasses.dataclass class Objects: - signal_filter = attr.ib() - signaller = attr.ib() + signal_filter: signalfilter.SignalFilter + signaller: Signaller @pytest.fixture diff --git a/tests/unit/browser/webkit/network/test_filescheme.py b/tests/unit/browser/webkit/network/test_filescheme.py index adcd1a7e5..5136fcf45 100644 --- a/tests/unit/browser/webkit/network/test_filescheme.py +++ b/tests/unit/browser/webkit/network/test_filescheme.py @@ -19,8 +19,9 @@ # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. import os +import dataclasses +from typing import List -import attr import pytest import bs4 from PyQt5.QtCore import QUrl @@ -111,18 +112,18 @@ def _file_url(path): class TestDirbrowserHtml: - @attr.s + @dataclasses.dataclass class Parsed: - parent = attr.ib() - folders = attr.ib() - files = attr.ib() + parent: str + folders: List[str] + files: List[str] - @attr.s + @dataclasses.dataclass class Item: - link = attr.ib() - text = attr.ib() + link: str + text: str @pytest.fixture def parser(self): diff --git a/tests/unit/browser/webkit/test_tabhistory.py b/tests/unit/browser/webkit/test_tabhistory.py index 48e0c98fc..379ef5afe 100644 --- a/tests/unit/browser/webkit/test_tabhistory.py +++ b/tests/unit/browser/webkit/test_tabhistory.py @@ -19,11 +19,15 @@ """Tests for webelement.tabhistory.""" -import attr -from PyQt5.QtCore import QUrl, QPoint +import dataclasses +from typing import Any + import pytest +pytest.importorskip('PyQt5.QtWebKit') +from PyQt5.QtCore import QUrl, QPoint +from PyQt5.QtWebKit import QWebHistory -tabhistory = pytest.importorskip('qutebrowser.browser.webkit.tabhistory') +from qutebrowser.browser.webkit import tabhistory from qutebrowser.misc.sessions import TabHistoryItem as Item from qutebrowser.utils import qtutils @@ -50,11 +54,11 @@ ITEMS = [ ] -@attr.s +@dataclasses.dataclass class Objects: - history = attr.ib() - user_data = attr.ib() + history: QWebHistory + user_data: Any @pytest.fixture diff --git a/tests/unit/browser/webkit/test_webkitelem.py b/tests/unit/browser/webkit/test_webkitelem.py index 37262a7b3..9f8caab55 100644 --- a/tests/unit/browser/webkit/test_webkitelem.py +++ b/tests/unit/browser/webkit/test_webkitelem.py @@ -19,12 +19,13 @@ """Tests for the webelement utils.""" +from typing import TYPE_CHECKING from unittest import mock import collections.abc import operator import itertools +import dataclasses -import attr import pytest from PyQt5.QtCore import QRect, QPoint, QUrl QWebElement = pytest.importorskip('PyQt5.QtWebKit').QWebElement @@ -33,6 +34,8 @@ from qutebrowser.browser import browsertab from qutebrowser.browser.webkit import webkitelem from qutebrowser.misc import objects from qutebrowser.utils import usertypes +if TYPE_CHECKING: + from helpers import stubs def get_webelem(geometry=None, frame=None, *, null=False, style=None, @@ -527,12 +530,12 @@ class TestIsVisibleIframe: elem1-elem4: FakeWebElements to test. """ - @attr.s + @dataclasses.dataclass class Objects: - frame = attr.ib() - iframe = attr.ib() - elems = attr.ib() + frame: 'stubs.FakeWebFrame' + iframe: 'stubs.FakeWebFrame' + elems: webkitelem.WebKitElement @pytest.fixture def objects(self, stubs): diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index 710018604..b1418c73a 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -25,8 +25,8 @@ import math import warnings import inspect import functools +import dataclasses -import attr import pytest import hypothesis from hypothesis import strategies @@ -1350,14 +1350,14 @@ class TestQssColor: klass().to_py(val) -@attr.s +@dataclasses.dataclass class FontDesc: - style = attr.ib() - weight = attr.ib() - pt = attr.ib() - px = attr.ib() - family = attr.ib() + style: QFont.Style + weight: QFont.Weight + pt: int + px: int + family: str class TestFont: diff --git a/tests/unit/keyinput/key_data.py b/tests/unit/keyinput/key_data.py index 7675ee6dd..1a7dfdd99 100644 --- a/tests/unit/keyinput/key_data.py +++ b/tests/unit/keyinput/key_data.py @@ -22,12 +22,13 @@ """Data used by test_keyutils.py to test all keys.""" +import dataclasses +from typing import Optional -import attr from PyQt5.QtCore import Qt -@attr.s +@dataclasses.dataclass(order=True) class Key: """A key with expected values. @@ -40,21 +41,21 @@ class Key: member: The numeric value. """ - attribute = attr.ib() - name = attr.ib(None) - text = attr.ib('') - uppertext = attr.ib('') - member = attr.ib(None) - qtest = attr.ib(True) + attribute: str + name: Optional[str] = None + text: str = '' + uppertext: str = '' + member: Optional[int] = None + qtest: bool = True - def __attrs_post_init__(self): + def __post_init__(self): if self.attribute: self.member = getattr(Qt, 'Key_' + self.attribute, None) if self.name is None: self.name = self.attribute -@attr.s +@dataclasses.dataclass(order=True) class Modifier: """A modifier with expected values. @@ -66,11 +67,11 @@ class Modifier: member: The numeric value. """ - attribute = attr.ib() - name = attr.ib(None) - member = attr.ib(None) + attribute: str + name: Optional[str] = None + member: Optional[int] = None - def __attrs_post_init__(self): + def __post_init__(self): self.member = getattr(Qt, self.attribute + 'Modifier') if self.name is None: self.name = self.attribute diff --git a/tests/unit/keyinput/test_modeman.py b/tests/unit/keyinput/test_modeman.py index b473294f8..037b0098b 100644 --- a/tests/unit/keyinput/test_modeman.py +++ b/tests/unit/keyinput/test_modeman.py @@ -23,6 +23,7 @@ from PyQt5.QtCore import Qt, QObject, pyqtSignal from qutebrowser.utils import usertypes from qutebrowser.keyinput import keyutils +from qutebrowser.misc import objects class FakeKeyparser(QObject): @@ -46,6 +47,11 @@ def modeman(mode_manager): return mode_manager +@pytest.fixture(autouse=True) +def set_qapp(monkeypatch, qapp): + monkeypatch.setattr(objects, 'qapp', qapp) + + @pytest.mark.parametrize('key, modifiers, filtered', [ (Qt.Key_A, Qt.NoModifier, True), (Qt.Key_Up, Qt.NoModifier, False), diff --git a/tests/unit/misc/test_checkpyver.py b/tests/unit/misc/test_checkpyver.py index 2d4da12e8..86b5912c5 100644 --- a/tests/unit/misc/test_checkpyver.py +++ b/tests/unit/misc/test_checkpyver.py @@ -28,7 +28,7 @@ import pytest from qutebrowser.misc import checkpyver -TEXT = (r"At least Python 3.6 is required to run qutebrowser, but it's " +TEXT = (r"At least Python 3.6.1 is required to run qutebrowser, but it's " r"running with \d+\.\d+\.\d+.") diff --git a/tests/unit/misc/test_ipc.py b/tests/unit/misc/test_ipc.py index 3f53ca238..6f77b7ec1 100644 --- a/tests/unit/misc/test_ipc.py +++ b/tests/unit/misc/test_ipc.py @@ -25,9 +25,10 @@ import getpass import logging import json import hashlib +import dataclasses from unittest import mock +from typing import Optional, List -import attr import pytest from PyQt5.QtCore import pyqtSignal, QObject from PyQt5.QtNetwork import QLocalServer, QLocalSocket, QAbstractSocket @@ -616,13 +617,13 @@ def test_ipcserver_socket_none_error(ipc_server, caplog): class TestSendOrListen: - @attr.s + @dataclasses.dataclass class Args: - no_err_windows = attr.ib() - basedir = attr.ib() - command = attr.ib() - target = attr.ib() + no_err_windows: bool + basedir: str + command: List[str] + target: Optional[str] @pytest.fixture def args(self): diff --git a/tests/unit/misc/test_msgbox.py b/tests/unit/misc/test_msgbox.py index 74422ea45..e2ea48fff 100644 --- a/tests/unit/misc/test_msgbox.py +++ b/tests/unit/misc/test_msgbox.py @@ -95,7 +95,7 @@ def test_information(qtbot): def test_no_err_windows(fake_args, capsys): fake_args.no_err_windows = True box = msgbox.information(parent=None, title='foo', text='bar') - box.exec_() # should do nothing + box.exec() # should do nothing out, err = capsys.readouterr() assert not out assert err == 'Message box: foo; bar\n' diff --git a/tests/unit/misc/test_split.py b/tests/unit/misc/test_split.py index 77dde75bc..e6bc3cc0c 100644 --- a/tests/unit/misc/test_split.py +++ b/tests/unit/misc/test_split.py @@ -19,7 +19,9 @@ """Tests for qutebrowser.misc.split.""" -import attr +import dataclasses +from typing import List + import pytest from qutebrowser.misc import split @@ -109,12 +111,12 @@ def _parse_split_test_data_str(): Returns: A list of TestCase objects with str attributes: inp, keep, no_keep """ - @attr.s + @dataclasses.dataclass class TestCase: - inp = attr.ib() - keep = attr.ib() - no_keep = attr.ib() + inp: str + keep: List[str] + no_keep: List[str] for line in test_data_str.splitlines(): if not line: diff --git a/tests/unit/misc/userscripts/test_qute_lastpass.py b/tests/unit/misc/userscripts/test_qute_lastpass.py index 229fcf09e..14f7e8e4a 100644 --- a/tests/unit/misc/userscripts/test_qute_lastpass.py +++ b/tests/unit/misc/userscripts/test_qute_lastpass.py @@ -20,10 +20,10 @@ """Tests for misc.userscripts.qute-lastpass.""" import json +import dataclasses from types import SimpleNamespace from unittest.mock import ANY, call -import attr import pytest from helpers import utils @@ -41,10 +41,16 @@ default_lpass_match = [ ] -@attr.s +@dataclasses.dataclass class FakeOutput: - stdout = attr.ib(default='', converter=str.encode) - stderr = attr.ib(default='', converter=str.encode) + + stdout: bytes = b'' + stderr: bytes = b'' + + @classmethod + def json(cls, obj): + """Get a FakeOutput for a json-encoded object.""" + return cls(stdout=json.dumps(obj).encode('ascii')) @pytest.fixture @@ -96,7 +102,7 @@ class TestQuteLastPassComponents: "2345 | example2.com | https://www.example2.com | jane.doe@example.com", ] - subprocess_mock.return_value = FakeOutput(stdout=entries[1]) + subprocess_mock.return_value = FakeOutput(stdout=entries[1].encode('ascii')) selected = qute_lastpass.dmenu(entries, 'rofi -dmenu', 'UTF-8') @@ -109,7 +115,7 @@ class TestQuteLastPassComponents: def test_pass_subprocess_args(self, subprocess_mock): """Test if pass_ calls subprocess with correct arguments.""" - subprocess_mock.return_value = FakeOutput(stdout='[{}]') + subprocess_mock.return_value = FakeOutput(stdout=b'[{}]') qute_lastpass.pass_('example.com', 'utf-8') @@ -119,8 +125,7 @@ class TestQuteLastPassComponents: def test_pass_returns_candidates(self, subprocess_mock): """Test if pass_ returns expected lpass site entry.""" - subprocess_mock.return_value = FakeOutput( - stdout=json.dumps(default_lpass_match)) + subprocess_mock.return_value = FakeOutput.json(default_lpass_match) response = qute_lastpass.pass_('www.example.com', 'utf-8') assert response[1] == '' @@ -132,7 +137,7 @@ class TestQuteLastPassComponents: def test_pass_no_accounts(self, subprocess_mock): """Test if pass_ handles no accounts as an empty lpass result.""" - error_message = 'Error: Could not find specified account(s).' + error_message = b'Error: Could not find specified account(s).' subprocess_mock.return_value = FakeOutput(stderr=error_message) response = qute_lastpass.pass_('www.example.com', 'utf-8') @@ -141,9 +146,9 @@ class TestQuteLastPassComponents: def test_pass_returns_error(self, subprocess_mock): """Test if pass_ returns error from lpass.""" - # pylint: disable=line-too-long - error_message = 'Error: Could not find decryption key. Perhaps you need to login with `lpass login`.' - subprocess_mock.return_value = FakeOutput(stderr=error_message) + error_message = ('Error: Could not find decryption key. ' + 'Perhaps you need to login with `lpass login`.') + subprocess_mock.return_value = FakeOutput(stderr=error_message.encode('ascii')) response = qute_lastpass.pass_('www.example.com', 'utf-8') assert response[0] == [] @@ -156,8 +161,7 @@ class TestQuteLastPassMain: def test_main_happy_path(self, subprocess_mock, arguments_mock, qutecommand_mock): """Test sending username/password to qutebrowser on *single* match.""" - subprocess_mock.return_value = FakeOutput( - stdout=json.dumps(default_lpass_match)) + subprocess_mock.return_value = FakeOutput.json(default_lpass_match) arguments_mock.url = default_lpass_match[0]['url'] exit_code = qute_lastpass.main(arguments_mock) @@ -175,7 +179,7 @@ class TestQuteLastPassMain: stderr_mock, qutecommand_mock): """Test correct exit code and message returned on no entries.""" - error_message = 'Error: Could not find specified account(s).' + error_message = b'Error: Could not find specified account(s).' subprocess_mock.return_value = FakeOutput(stderr=error_message) arguments_mock.url = default_lpass_match[0]['url'] @@ -190,8 +194,8 @@ class TestQuteLastPassMain: stderr_mock, qutecommand_mock): """Test correct exit code and message on lpass failure.""" - # pylint: disable=line-too-long - error_message = 'Error: Could not find decryption key. Perhaps you need to login with `lpass login`.' + error_message = (b'Error: Could not find decryption key. ' + b'Perhaps you need to login with `lpass login`.') subprocess_mock.return_value = FakeOutput(stderr=error_message) arguments_mock.url = default_lpass_match[0]['url'] @@ -206,8 +210,7 @@ class TestQuteLastPassMain: def test_main_username_only_flag(self, subprocess_mock, arguments_mock, qutecommand_mock): """Test if --username-only flag sends username only.""" - subprocess_mock.return_value = FakeOutput( - stdout=json.dumps(default_lpass_match)) + subprocess_mock.return_value = FakeOutput.json(default_lpass_match) arguments_mock.url = default_lpass_match[0]['url'] arguments_mock.username_only = True @@ -221,8 +224,7 @@ class TestQuteLastPassMain: def test_main_password_only_flag(self, subprocess_mock, arguments_mock, qutecommand_mock): """Test if --password-only flag sends password only.""" - subprocess_mock.return_value = FakeOutput( - stdout=json.dumps(default_lpass_match)) + subprocess_mock.return_value = FakeOutput.json(default_lpass_match) arguments_mock.url = default_lpass_match[0]['url'] arguments_mock.password_only = True @@ -247,9 +249,9 @@ class TestQuteLastPassMain: } ) - lpass_response = FakeOutput(stdout=json.dumps(multiple_matches)) + lpass_response = FakeOutput.json(multiple_matches) dmenu_response = FakeOutput( - stdout='23456 | Sites/www.example.com | https://www.example.com | john.doe@fake.com') + stdout=b'23456 | Sites/www.example.com | https://www.example.com | john.doe@fake.com') subprocess_mock.side_effect = [lpass_response, dmenu_response] @@ -305,12 +307,11 @@ class TestQuteLastPassMain: } ] - fqdn_response = FakeOutput(stdout=json.dumps(fqdn_matches)) - domain_response = FakeOutput(stdout=json.dumps(domain_matches)) - no_response = FakeOutput( - stderr='Error: Could not find specified account(s).') + fqdn_response = FakeOutput.json(fqdn_matches) + domain_response = FakeOutput.json(domain_matches) + no_response = FakeOutput(stderr=b'Error: Could not find specified account(s).') dmenu_response = FakeOutput( - stdout='23456 | Sites/www.example.com | https://www.example.com | john.doe@fake.com') + stdout=b'23456 | Sites/www.example.com | https://www.example.com | john.doe@fake.com') # lpass command will return results for search against # www.example.com, example.com, but not wwwexample.com and its ipv4 diff --git a/tests/unit/scripts/test_dictcli.py b/tests/unit/scripts/test_dictcli.py index 9add389d8..8ba0dc1cc 100644 --- a/tests/unit/scripts/test_dictcli.py +++ b/tests/unit/scripts/test_dictcli.py @@ -89,7 +89,8 @@ def test_available_languages(dict_tmp_path, monkeypatch): monkeypatch.setattr(dictcli, 'language_list_from_api', lambda: [ (lang.code, lang.remote_filename) for lang in langs() ]) - assert sorted(dictcli.available_languages()) == [ + languages = sorted(dictcli.available_languages(), key=lambda lang: lang.code) + assert languages == [ dictcli.Language( code='af-ZA', name='Afrikaans (South Africa)', diff --git a/tests/unit/utils/test_debug.py b/tests/unit/utils/test_debug.py index 68484e3c5..2424c2a0b 100644 --- a/tests/unit/utils/test_debug.py +++ b/tests/unit/utils/test_debug.py @@ -29,6 +29,7 @@ from PyQt5.QtCore import pyqtSignal, Qt, QEvent, QObject, QTimer from PyQt5.QtWidgets import QStyle, QFrame, QSpinBox from qutebrowser.utils import debug +from qutebrowser.misc import objects @debug.log_events @@ -271,7 +272,7 @@ class TestGetAllObjects: # pylint: disable=unused-variable widgets = [self.Object('Widget 1'), self.Object('Widget 2')] app = stubs.FakeQApplication(all_widgets=widgets) - monkeypatch.setattr(debug, 'QApplication', app) + monkeypatch.setattr(objects, 'qapp', app) root = QObject() o1 = self.Object('Object 1', root) @@ -293,9 +294,9 @@ class TestGetAllObjects: assert debug.get_all_objects(start_obj=root) == expected - @pytest.mark.usefixtures('qapp') - def test_get_all_objects_qapp(self): - objects = debug.get_all_objects() + def test_get_all_objects_qapp(self, qapp, monkeypatch): + monkeypatch.setattr(objects, 'qapp', qapp) + objs = debug.get_all_objects() event_dispatcher = '<PyQt5.QtCore.QAbstractEventDispatcher object at' session_manager = '<PyQt5.QtGui.QSessionManager object at' - assert event_dispatcher in objects or session_manager in objects + assert event_dispatcher in objs or session_manager in objs diff --git a/tests/unit/utils/test_javascript.py b/tests/unit/utils/test_javascript.py index fc8267435..9366c46fb 100644 --- a/tests/unit/utils/test_javascript.py +++ b/tests/unit/utils/test_javascript.py @@ -19,20 +19,21 @@ """Tests for qutebrowser.utils.javascript.""" +import dataclasses + import pytest import hypothesis import hypothesis.strategies -import attr from qutebrowser.utils import javascript, usertypes -@attr.s +@dataclasses.dataclass class Case: - original = attr.ib() - replacement = attr.ib() - webkit_only = attr.ib(False) + original: str + replacement: str + webkit_only: bool = False def __str__(self): return self.original diff --git a/tests/unit/utils/test_log.py b/tests/unit/utils/test_log.py index 8e8fa47a4..4b2db95d8 100644 --- a/tests/unit/utils/test_log.py +++ b/tests/unit/utils/test_log.py @@ -24,8 +24,8 @@ import argparse import itertools import sys import warnings +import dataclasses -import attr import pytest import _pytest.logging from PyQt5 import QtCore @@ -412,15 +412,15 @@ def test_warning_still_errors(): class TestQtMessageHandler: - @attr.s + @dataclasses.dataclass class Context: """Fake QMessageLogContext.""" - function = attr.ib(default=None) - category = attr.ib(default=None) - file = attr.ib(default=None) - line = attr.ib(default=None) + function: str = None + category: str = None + file: str = None + line: int = None @pytest.fixture(autouse=True) def init_args(self): diff --git a/tests/unit/utils/test_qtutils.py b/tests/unit/utils/test_qtutils.py index 2e54fb42e..eff531a46 100644 --- a/tests/unit/utils/test_qtutils.py +++ b/tests/unit/utils/test_qtutils.py @@ -23,10 +23,10 @@ import io import os import os.path +import dataclasses import unittest import unittest.mock -import attr import pytest from PyQt5.QtCore import (QDataStream, QPoint, QUrl, QByteArray, QIODevice, QTimer, QBuffer, QFile, QProcess, QFileDevice) @@ -122,7 +122,7 @@ def test_is_new_qtwebkit(monkeypatch, version, is_new): ]) def test_is_single_process(monkeypatch, stubs, backend, arguments, single_process): qapp = stubs.FakeQApplication(arguments=arguments) - monkeypatch.setattr(qtutils, 'QApplication', qapp) + monkeypatch.setattr(qtutils.objects, 'qapp', qapp) monkeypatch.setattr(qtutils.objects, 'backend', backend) assert qtutils.is_single_process() == single_process @@ -920,14 +920,14 @@ class TestEventLoop: def _double_exec(self): """Slot which gets called from timers to assert double-exec fails.""" with pytest.raises(AssertionError): - self.loop.exec_() + self.loop.exec() def test_normal_exec(self): """Test exec_ without double-executing.""" self.loop = qtutils.EventLoop() QTimer.singleShot(100, self._assert_executing) QTimer.singleShot(200, self.loop.quit) - self.loop.exec_() + self.loop.exec() assert not self.loop._executing def test_double_exec(self): @@ -937,7 +937,7 @@ class TestEventLoop: QTimer.singleShot(200, self._double_exec) QTimer.singleShot(300, self._assert_executing) QTimer.singleShot(400, self.loop.quit) - self.loop.exec_() + self.loop.exec() assert not self.loop._executing @@ -953,11 +953,11 @@ class Color(QColor): class TestInterpolateColor: - @attr.s + @dataclasses.dataclass class Colors: - white = attr.ib() - black = attr.ib() + white: Color + black: Color @pytest.fixture def colors(self): diff --git a/tests/unit/utils/test_urlutils.py b/tests/unit/utils/test_urlutils.py index 13c94d00e..19815b423 100644 --- a/tests/unit/utils/test_urlutils.py +++ b/tests/unit/utils/test_urlutils.py @@ -21,9 +21,9 @@ import os.path import logging +import dataclasses import urllib.parse -import attr from PyQt5.QtCore import QUrl from PyQt5.QtNetwork import QNetworkProxy import pytest @@ -50,10 +50,10 @@ class FakeDNS: when fromname_mock is called. """ - @attr.s + @dataclasses.dataclass class FakeDNSAnswer: - error = attr.ib() + error: bool def __init__(self): self.used = False @@ -350,14 +350,14 @@ def test_get_search_url_invalid(url): urlutils._get_search_url(url) -@attr.s +@dataclasses.dataclass class UrlParams: - url = attr.ib() - is_url = attr.ib(True) - is_url_no_autosearch = attr.ib(True) - use_dns = attr.ib(True) - is_url_in_schemeless = attr.ib(False) + url: QUrl + is_url: bool = True + is_url_no_autosearch: bool = True + use_dns: bool = True + is_url_in_schemeless: bool = False @pytest.mark.parametrize('auto_search', diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py index e16bd2318..912be3bec 100644 --- a/tests/unit/utils/test_version.py +++ b/tests/unit/utils/test_version.py @@ -31,8 +31,8 @@ import importlib import logging import textwrap import datetime +import dataclasses -import attr import pytest import hypothesis import hypothesis.strategies @@ -714,7 +714,7 @@ class TestModuleVersions: ('pygments', True), ('yaml', True), ('adblock', True), - ('attr', True), + ('dataclasses', False), ]) def test_existing_attributes(self, name, has_version): """Check if all dependencies have an expected __version__ attribute. @@ -943,18 +943,18 @@ class TestChromiumVersion: assert version._chromium_version() == 'avoided' -@attr.s +@dataclasses.dataclass class VersionParams: - name = attr.ib() - git_commit = attr.ib(True) - frozen = attr.ib(False) - qapp = attr.ib(True) - with_webkit = attr.ib(True) - known_distribution = attr.ib(True) - ssl_support = attr.ib(True) - autoconfig_loaded = attr.ib(True) - config_py_loaded = attr.ib(True) + name: str + git_commit: bool = True + frozen: bool = False + qapp: bool = True + with_webkit: bool = True + known_distribution: bool = True + ssl_support: bool = True + autoconfig_loaded: bool = True + config_py_loaded: bool = True @pytest.mark.parametrize('params', [ @@ -989,10 +989,8 @@ def test_version_info(params, stubs, monkeypatch, config_stub): 'platform.architecture': lambda: ('ARCHITECTURE', ''), '_os_info': lambda: ['OS INFO 1', 'OS INFO 2'], '_path_info': lambda: {'PATH DESC': 'PATH NAME'}, - 'QApplication': (stubs.FakeQApplication(style='STYLE', - platform_name='PLATFORM') - if params.qapp else - stubs.FakeQApplication(instance=None)), + 'objects.qapp': (stubs.FakeQApplication(style='STYLE', platform_name='PLATFORM') + if params.qapp else None), 'QLibraryInfo.location': (lambda _loc: 'QT PATH'), 'sql.version': lambda: 'SQLITE VERSION', '_uptime': lambda: datetime.timedelta(hours=1, minutes=23, seconds=45), @@ -1214,6 +1212,8 @@ def test_pastebin_version_error(pbclient, caplog, message_mock, monkeypatch): def test_uptime(monkeypatch, qapp): """Test _uptime runs and check if microseconds are dropped.""" + monkeypatch.setattr(objects, 'qapp', qapp) + launch_time = datetime.datetime(1, 1, 1, 1, 1, 1, 1) monkeypatch.setattr(qapp, "launch_time", launch_time, raising=False) |