summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Bruhin <me@the-compiler.org>2021-01-14 11:47:04 +0100
committerFlorian Bruhin <me@the-compiler.org>2021-01-14 11:47:04 +0100
commit482cf9ae047b8a08ee7ad6a1ff64feff39fe6e3c (patch)
treebd9a1784d271b57bd3571f2839100306366eb94d
parent4d893e75a175d7f05c5627a1dca4e176ce1e70e6 (diff)
parente66968737d433ccc4a5664491e50fad38d5655c3 (diff)
downloadqutebrowser-482cf9ae047b8a08ee7ad6a1ff64feff39fe6e3c.tar.gz
qutebrowser-482cf9ae047b8a08ee7ad6a1ff64feff39fe6e3c.zip
Merge branch 'master' into history-cleanup
# Conflicts: # qutebrowser/app.py
-rw-r--r--.flake82
-rw-r--r--.github/workflows/ci.yml10
-rw-r--r--README.asciidoc12
-rw-r--r--doc/changelog.asciidoc10
-rw-r--r--doc/help/commands.asciidoc1
-rw-r--r--doc/install.asciidoc2
-rw-r--r--misc/requirements/requirements-check-manifest.txt2
-rw-r--r--misc/requirements/requirements-dev.txt4
-rw-r--r--misc/requirements/requirements-mypy.txt6
-rw-r--r--misc/requirements/requirements-qutebrowser.txt-raw12
-rw-r--r--misc/requirements/requirements-sphinx.txt2
-rw-r--r--misc/requirements/requirements-tests.txt6
-rw-r--r--misc/requirements/requirements-tox.txt4
-rwxr-xr-xmisc/userscripts/readability-js4
-rw-r--r--pytest.ini2
-rw-r--r--qutebrowser/app.py42
-rw-r--r--qutebrowser/browser/browsertab.py91
-rw-r--r--qutebrowser/browser/commands.py3
-rw-r--r--qutebrowser/browser/greasemonkey.py12
-rw-r--r--qutebrowser/browser/hints.py59
-rw-r--r--qutebrowser/browser/network/pac.py2
-rw-r--r--qutebrowser/browser/qtnetworkdownloads.py10
-rw-r--r--qutebrowser/browser/webengine/interceptor.py15
-rw-r--r--qutebrowser/browser/webengine/webenginesettings.py2
-rw-r--r--qutebrowser/browser/webengine/webenginetab.py9
-rw-r--r--qutebrowser/browser/webkit/mhtml.py14
-rw-r--r--qutebrowser/browser/webkit/network/networkmanager.py28
-rw-r--r--qutebrowser/browser/webkit/rfc6266.py9
-rw-r--r--qutebrowser/browser/webkit/webview.py2
-rw-r--r--qutebrowser/commands/command.py21
-rw-r--r--qutebrowser/commands/runners.py15
-rw-r--r--qutebrowser/completion/completer.py16
-rw-r--r--qutebrowser/components/misccommands.py3
-rw-r--r--qutebrowser/components/readlinecommands.py6
-rw-r--r--qutebrowser/config/configdata.py28
-rw-r--r--qutebrowser/config/configexc.py10
-rw-r--r--qutebrowser/config/configinit.py2
-rw-r--r--qutebrowser/config/configtypes.py12
-rw-r--r--qutebrowser/config/websettings.py14
-rw-r--r--qutebrowser/extensions/interceptors.py13
-rw-r--r--qutebrowser/extensions/loader.py26
-rw-r--r--qutebrowser/html/pre.html3
-rw-r--r--qutebrowser/javascript/quirks/globalthis.user.js (renamed from qutebrowser/javascript/globalthis_quirk.user.js)0
-rw-r--r--qutebrowser/javascript/quirks/object_fromentries.user.js (renamed from qutebrowser/javascript/object_fromentries_quirk.user.js)0
-rw-r--r--qutebrowser/javascript/quirks/whatsapp_web.user.js (renamed from qutebrowser/javascript/whatsapp_web_quirk.user.js)0
-rw-r--r--qutebrowser/keyinput/basekeyparser.py12
-rw-r--r--qutebrowser/keyinput/eventfilter.py11
-rw-r--r--qutebrowser/keyinput/keyutils.py8
-rw-r--r--qutebrowser/keyinput/modeman.py12
-rw-r--r--qutebrowser/mainwindow/mainwindow.py8
-rw-r--r--qutebrowser/mainwindow/prompt.py14
-rw-r--r--qutebrowser/mainwindow/statusbar/bar.py17
-rw-r--r--qutebrowser/mainwindow/tabbedbrowser.py21
-rw-r--r--qutebrowser/mainwindow/tabwidget.py10
-rw-r--r--qutebrowser/mainwindow/windowundo.py20
-rw-r--r--qutebrowser/misc/backendproblem.py60
-rw-r--r--qutebrowser/misc/checkpyver.py4
-rw-r--r--qutebrowser/misc/crashdialog.py7
-rw-r--r--qutebrowser/misc/crashsignal.py14
-rw-r--r--qutebrowser/misc/earlyinit.py7
-rw-r--r--qutebrowser/misc/msgbox.py2
-rw-r--r--qutebrowser/misc/objects.py2
-rw-r--r--qutebrowser/misc/quitter.py8
-rw-r--r--qutebrowser/misc/sessions.py4
-rw-r--r--qutebrowser/misc/sql.py2
-rw-r--r--qutebrowser/misc/throttle.py8
-rw-r--r--qutebrowser/utils/debug.py6
-rw-r--r--qutebrowser/utils/error.py2
-rw-r--r--qutebrowser/utils/log.py10
-rw-r--r--qutebrowser/utils/qtutils.py9
-rw-r--r--qutebrowser/utils/usertypes.py12
-rw-r--r--qutebrowser/utils/version.py36
-rw-r--r--requirements.txt3
-rw-r--r--scripts/dev/check_coverage.py11
-rw-r--r--scripts/dev/get_coredumpctl_traces.py19
-rw-r--r--scripts/dev/misc_checks.py16
-rw-r--r--scripts/dev/recompile_requirements.py1
-rwxr-xr-xscripts/dictcli.py16
-rw-r--r--scripts/keytester.py2
-rwxr-xr-xscripts/testbrowser/testbrowser_webengine.py2
-rwxr-xr-xscripts/testbrowser/testbrowser_webkit.py2
-rwxr-xr-xsetup.py3
-rw-r--r--tests/conftest.py13
-rw-r--r--tests/end2end/fixtures/testprocess.py8
-rw-r--r--tests/end2end/fixtures/webserver.py8
-rw-r--r--tests/end2end/test_dirbrowser.py21
-rw-r--r--tests/end2end/test_hints_html.py9
-rw-r--r--tests/end2end/test_invocations.py2
-rw-r--r--tests/helpers/fixtures.py12
-rw-r--r--tests/helpers/messagemock.py8
-rw-r--r--tests/helpers/stubs.py31
-rw-r--r--tests/unit/browser/test_signalfilter.py8
-rw-r--r--tests/unit/browser/webkit/network/test_filescheme.py17
-rw-r--r--tests/unit/browser/webkit/test_tabhistory.py16
-rw-r--r--tests/unit/browser/webkit/test_webkitelem.py13
-rw-r--r--tests/unit/config/test_configtypes.py14
-rw-r--r--tests/unit/keyinput/key_data.py29
-rw-r--r--tests/unit/keyinput/test_modeman.py6
-rw-r--r--tests/unit/misc/test_checkpyver.py2
-rw-r--r--tests/unit/misc/test_ipc.py13
-rw-r--r--tests/unit/misc/test_msgbox.py2
-rw-r--r--tests/unit/misc/test_split.py12
-rw-r--r--tests/unit/misc/userscripts/test_qute_lastpass.py57
-rw-r--r--tests/unit/scripts/test_dictcli.py3
-rw-r--r--tests/unit/utils/test_debug.py11
-rw-r--r--tests/unit/utils/test_javascript.py11
-rw-r--r--tests/unit/utils/test_log.py12
-rw-r--r--tests/unit/utils/test_qtutils.py16
-rw-r--r--tests/unit/utils/test_urlutils.py18
-rw-r--r--tests/unit/utils/test_version.py32
110 files changed, 722 insertions, 629 deletions
diff --git a/.flake8 b/.flake8
index 7709eacaf..9331484c3 100644
--- a/.flake8
+++ b/.flake8
@@ -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()
diff --git a/setup.py b/setup.py
index 137b514ca..825daf64d 100755
--- a/setup.py
+++ b/setup.py
@@ -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)