Merge branch 'master' into focus-stack
This commit is contained in:
commit
44942e5320
|
|
@ -43,8 +43,9 @@ matrix:
|
|||
packages:
|
||||
- libxkbcommon-x11-0
|
||||
|
||||
### PyQt 5.13 (Python 3.7, with coverage)
|
||||
- env: TESTENV=py37-pyqt513-cov
|
||||
### PyQt 5.13 (Python 3.8, with coverage)
|
||||
- env: TESTENV=py38-pyqt513-cov
|
||||
python: 3.8-dev
|
||||
# http://code.qt.io/cgit/qt/qtbase.git/commit/?id=c3a963da1f9e7b1d37e63eedded61da4fbdaaf9a
|
||||
addons:
|
||||
apt:
|
||||
|
|
|
|||
|
|
@ -143,7 +143,13 @@ If you want to give me a beer or a pizza back, I'm trying to make it as easy as
|
|||
possible for you to do so. If some other way would be easier for you, please
|
||||
get in touch!
|
||||
|
||||
* SEPA bank transfer inside Europe (no fee): Contact me at mail@qutebrowser.org for details
|
||||
* SEPA bank transfer inside Europe (no fee):
|
||||
- Account holder: Florian Bruhin
|
||||
- Country: Switzerland
|
||||
- IBAN (EUR): CH13 0900 0000 9160 4094 6
|
||||
- IBAN (other): CH80 0900 0000 8711 8587 3
|
||||
- Bank: PostFinance AG, Mingerstrasse 20, 3030 Bern, Switzerland (BIC: POFICHBEXXX)
|
||||
- If you need any other information: Contact me at mail@qutebrowser.org.
|
||||
* PayPal: https://www.paypal.me/thecompiler[thecompiler] / me@the-compiler.org
|
||||
* Bitcoin: link:bitcoin:1PMzbcetAHfpxoXww8Bj5XqquHtVvMjJtE[1PMzbcetAHfpxoXww8Bj5XqquHtVvMjJtE]
|
||||
|
||||
|
|
|
|||
|
|
@ -18,10 +18,32 @@ breaking changes (such as renamed commands) can happen in minor releases.
|
|||
v1.9.0 (unreleased)
|
||||
-------------------
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
- The `qute-pass` userscript now has a new `--extra-url-suffixes` (`-s`)
|
||||
argument which passes extra URL suffixes to the tldextract library.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- dictcli.py now works correctly on Windows again.
|
||||
- Unbinding keys via `config.bind(key, None)` accidentally worked in
|
||||
v1.7.0 but raises an exception in v1.8.0. It now works again, but is
|
||||
deprecated and shows an error. Note that `:config-py-write` did write
|
||||
such invalid lines before v1.8.0, so existing config files might need
|
||||
adjustments.
|
||||
- The `readability-js` userscript now handles encodings correctly (which it
|
||||
didn't before for some websites).
|
||||
- New `tabs.tooltips` setting which can be used to disable hover tooltips for
|
||||
tabs.
|
||||
- <Shift-Insert> can now be used to paste text starting with a hyphen.
|
||||
- Following hints via the number keypad now works properly again.
|
||||
- The logic for `:restart` has been revisited which should fix issues with
|
||||
relative basedirs.
|
||||
- Errors while reading the state file are now displayed instead of causing a
|
||||
crash.
|
||||
- Crash when using `:debug-log-level` without a console attached.
|
||||
|
||||
v1.8.1 (2019-09-27)
|
||||
-------------------
|
||||
|
|
|
|||
|
|
@ -183,17 +183,21 @@ tox -e py35-cov -- tests/unit/browser/test_webelem.py
|
|||
Profiling
|
||||
~~~~~~~~~
|
||||
|
||||
In the _scripts/_ subfolder there's a `run_profile.py` which profiles the code
|
||||
and shows a graphical representation of what takes how much time.
|
||||
In the _scripts/dev/_ subfolder there's `run_profile.py` which profiles the
|
||||
code and shows a graphical representation of what takes how much time.
|
||||
|
||||
It uses the built-in Python
|
||||
https://docs.python.org/3.6/library/profile.html[cProfile] module and can show
|
||||
the output in four different ways:
|
||||
https://docs.python.org/3/library/profile.html[cProfile] module. It launches a
|
||||
qutebrowser instance, waits for it to exit and then shows the graph.
|
||||
|
||||
* Raw profile file (`--profile-tool=none`)
|
||||
Available methods for visualization are:
|
||||
|
||||
* https://jiffyclub.github.io/snakeviz/[SnakeViz] (`--profile-tool=snakeviz`, the default)
|
||||
* https://pypi.python.org/pypi/pyprof2calltree/[pyprof2calltree] and http://kcachegrind.sourceforge.net/html/Home.html[KCacheGrind] (`--profile-tool=kcachegrind`)
|
||||
* https://jiffyclub.github.io/snakeviz/[SnakeViz] (`--profile-tool=snakeviz`)
|
||||
* https://github.com/jrfonseca/gprof2dot[gprof2dot] (needs `dot` from http://graphviz.org/[Graphviz] and http://feh.finalrewind.org/[feh])
|
||||
* https://github.com/jrfonseca/gprof2dot[gprof2dot] (`--profile-tool=gprof2dot`, needs `dot` from http://graphviz.org/[Graphviz] and http://feh.finalrewind.org/[feh])
|
||||
* https://github.com/nschloe/tuna[tuna] (`--profile-tool=tuna`)
|
||||
|
||||
You can also save the binary profile data to a file (`--profile-tool=none`).
|
||||
|
||||
Debugging
|
||||
~~~~~~~~~
|
||||
|
|
|
|||
|
|
@ -282,6 +282,7 @@
|
|||
|<<tabs.title.alignment,tabs.title.alignment>>|Alignment of the text inside of tabs.
|
||||
|<<tabs.title.format,tabs.title.format>>|Format to use for the tab title.
|
||||
|<<tabs.title.format_pinned,tabs.title.format_pinned>>|Format to use for the tab title for pinned tabs. The same placeholders like for `tabs.title.format` are defined.
|
||||
|<<tabs.tooltips,tabs.tooltips>>|Show tooltips on tabs.
|
||||
|<<tabs.undo_stack_size,tabs.undo_stack_size>>|Number of close tab actions to remember, per window (-1 for no maximum).
|
||||
|<<tabs.width,tabs.width>>|Width (in pixels or as percentage of the window) of the tab bar if it's vertical.
|
||||
|<<tabs.wrap,tabs.wrap>>|Wrap when changing tabs.
|
||||
|
|
@ -487,7 +488,7 @@ Default:
|
|||
|
||||
* +pass:[<Ctrl-E>]+: +pass:[open-editor]+
|
||||
* +pass:[<Escape>]+: +pass:[leave-mode]+
|
||||
* +pass:[<Shift-Ins>]+: +pass:[insert-text {primary}]+
|
||||
* +pass:[<Shift-Ins>]+: +pass:[insert-text -- {primary}]+
|
||||
- +pass:[normal]+:
|
||||
|
||||
* +pass:[']+: +pass:[enter-mode jump_mark]+
|
||||
|
|
@ -3538,6 +3539,15 @@ Type: <<types,FormatString>>
|
|||
|
||||
Default: +pass:[{index}]+
|
||||
|
||||
[[tabs.tooltips]]
|
||||
=== tabs.tooltips
|
||||
Show tooltips on tabs.
|
||||
Note this setting only affects windows opened after it has been set.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[true]+
|
||||
|
||||
[[tabs.undo_stack_size]]
|
||||
=== tabs.undo_stack_size
|
||||
Number of close tab actions to remember, per window (-1 for no maximum).
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
asn1crypto==0.24.0
|
||||
asn1crypto==1.1.0
|
||||
bump2version==0.5.11
|
||||
certifi==2019.9.11
|
||||
cffi==1.12.3
|
||||
|
|
@ -9,7 +9,7 @@ colorama==0.4.1
|
|||
cryptography==2.7
|
||||
cssutils==1.0.2
|
||||
github3.py==1.3.0
|
||||
hunter==3.0.1
|
||||
hunter==3.0.3
|
||||
idna==2.8
|
||||
jwcrypto==0.6.0
|
||||
lxml==4.4.1
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
attrs==19.1.0
|
||||
attrs==19.2.0
|
||||
entrypoints==0.3
|
||||
flake8==3.7.8
|
||||
flake8-bugbear==19.8.0
|
||||
flake8-builtins==1.4.1
|
||||
flake8-comprehensions==2.2.0
|
||||
flake8-copyright==0.2.2
|
||||
flake8-debugger==3.1.0
|
||||
flake8-debugger==3.1.1
|
||||
flake8-deprecated==1.3
|
||||
flake8-docstrings==1.5.0
|
||||
flake8-future-import==0.4.6
|
||||
|
|
@ -22,4 +22,4 @@ pycodestyle==2.5.0
|
|||
pydocstyle==4.0.1
|
||||
pyflakes==2.1.1
|
||||
six==1.12.0
|
||||
snowballstemmer==1.9.1
|
||||
snowballstemmer==2.0.0
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
mypy==0.730
|
||||
mypy-extensions==0.4.1
|
||||
mypy-extensions==0.4.2
|
||||
# PyQt5==5.11.3
|
||||
# PyQt5-sip==4.19.19
|
||||
-e git+https://github.com/qutebrowser/PyQt5-stubs.git@wip#egg=PyQt5_stubs
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
appdirs==1.4.3
|
||||
packaging==19.2
|
||||
pyparsing==2.4.2
|
||||
setuptools==41.2.0
|
||||
setuptools==41.4.0
|
||||
six==1.12.0
|
||||
wheel==0.33.6
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
asn1crypto==0.24.0
|
||||
asn1crypto==1.1.0
|
||||
astroid==2.3.1
|
||||
certifi==2019.9.11
|
||||
cffi==1.12.3
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
PyQt5==5.10.1 # rq.filter: < 5.11
|
||||
sip==4.19.8
|
||||
sip==4.19.8 # rq.filter: < 5
|
||||
|
|
|
|||
|
|
@ -1,2 +1,4 @@
|
|||
#@ filter: PyQt5 < 5.11
|
||||
PyQt5 >= 5.10, < 5.11
|
||||
#@ filter: sip < 5
|
||||
sip < 5
|
||||
|
|
|
|||
|
|
@ -2,3 +2,4 @@
|
|||
|
||||
PyQt5==5.11.3 # rq.filter: < 5.12
|
||||
PyQt5-sip==4.19.19
|
||||
sip==4.19.8 # rq.filter: < 5
|
||||
|
|
|
|||
|
|
@ -1,2 +1,4 @@
|
|||
#@ filter: PyQt5 < 5.12
|
||||
PyQt5 >= 5.11, < 5.12
|
||||
#@ filter: sip < 5
|
||||
sip < 5
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
PyQt5==5.12.3 # rq.filter: < 5.13
|
||||
PyQt5-sip==4.19.19
|
||||
PyQt5-sip==12.7.0
|
||||
PyQtWebEngine==5.12.1 # rq.filter: < 5.13
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
PyQt5==5.13.0 # rq.filter: < 5.14, != 5.13.1
|
||||
PyQt5-sip==4.19.19
|
||||
PyQt5-sip==12.7.0
|
||||
PyQtWebEngine==5.13.1 # rq.filter: < 5.14
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
PyQt5==5.7.1 # rq.filter: < 5.8
|
||||
sip==4.19.8
|
||||
sip==4.19.8 # rq.filter: < 5
|
||||
|
|
|
|||
|
|
@ -1,2 +1,4 @@
|
|||
#@ filter: PyQt5 < 5.8
|
||||
#@ filter: sip < 5
|
||||
PyQt5 >= 5.7, < 5.8
|
||||
sip < 5
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
PyQt5==5.9.2 # rq.filter: < 5.10
|
||||
sip==4.19.8
|
||||
sip==4.19.8 # rq.filter: < 5
|
||||
|
|
|
|||
|
|
@ -1,2 +1,4 @@
|
|||
#@ filter: PyQt5 < 5.10
|
||||
PyQt5 >= 5.9, < 5.10
|
||||
#@ filter: sip < 5
|
||||
sip < 5
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
PyQt5==5.13.0 # rq.filter: != 5.13.1
|
||||
PyQt5-sip==4.19.19
|
||||
PyQt5-sip==12.7.0
|
||||
PyQtWebEngine==5.13.1
|
||||
|
|
|
|||
|
|
@ -1,22 +1,21 @@
|
|||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
alabaster==0.7.12
|
||||
attrs==19.1.0
|
||||
Babel==2.7.0
|
||||
certifi==2019.9.11
|
||||
chardet==3.0.4
|
||||
docutils==0.15.2
|
||||
idna==2.8
|
||||
imagesize==1.1.0
|
||||
Jinja2==2.10.1
|
||||
Jinja2==2.10.3
|
||||
MarkupSafe==1.1.1
|
||||
packaging==19.2
|
||||
Pygments==2.4.2
|
||||
pyparsing==2.4.2
|
||||
pytz==2019.2
|
||||
pytz==2019.3
|
||||
requests==2.22.0
|
||||
six==1.12.0
|
||||
snowballstemmer==1.9.1
|
||||
snowballstemmer==2.0.0
|
||||
Sphinx==2.2.0
|
||||
sphinxcontrib-applehelp==1.0.1
|
||||
sphinxcontrib-devhelp==1.0.1
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
atomicwrites==1.3.0
|
||||
attrs==19.1.0
|
||||
backports.functools-lru-cache==1.5
|
||||
beautifulsoup4==4.8.0
|
||||
cheroot==7.0.0
|
||||
attrs==19.2.0
|
||||
beautifulsoup4==4.8.1
|
||||
cheroot==8.1.0
|
||||
Click==7.0
|
||||
# colorama==0.4.1
|
||||
coverage==4.5.4
|
||||
EasyProcess==0.2.7
|
||||
Flask==1.1.1
|
||||
glob2==0.7
|
||||
hunter==3.0.1
|
||||
hypothesis==4.37.0
|
||||
hunter==3.0.3
|
||||
hypothesis==4.40.0
|
||||
importlib-metadata==0.23
|
||||
itsdangerous==1.1.0
|
||||
# Jinja2==2.10.1
|
||||
jaraco.functools==2.0
|
||||
# Jinja2==2.10.3
|
||||
Mako==1.1.0
|
||||
manhole==1.6.0
|
||||
# MarkupSafe==1.1.1
|
||||
|
|
@ -27,12 +27,12 @@ pluggy==0.13.0
|
|||
py==1.8.0
|
||||
py-cpuinfo==5.0.0
|
||||
pyparsing==2.4.2
|
||||
pytest==5.2.0
|
||||
pytest==5.2.1
|
||||
pytest-bdd==3.2.1
|
||||
pytest-benchmark==3.2.2
|
||||
pytest-cov==2.7.1
|
||||
pytest-cov==2.8.1
|
||||
pytest-instafail==0.4.1
|
||||
pytest-mock==1.11.0
|
||||
pytest-mock==1.11.1
|
||||
pytest-qt==3.2.2
|
||||
pytest-repeat==0.8.0
|
||||
pytest-rerunfailures==7.0
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
attrs==19.1.0
|
||||
filelock==3.0.12
|
||||
importlib-metadata==0.23
|
||||
more-itertools==7.2.0
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@ def run(args):
|
|||
args.io_encoding)
|
||||
|
||||
if selection not in candidates_map:
|
||||
stderr("'{}' was not a valid entry!").format(selection)
|
||||
stderr("'{}' was not a valid entry!".format(selection))
|
||||
return ExitCodes.USER_QUIT
|
||||
|
||||
selection = candidates_map[selection]
|
||||
|
|
|
|||
|
|
@ -72,6 +72,8 @@ argument_parser.add_argument('--io-encoding', '-i', default='UTF-8',
|
|||
help='Encoding used to communicate with subprocesses')
|
||||
argument_parser.add_argument('--merge-candidates', '-m', action='store_true',
|
||||
help='Merge pass candidates for fully-qualified and registered domain name')
|
||||
argument_parser.add_argument('--extra-url-suffixes', '-s', default='',
|
||||
help='Comma-separated string containing extra suffixes (e.g local)')
|
||||
group = argument_parser.add_mutually_exclusive_group()
|
||||
group.add_argument('--username-only', '-e', action='store_true', help='Only insert username')
|
||||
group.add_argument('--password-only', '-w', action='store_true', help='Only insert password')
|
||||
|
|
@ -145,7 +147,8 @@ def main(arguments):
|
|||
argument_parser.print_help()
|
||||
return ExitCodes.FAILURE
|
||||
|
||||
extract_result = tldextract.extract(arguments.url)
|
||||
extractor = tldextract.TLDExtract(extra_suffixes=arguments.extra_url_suffixes.split(','))
|
||||
extract_result = extractor(arguments.url)
|
||||
|
||||
# Expand potential ~ in paths, since this script won't be called from a shell that does it for us
|
||||
password_store_path = os.path.expanduser(arguments.password_store)
|
||||
|
|
|
|||
|
|
@ -49,12 +49,13 @@ const HEADER = `
|
|||
</head>`;
|
||||
const scriptsDir = path.join(process.env.QUTE_DATA_DIR, 'userscripts');
|
||||
const tmpFile = path.join(scriptsDir, '/readability.html');
|
||||
const domOpts = {url: process.env.QUTE_URL, contentType: "text/html; charset=utf-8"}
|
||||
|
||||
if (!fs.existsSync(scriptsDir)){
|
||||
fs.mkdirSync(scriptsDir);
|
||||
}
|
||||
|
||||
JSDOM.fromFile(process.env.QUTE_HTML, { url: process.env.QUTE_URL }).then(dom => {
|
||||
JSDOM.fromFile(process.env.QUTE_HTML, domOpts).then(dom => {
|
||||
let reader = new Readability(dom.window.document);
|
||||
let article = reader.parse();
|
||||
let content = util.format(HEADER, article.title) + article.content;
|
||||
|
|
|
|||
32
mypy.ini
32
mypy.ini
|
|
@ -58,10 +58,26 @@ disallow_subclassing_any = False
|
|||
disallow_untyped_defs = True
|
||||
disallow_incomplete_defs = True
|
||||
|
||||
[mypy-qutebrowser.browser.hints]
|
||||
disallow_untyped_defs = True
|
||||
disallow_incomplete_defs = True
|
||||
|
||||
[mypy-qutebrowser.misc.objects]
|
||||
disallow_untyped_defs = True
|
||||
disallow_incomplete_defs = True
|
||||
|
||||
[mypy-qutebrowser.misc.debugcachestats]
|
||||
disallow_untyped_defs = True
|
||||
disallow_incomplete_defs = True
|
||||
|
||||
[mypy-qutebrowser.misc.utilcmds]
|
||||
disallow_untyped_defs = True
|
||||
disallow_incomplete_defs = True
|
||||
|
||||
[mypy-qutebrowser.misc.throttle]
|
||||
disallow_untyped_defs = True
|
||||
disallow_incomplete_defs = True
|
||||
|
||||
[mypy-qutebrowser.commands.cmdutils]
|
||||
disallow_untyped_defs = True
|
||||
disallow_incomplete_defs = True
|
||||
|
|
@ -93,3 +109,19 @@ disallow_incomplete_defs = True
|
|||
[mypy-qutebrowser.browser.webengine.webengineelem]
|
||||
disallow_untyped_defs = True
|
||||
disallow_incomplete_defs = True
|
||||
|
||||
[mypy-qutebrowser.keyinput.*]
|
||||
disallow_untyped_defs = True
|
||||
disallow_incomplete_defs = True
|
||||
|
||||
[mypy-qutebrowser.utils.*]
|
||||
disallow_untyped_defs = True
|
||||
disallow_incomplete_defs = True
|
||||
|
||||
[mypy-qutebrowser.misc.backendproblem]
|
||||
disallow_untyped_defs = True
|
||||
disallow_incomplete_defs = True
|
||||
|
||||
[mypy-qutebrowser.mainwindow.statusbar.command]
|
||||
disallow_untyped_defs = True
|
||||
disallow_incomplete_defs = True
|
||||
|
|
|
|||
|
|
@ -47,11 +47,13 @@ import tempfile
|
|||
import atexit
|
||||
import datetime
|
||||
import tokenize
|
||||
import argparse
|
||||
import typing
|
||||
|
||||
from PyQt5.QtWidgets import QApplication, QWidget
|
||||
from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon, QWindow
|
||||
from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QTimer, QUrl,
|
||||
QObject, QEvent, pyqtSignal, Qt)
|
||||
from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon, QWindow, QKeyEvent
|
||||
from PyQt5.QtCore import (pyqtSlot, QTimer, QUrl, QObject, QEvent, pyqtSignal,
|
||||
Qt)
|
||||
try:
|
||||
import hunter
|
||||
except ImportError:
|
||||
|
|
@ -83,7 +85,7 @@ from qutebrowser.misc import utilcmds
|
|||
# pylint: enable=unused-import
|
||||
|
||||
|
||||
q_app = None
|
||||
q_app = typing.cast(QApplication, None)
|
||||
|
||||
|
||||
def run(args):
|
||||
|
|
@ -91,8 +93,9 @@ def run(args):
|
|||
if args.temp_basedir:
|
||||
args.basedir = tempfile.mkdtemp(prefix='qutebrowser-basedir-')
|
||||
|
||||
quitter = Quitter(args)
|
||||
objreg.register('quitter', quitter)
|
||||
quitter = Quitter(args=args)
|
||||
objreg.register('quitter', quitter, command_only=True)
|
||||
quitter.shutting_down.connect(log.shutdown_log)
|
||||
|
||||
log.init.debug("Initializing directories...")
|
||||
standarddir.init(args)
|
||||
|
|
@ -115,13 +118,14 @@ def run(args):
|
|||
|
||||
crash_handler = crashsignal.CrashHandler(
|
||||
app=q_app, quitter=quitter, args=args, parent=q_app)
|
||||
objreg.register('crash-handler', crash_handler, command_only=True)
|
||||
crash_handler.activate()
|
||||
objreg.register('crash-handler', crash_handler)
|
||||
quitter.shutting_down.connect(crash_handler.shutdown)
|
||||
|
||||
signal_handler = crashsignal.SignalHandler(app=q_app, quitter=quitter,
|
||||
parent=q_app)
|
||||
signal_handler.activate()
|
||||
objreg.register('signal-handler', signal_handler)
|
||||
quitter.shutting_down.connect(signal_handler.deactivate)
|
||||
|
||||
try:
|
||||
server = ipc.send_or_listen(args)
|
||||
|
|
@ -136,11 +140,12 @@ def run(args):
|
|||
"Backend from the running instance will be used")
|
||||
sys.exit(usertypes.Exit.ok)
|
||||
else:
|
||||
quitter.shutting_down.connect(server.shutdown)
|
||||
server.got_args.connect(lambda args, target_arg, cwd:
|
||||
process_pos_args(args, cwd=cwd, via_ipc=True,
|
||||
target_arg=target_arg))
|
||||
|
||||
init(args, crash_handler)
|
||||
init(args=args, crash_handler=crash_handler, quitter=quitter)
|
||||
ret = qt_mainloop()
|
||||
return ret
|
||||
|
||||
|
|
@ -154,24 +159,23 @@ def qt_mainloop():
|
|||
return q_app.exec_()
|
||||
|
||||
|
||||
def init(args, crash_handler):
|
||||
"""Initialize everything.
|
||||
|
||||
Args:
|
||||
args: The argparse namespace.
|
||||
crash_handler: The CrashHandler instance.
|
||||
"""
|
||||
def init(*, args: argparse.Namespace,
|
||||
crash_handler: crashsignal.CrashHandler,
|
||||
quitter: 'Quitter'):
|
||||
"""Initialize everything."""
|
||||
log.init.debug("Starting init...")
|
||||
|
||||
crash_handler.init_faulthandler()
|
||||
|
||||
q_app.setQuitOnLastWindowClosed(False)
|
||||
quitter.shutting_down.connect(QApplication.closeAllWindows)
|
||||
|
||||
_init_icon()
|
||||
|
||||
loader.init()
|
||||
loader.load_components()
|
||||
try:
|
||||
_init_modules(args, crash_handler)
|
||||
_init_modules(args=args, crash_handler=crash_handler, quitter=quitter)
|
||||
except (OSError, UnicodeDecodeError, browsertab.WebTabError) as e:
|
||||
error.handle_fatal_exc(e, args, "Error while initializing!",
|
||||
pre_text="Error while initializing")
|
||||
|
|
@ -179,17 +183,17 @@ def init(args, crash_handler):
|
|||
|
||||
log.init.debug("Initializing eventfilter...")
|
||||
event_filter = EventFilter(q_app)
|
||||
q_app.installEventFilter(event_filter)
|
||||
objreg.register('event-filter', event_filter)
|
||||
event_filter.install()
|
||||
quitter.shutting_down.connect(event_filter.shutdown)
|
||||
|
||||
log.init.debug("Connecting signals...")
|
||||
q_app.focusChanged.connect(on_focus_changed)
|
||||
q_app.focusChanged.connect(on_focus_changed) # type: ignore
|
||||
|
||||
_process_args(args)
|
||||
|
||||
QDesktopServices.setUrlHandler('http', open_desktopservices_url)
|
||||
QDesktopServices.setUrlHandler('https', open_desktopservices_url)
|
||||
QDesktopServices.setUrlHandler('qute', open_desktopservices_url)
|
||||
for scheme in ['http', 'https', 'qute']:
|
||||
QDesktopServices.setUrlHandler(
|
||||
scheme, open_desktopservices_url) # type: ignore
|
||||
|
||||
log.init.debug("Init done!")
|
||||
crash_handler.raise_crashdlg()
|
||||
|
|
@ -217,8 +221,8 @@ def _process_args(args):
|
|||
"""Open startpage etc. and process commandline args."""
|
||||
if not args.override_restore:
|
||||
_load_session(args.session)
|
||||
session_manager = objreg.get('session-manager')
|
||||
if not session_manager.did_load:
|
||||
|
||||
if not sessions.session_manager.did_load:
|
||||
log.init.debug("Initializing main window...")
|
||||
if config.val.content.private_browsing and qtutils.is_single_process():
|
||||
err = Exception("Private windows are unavailable with "
|
||||
|
|
@ -244,8 +248,7 @@ def _load_session(name):
|
|||
Args:
|
||||
name: The name of the session to load, or None to read state file.
|
||||
"""
|
||||
session_manager = objreg.get('session-manager')
|
||||
if name is None and session_manager.exists('_autosave'):
|
||||
if name is None and sessions.session_manager.exists('_autosave'):
|
||||
name = '_autosave'
|
||||
elif name is None:
|
||||
try:
|
||||
|
|
@ -256,7 +259,7 @@ def _load_session(name):
|
|||
return
|
||||
|
||||
try:
|
||||
session_manager.load(name)
|
||||
sessions.session_manager.load(name)
|
||||
except sessions.SessionNotFoundError:
|
||||
message.error("Session {} not found!".format(name))
|
||||
except sessions.SessionError as e:
|
||||
|
|
@ -267,7 +270,7 @@ def _load_session(name):
|
|||
pass
|
||||
# If this was a _restart session, delete it.
|
||||
if name == '_restart':
|
||||
session_manager.delete('_restart')
|
||||
sessions.session_manager.delete('_restart')
|
||||
|
||||
|
||||
def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None):
|
||||
|
|
@ -425,20 +428,22 @@ def open_desktopservices_url(url):
|
|||
tabbed_browser.tabopen(url)
|
||||
|
||||
|
||||
def _init_modules(args, crash_handler):
|
||||
def _init_modules(*, args, crash_handler, quitter):
|
||||
"""Initialize all 'modules' which need to be initialized.
|
||||
|
||||
Args:
|
||||
args: The argparse namespace.
|
||||
crash_handler: The CrashHandler instance.
|
||||
quitter: The Quitter instance.
|
||||
"""
|
||||
log.init.debug("Initializing save manager...")
|
||||
save_manager = savemanager.SaveManager(q_app)
|
||||
objreg.register('save-manager', save_manager)
|
||||
quitter.shutting_down.connect(save_manager.shutdown)
|
||||
configinit.late_init(save_manager)
|
||||
|
||||
log.init.debug("Checking backend requirements...")
|
||||
backendproblem.init()
|
||||
backendproblem.init(quitter=quitter, args=args, save_manager=save_manager)
|
||||
|
||||
log.init.debug("Initializing prompts...")
|
||||
prompt.init()
|
||||
|
|
@ -448,13 +453,15 @@ def _init_modules(args, crash_handler):
|
|||
|
||||
log.init.debug("Initializing proxy...")
|
||||
proxy.init()
|
||||
quitter.shutting_down.connect(proxy.shutdown)
|
||||
|
||||
log.init.debug("Initializing downloads...")
|
||||
downloads.init()
|
||||
quitter.shutting_down.connect(downloads.shutdown)
|
||||
|
||||
log.init.debug("Initializing readline-bridge...")
|
||||
readline_bridge = readline.ReadlineBridge()
|
||||
objreg.register('readline-bridge', readline_bridge)
|
||||
objreg.register('readline-bridge', readline_bridge, command_only=True)
|
||||
|
||||
try:
|
||||
log.init.debug("Initializing SQL...")
|
||||
|
|
@ -471,9 +478,11 @@ def _init_modules(args, crash_handler):
|
|||
cmdhistory.init()
|
||||
log.init.debug("Initializing sessions...")
|
||||
sessions.init(q_app)
|
||||
quitter.shutting_down.connect(sessions.shutdown)
|
||||
|
||||
log.init.debug("Initializing websettings...")
|
||||
websettings.init(args)
|
||||
quitter.shutting_down.connect(websettings.shutdown)
|
||||
|
||||
if not args.no_err_windows:
|
||||
crash_handler.display_faulthandler()
|
||||
|
|
@ -487,14 +496,10 @@ def _init_modules(args, crash_handler):
|
|||
objreg.register('bookmark-manager', bookmark_manager)
|
||||
|
||||
log.init.debug("Initializing cookies...")
|
||||
cookie_jar = cookies.CookieJar(q_app)
|
||||
ram_cookie_jar = cookies.RAMCookieJar(q_app)
|
||||
objreg.register('cookie-jar', cookie_jar)
|
||||
objreg.register('ram-cookie-jar', ram_cookie_jar)
|
||||
cookies.init(q_app)
|
||||
|
||||
log.init.debug("Initializing cache...")
|
||||
diskcache = cache.DiskCache(standarddir.cache(), parent=q_app)
|
||||
objreg.register('cache', diskcache)
|
||||
cache.init(q_app)
|
||||
|
||||
log.init.debug("Initializing downloads...")
|
||||
download_manager = qtnetworkdownloads.DownloadManager(parent=q_app)
|
||||
|
|
@ -509,30 +514,35 @@ def _init_modules(args, crash_handler):
|
|||
browsertab.init()
|
||||
|
||||
|
||||
class Quitter:
|
||||
class Quitter(QObject):
|
||||
|
||||
"""Utility class to quit/restart the QApplication.
|
||||
|
||||
Attributes:
|
||||
quit_status: The current quitting status.
|
||||
_shutting_down: Whether we're currently shutting down.
|
||||
_is_shutting_down: Whether we're currently shutting down.
|
||||
_args: The argparse namespace.
|
||||
"""
|
||||
|
||||
def __init__(self, args):
|
||||
shutting_down = pyqtSignal() # Emitted immediately before shut down
|
||||
|
||||
def __init__(self, *,
|
||||
args: argparse.Namespace,
|
||||
parent: QObject = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.quit_status = {
|
||||
'crash': True,
|
||||
'tabs': False,
|
||||
'main': False,
|
||||
}
|
||||
self._shutting_down = False
|
||||
self._is_shutting_down = False
|
||||
self._args = args
|
||||
|
||||
def on_last_window_closed(self):
|
||||
def on_last_window_closed(self) -> None:
|
||||
"""Slot which gets invoked when the last window was closed."""
|
||||
self.shutdown(last_window=True)
|
||||
|
||||
def _compile_modules(self):
|
||||
def _compile_modules(self) -> None:
|
||||
"""Compile all modules to catch SyntaxErrors."""
|
||||
if os.path.basename(sys.argv[0]) == 'qutebrowser':
|
||||
# Launched via launcher script
|
||||
|
|
@ -551,8 +561,12 @@ class Quitter:
|
|||
with tokenize.open(os.path.join(dirpath, fn)) as f:
|
||||
compile(f.read(), fn, 'exec')
|
||||
|
||||
def _get_restart_args(self, pages=(), session=None, override_args=None):
|
||||
"""Get the current working directory and args to relaunch qutebrowser.
|
||||
def _get_restart_args(
|
||||
self, pages: typing.Iterable[str] = (),
|
||||
session: str = None,
|
||||
override_args: typing.Mapping[str, str] = None
|
||||
) -> typing.Sequence[str]:
|
||||
"""Get args to relaunch qutebrowser.
|
||||
|
||||
Args:
|
||||
pages: The pages to re-open.
|
||||
|
|
@ -560,29 +574,18 @@ class Quitter:
|
|||
override_args: Argument overrides as a dict.
|
||||
|
||||
Return:
|
||||
An (args, cwd) tuple.
|
||||
args: The commandline as a list of strings.
|
||||
cwd: The current working directory as a string.
|
||||
The commandline as a list of strings.
|
||||
"""
|
||||
if os.path.basename(sys.argv[0]) == 'qutebrowser':
|
||||
# Launched via launcher script
|
||||
args = [sys.argv[0]]
|
||||
cwd = None
|
||||
elif hasattr(sys, 'frozen'):
|
||||
args = [sys.executable]
|
||||
cwd = os.path.abspath(os.path.dirname(sys.executable))
|
||||
else:
|
||||
args = [sys.executable, '-m', 'qutebrowser']
|
||||
cwd = os.path.join(
|
||||
os.path.abspath(os.path.dirname(qutebrowser.__file__)), '..')
|
||||
if not os.path.isdir(cwd):
|
||||
# Probably running from a python egg. Let's fallback to
|
||||
# cwd=None and see if that works out.
|
||||
# See https://github.com/qutebrowser/qutebrowser/issues/323
|
||||
cwd = None
|
||||
|
||||
# Add all open pages so they get reopened.
|
||||
page_args = []
|
||||
page_args = [] # type: typing.MutableSequence[str]
|
||||
for win in pages:
|
||||
page_args.extend(win)
|
||||
page_args.append('')
|
||||
|
|
@ -616,12 +619,11 @@ class Quitter:
|
|||
args += ['--json-args', data]
|
||||
|
||||
log.destroy.debug("args: {}".format(args))
|
||||
log.destroy.debug("cwd: {}".format(cwd))
|
||||
|
||||
return args, cwd
|
||||
return args
|
||||
|
||||
@cmdutils.register(instance='quitter', name='restart')
|
||||
def restart_cmd(self):
|
||||
def restart_cmd(self) -> None:
|
||||
"""Restart qutebrowser while keeping existing tabs open."""
|
||||
try:
|
||||
ok = self.restart(session='_restart')
|
||||
|
|
@ -636,7 +638,9 @@ class Quitter:
|
|||
if ok:
|
||||
self.shutdown(restart=True)
|
||||
|
||||
def restart(self, pages=(), session=None, override_args=None):
|
||||
def restart(self, pages: typing.Sequence[str] = (),
|
||||
session: str = None,
|
||||
override_args: typing.Mapping[str, str] = None) -> bool:
|
||||
"""Inner logic to restart qutebrowser.
|
||||
|
||||
The "better" way to restart is to pass a session (_restart usually) as
|
||||
|
|
@ -662,20 +666,17 @@ class Quitter:
|
|||
|
||||
# Save the session if one is given.
|
||||
if session is not None:
|
||||
session_manager = objreg.get('session-manager')
|
||||
session_manager.save(session, with_private=True)
|
||||
sessions.session_manager.save(session, with_private=True)
|
||||
|
||||
# Make sure we're not accepting a connection from the new process
|
||||
# before we fully exited.
|
||||
assert ipc.server is not None
|
||||
ipc.server.shutdown()
|
||||
|
||||
# Open a new process and immediately shutdown the existing one
|
||||
try:
|
||||
args, cwd = self._get_restart_args(pages, session, override_args)
|
||||
if cwd is None:
|
||||
subprocess.Popen(args)
|
||||
else:
|
||||
subprocess.Popen(args, cwd=cwd)
|
||||
args = self._get_restart_args(pages, session, override_args)
|
||||
subprocess.Popen(args)
|
||||
except OSError:
|
||||
log.destroy.exception("Failed to restart")
|
||||
return False
|
||||
|
|
@ -684,7 +685,9 @@ class Quitter:
|
|||
|
||||
@cmdutils.register(instance='quitter', name='quit')
|
||||
@cmdutils.argument('session', completion=miscmodels.session)
|
||||
def quit(self, save=False, session=None):
|
||||
def quit(self,
|
||||
save: bool = False,
|
||||
session: sessions.ArgType = None) -> None:
|
||||
"""Quit qutebrowser.
|
||||
|
||||
Args:
|
||||
|
|
@ -701,8 +704,10 @@ class Quitter:
|
|||
else:
|
||||
self.shutdown()
|
||||
|
||||
def shutdown(self, status=0, session=None, last_window=False,
|
||||
restart=False):
|
||||
def shutdown(self, status: int = 0,
|
||||
session: sessions.ArgType = None,
|
||||
last_window: bool = False,
|
||||
restart: bool = False) -> None:
|
||||
"""Quit qutebrowser.
|
||||
|
||||
Args:
|
||||
|
|
@ -712,19 +717,20 @@ class Quitter:
|
|||
closing.
|
||||
restart: If we're planning to restart.
|
||||
"""
|
||||
if self._shutting_down:
|
||||
if self._is_shutting_down:
|
||||
return
|
||||
self._shutting_down = True
|
||||
self._is_shutting_down = True
|
||||
log.destroy.debug("Shutting down with status {}, session {}...".format(
|
||||
status, session))
|
||||
session_manager = objreg.get('session-manager', None)
|
||||
if session_manager is not None:
|
||||
if sessions.session_manager is not None:
|
||||
if session is not None:
|
||||
session_manager.save(session, last_window=last_window,
|
||||
load_next_time=True)
|
||||
sessions.session_manager.save(session,
|
||||
last_window=last_window,
|
||||
load_next_time=True)
|
||||
elif config.val.auto_save.session:
|
||||
session_manager.save(sessions.default, last_window=last_window,
|
||||
load_next_time=True)
|
||||
sessions.session_manager.save(sessions.default,
|
||||
last_window=last_window,
|
||||
load_next_time=True)
|
||||
|
||||
if prompt.prompt_queue.shutdown():
|
||||
# If shutdown was called while we were asking a question, we're in
|
||||
|
|
@ -741,64 +747,24 @@ class Quitter:
|
|||
# event loop, so we can shut down immediately.
|
||||
self._shutdown(status, restart=restart)
|
||||
|
||||
def _shutdown(self, status, restart): # noqa
|
||||
def _shutdown(self, status: int, restart: bool) -> None: # noqa
|
||||
"""Second stage of shutdown."""
|
||||
log.destroy.debug("Stage 2 of shutting down...")
|
||||
if q_app is None:
|
||||
# No QApplication exists yet, so quit hard.
|
||||
sys.exit(status)
|
||||
# Remove eventfilter
|
||||
try:
|
||||
log.destroy.debug("Removing eventfilter...")
|
||||
event_filter = objreg.get('event-filter', None)
|
||||
if event_filter is not None:
|
||||
q_app.removeEventFilter(event_filter)
|
||||
except AttributeError:
|
||||
pass
|
||||
# Close all windows
|
||||
QApplication.closeAllWindows()
|
||||
# Shut down IPC
|
||||
try:
|
||||
ipc.server.shutdown()
|
||||
except KeyError:
|
||||
pass
|
||||
# Save everything
|
||||
try:
|
||||
save_manager = objreg.get('save-manager')
|
||||
except KeyError:
|
||||
log.destroy.debug("Save manager not initialized yet, so not "
|
||||
"saving anything.")
|
||||
else:
|
||||
for key in save_manager.saveables:
|
||||
try:
|
||||
save_manager.save(key, is_exit=True)
|
||||
except OSError as e:
|
||||
error.handle_fatal_exc(
|
||||
e, self._args, "Error while saving!",
|
||||
pre_text="Error while saving {}".format(key))
|
||||
# Disable storage so removing tempdir will work
|
||||
websettings.shutdown()
|
||||
# Disable application proxy factory to fix segfaults with Qt 5.10.1
|
||||
proxy.shutdown()
|
||||
# Re-enable faulthandler to stdout, then remove crash log
|
||||
log.destroy.debug("Deactivating crash log...")
|
||||
objreg.get('crash-handler').destroy_crashlogfile()
|
||||
|
||||
# Tell everything to shut itself down
|
||||
self.shutting_down.emit()
|
||||
|
||||
# Delete temp basedir
|
||||
if ((self._args.temp_basedir or self._args.temp_basedir_restarted) and
|
||||
not restart):
|
||||
atexit.register(shutil.rmtree, self._args.basedir,
|
||||
ignore_errors=True)
|
||||
# Delete temp download dir
|
||||
downloads.temp_download_manager.cleanup()
|
||||
# If we don't kill our custom handler here we might get segfaults
|
||||
log.destroy.debug("Deactivating message handler...")
|
||||
qInstallMessageHandler(None)
|
||||
|
||||
# Now we can hopefully quit without segfaults
|
||||
log.destroy.debug("Deferring QApplication::exit...")
|
||||
objreg.get('signal-handler').deactivate()
|
||||
session_manager = objreg.get('session-manager', None)
|
||||
if session_manager is not None:
|
||||
session_manager.delete_autosave()
|
||||
# We use a singleshot timer to exit here to minimize the likelihood of
|
||||
# segfaults.
|
||||
QTimer.singleShot(0, functools.partial(q_app.exit, status))
|
||||
|
|
@ -827,10 +793,9 @@ class Application(QApplication):
|
|||
log.init.debug("Qt arguments: {}, based on {}".format(qt_args, args))
|
||||
super().__init__(qt_args)
|
||||
|
||||
log.init.debug("Initializing application...")
|
||||
objects.args = args
|
||||
|
||||
objreg.register('args', args)
|
||||
objreg.register('app', self)
|
||||
log.init.debug("Initializing application...")
|
||||
|
||||
self.launch_time = datetime.datetime.now()
|
||||
self.focusObjectChanged.connect(self.on_focus_object_changed)
|
||||
|
|
@ -883,7 +848,7 @@ class EventFilter(QObject):
|
|||
event.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, parent: QObject = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._activated = True
|
||||
self._handlers = {
|
||||
|
|
@ -892,7 +857,14 @@ class EventFilter(QObject):
|
|||
QEvent.ShortcutOverride: self._handle_key_event,
|
||||
}
|
||||
|
||||
def _handle_key_event(self, event):
|
||||
def install(self):
|
||||
q_app.installEventFilter(self)
|
||||
|
||||
@pyqtSlot()
|
||||
def shutdown(self):
|
||||
q_app.removeEventFilter(self)
|
||||
|
||||
def _handle_key_event(self, event: QKeyEvent) -> bool:
|
||||
"""Handle a key press/release event.
|
||||
|
||||
Args:
|
||||
|
|
@ -912,7 +884,7 @@ class EventFilter(QObject):
|
|||
# No window available yet, or not a MainWindow
|
||||
return False
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
|
||||
"""Handle an event.
|
||||
|
||||
Args:
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@ from qutebrowser.keyinput import modeman
|
|||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import (utils, objreg, usertypes, log, qtutils,
|
||||
urlutils, message)
|
||||
from qutebrowser.misc import miscwidgets, objects
|
||||
from qutebrowser.browser import eventfilter, hints
|
||||
from qutebrowser.misc import miscwidgets, objects, sessions
|
||||
from qutebrowser.browser import eventfilter
|
||||
from qutebrowser.qt import sip
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
|
|
@ -374,7 +374,7 @@ class AbstractZoom(QObject):
|
|||
levels, mode=usertypes.NeighborList.Modes.edge)
|
||||
self._neighborlist.fuzzyval = config.val.zoom.default
|
||||
|
||||
def apply_offset(self, offset: int) -> None:
|
||||
def apply_offset(self, offset: int) -> int:
|
||||
"""Increase/Decrease the zoom level by the given offset.
|
||||
|
||||
Args:
|
||||
|
|
@ -383,7 +383,7 @@ class AbstractZoom(QObject):
|
|||
Return:
|
||||
The new zoom percentage.
|
||||
"""
|
||||
level = self._neighborlist.getitem(offset)
|
||||
level = self._neighborlist.getitem(offset) # type: int
|
||||
self.set_factor(float(level) / 100, fuzzyval=False)
|
||||
return level
|
||||
|
||||
|
|
@ -888,17 +888,12 @@ class AbstractTab(QWidget):
|
|||
self._tab_event_filter = eventfilter.TabEventFilter(
|
||||
self, parent=self)
|
||||
self.backend = None
|
||||
|
||||
# If true, this tab has been requested to be removed (or is removed).
|
||||
self.pending_removal = False
|
||||
self.shutting_down.connect(functools.partial(
|
||||
setattr, self, 'pending_removal', True))
|
||||
|
||||
# FIXME:qtwebengine Should this be public api via self.hints?
|
||||
# Also, should we get it out of objreg?
|
||||
hintmanager = hints.HintManager(win_id, self.tab_id, parent=self)
|
||||
objreg.register('hintmanager', hintmanager, scope='tab',
|
||||
window=self.win_id, tab=self.tab_id)
|
||||
|
||||
self.before_load_started.connect(self._on_before_load_started)
|
||||
|
||||
def _set_widget(self, widget: QWidget) -> None:
|
||||
|
|
@ -1015,13 +1010,9 @@ class AbstractTab(QWidget):
|
|||
# https://github.com/qutebrowser/qutebrowser/issues/3498
|
||||
return
|
||||
|
||||
try:
|
||||
sess_manager = objreg.get('session-manager')
|
||||
except KeyError:
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/4311
|
||||
return
|
||||
if sessions.session_manager is not None:
|
||||
sessions.session_manager.save_autosave()
|
||||
|
||||
sess_manager.save_autosave()
|
||||
self.load_finished.emit(ok)
|
||||
|
||||
if not self.title():
|
||||
|
|
|
|||
|
|
@ -849,7 +849,7 @@ class CommandDispatcher:
|
|||
idx = int(index_parts[1])
|
||||
elif len(index_parts) == 1:
|
||||
idx = int(index_parts[0])
|
||||
active_win = objreg.get('app').activeWindow()
|
||||
active_win = QApplication.activeWindow()
|
||||
if active_win is None:
|
||||
# Not sure how you enter a command without an active window...
|
||||
raise cmdutils.CommandError(
|
||||
|
|
|
|||
|
|
@ -77,6 +77,11 @@ def init():
|
|||
config.instance.changed.connect(_clear_last_used)
|
||||
|
||||
|
||||
@pyqtSlot()
|
||||
def shutdown():
|
||||
temp_download_manager.cleanup()
|
||||
|
||||
|
||||
@config.change_filter('downloads.location.directory', function=True)
|
||||
def _clear_last_used():
|
||||
global last_used_directory
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ from PyQt5.QtWidgets import QListView, QSizePolicy, QMenu, QStyleFactory
|
|||
|
||||
from qutebrowser.browser import downloads
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import qtutils, utils, objreg
|
||||
from qutebrowser.utils import qtutils, utils
|
||||
from qutebrowser.qt import sip
|
||||
|
||||
|
||||
|
|
@ -59,7 +59,6 @@ class DownloadView(QListView):
|
|||
|
||||
Attributes:
|
||||
_menu: The QMenu which is currently displayed.
|
||||
_model: The currently set model.
|
||||
"""
|
||||
|
||||
STYLESHEET = """
|
||||
|
|
@ -73,7 +72,7 @@ class DownloadView(QListView):
|
|||
}
|
||||
"""
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
def __init__(self, model, parent=None):
|
||||
super().__init__(parent)
|
||||
if not utils.is_mac:
|
||||
self.setStyle(QStyleFactory.create('Fusion'))
|
||||
|
|
@ -85,7 +84,6 @@ class DownloadView(QListView):
|
|||
self.setFlow(QListView.LeftToRight)
|
||||
self.setSpacing(1)
|
||||
self._menu = None
|
||||
model = objreg.get('download-model', scope='window', window=win_id)
|
||||
model.rowsInserted.connect(functools.partial(update_geometry, self))
|
||||
model.rowsRemoved.connect(functools.partial(update_geometry, self))
|
||||
model.dataChanged.connect(functools.partial(update_geometry, self))
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"""A HintManager to draw hints over links."""
|
||||
|
||||
import collections
|
||||
import typing
|
||||
import functools
|
||||
import os
|
||||
import re
|
||||
|
|
@ -37,6 +38,8 @@ from qutebrowser.browser import webelem
|
|||
from qutebrowser.commands import userscripts, runners
|
||||
from qutebrowser.api import cmdutils
|
||||
from qutebrowser.utils import usertypes, log, qtutils, message, objreg, utils
|
||||
if typing.TYPE_CHECKING:
|
||||
from qutebrowser.browser import browsertab
|
||||
|
||||
|
||||
Target = enum.Enum('Target', ['normal', 'current', 'tab', 'tab_fg', 'tab_bg',
|
||||
|
|
@ -50,7 +53,7 @@ class HintingError(Exception):
|
|||
"""Exception raised on errors during hinting."""
|
||||
|
||||
|
||||
def on_mode_entered(mode, win_id):
|
||||
def on_mode_entered(mode: usertypes.KeyMode, win_id: int) -> None:
|
||||
"""Stop hinting when insert mode was entered."""
|
||||
if mode == usertypes.KeyMode.insert:
|
||||
modeman.leave(win_id, usertypes.KeyMode.hint, 'insert mode',
|
||||
|
|
@ -66,7 +69,8 @@ class HintLabel(QLabel):
|
|||
_context: The current hinting context.
|
||||
"""
|
||||
|
||||
def __init__(self, elem, context):
|
||||
def __init__(self, elem: webelem.AbstractWebElement,
|
||||
context: 'HintContext') -> None:
|
||||
super().__init__(parent=context.tab)
|
||||
self._context = context
|
||||
self.elem = elem
|
||||
|
|
@ -81,14 +85,14 @@ class HintLabel(QLabel):
|
|||
self._move_to_elem()
|
||||
self.show()
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
try:
|
||||
text = self.text()
|
||||
except RuntimeError:
|
||||
text = '<deleted>'
|
||||
return utils.get_repr(self, elem=self.elem, text=text)
|
||||
|
||||
def update_text(self, matched, unmatched):
|
||||
def update_text(self, matched: str, unmatched: str) -> None:
|
||||
"""Set the text for the hint.
|
||||
|
||||
Args:
|
||||
|
|
@ -112,7 +116,7 @@ class HintLabel(QLabel):
|
|||
self.adjustSize()
|
||||
|
||||
@pyqtSlot()
|
||||
def _move_to_elem(self):
|
||||
def _move_to_elem(self) -> None:
|
||||
"""Reposition the label to its element."""
|
||||
if not self.elem.has_frame():
|
||||
# This sometimes happens for some reason...
|
||||
|
|
@ -123,7 +127,7 @@ class HintLabel(QLabel):
|
|||
rect = self.elem.rect_on_view(no_js=no_js)
|
||||
self.move(rect.x(), rect.y())
|
||||
|
||||
def cleanup(self):
|
||||
def cleanup(self) -> None:
|
||||
"""Clean up this element and hide it."""
|
||||
self.hide()
|
||||
self.deleteLater()
|
||||
|
|
@ -159,22 +163,22 @@ class HintContext:
|
|||
group: The group of web elements to hint.
|
||||
"""
|
||||
|
||||
all_labels = attr.ib(attr.Factory(list))
|
||||
labels = attr.ib(attr.Factory(dict))
|
||||
target = attr.ib(None)
|
||||
baseurl = attr.ib(None)
|
||||
to_follow = attr.ib(None)
|
||||
rapid = attr.ib(False)
|
||||
first_run = attr.ib(True)
|
||||
add_history = attr.ib(False)
|
||||
filterstr = attr.ib(None)
|
||||
args = attr.ib(attr.Factory(list))
|
||||
tab = attr.ib(None)
|
||||
group = attr.ib(None)
|
||||
hint_mode = attr.ib(None)
|
||||
first = attr.ib(False)
|
||||
all_labels = attr.ib(attr.Factory(list)) # type: typing.List[HintLabel]
|
||||
labels = attr.ib(attr.Factory(dict)) # type: typing.Dict[str, HintLabel]
|
||||
target = attr.ib(None) # type: Target
|
||||
baseurl = attr.ib(None) # type: QUrl
|
||||
to_follow = attr.ib(None) # type: str
|
||||
rapid = attr.ib(False) # type: bool
|
||||
first_run = attr.ib(True) # type: bool
|
||||
add_history = attr.ib(False) # type: bool
|
||||
filterstr = attr.ib(None) # type: str
|
||||
args = attr.ib(attr.Factory(list)) # type: typing.List[str]
|
||||
tab = attr.ib(None) # type: browsertab.AbstractTab
|
||||
group = attr.ib(None) # type: str
|
||||
hint_mode = attr.ib(None) # type: str
|
||||
first = attr.ib(False) # type: bool
|
||||
|
||||
def get_args(self, urlstr):
|
||||
def get_args(self, urlstr: str) -> typing.Sequence[str]:
|
||||
"""Get the arguments, with {hint-url} replaced by the given URL."""
|
||||
args = []
|
||||
for arg in self.args:
|
||||
|
|
@ -187,16 +191,12 @@ class HintActions:
|
|||
|
||||
"""Actions which can be done after selecting a hint."""
|
||||
|
||||
def __init__(self, win_id):
|
||||
def __init__(self, win_id: int) -> None:
|
||||
self._win_id = win_id
|
||||
|
||||
def click(self, elem, context):
|
||||
"""Click an element.
|
||||
|
||||
Args:
|
||||
elem: The QWebElement to click.
|
||||
context: The HintContext to use.
|
||||
"""
|
||||
def click(self, elem: webelem.AbstractWebElement,
|
||||
context: HintContext) -> None:
|
||||
"""Click an element."""
|
||||
target_mapping = {
|
||||
Target.normal: usertypes.ClickTarget.normal,
|
||||
Target.current: usertypes.ClickTarget.normal,
|
||||
|
|
@ -225,20 +225,15 @@ class HintActions:
|
|||
except webelem.Error as e:
|
||||
raise HintingError(str(e))
|
||||
|
||||
def yank(self, url, context):
|
||||
"""Yank an element to the clipboard or primary selection.
|
||||
|
||||
Args:
|
||||
url: The URL to open as a QUrl.
|
||||
context: The HintContext to use.
|
||||
"""
|
||||
def yank(self, url: QUrl, context: HintContext) -> None:
|
||||
"""Yank an element to the clipboard or primary selection."""
|
||||
sel = (context.target == Target.yank_primary and
|
||||
utils.supports_selection())
|
||||
|
||||
flags = QUrl.FullyEncoded | QUrl.RemovePassword
|
||||
if url.scheme() == 'mailto':
|
||||
flags |= QUrl.RemoveScheme
|
||||
urlstr = url.toString(flags)
|
||||
urlstr = url.toString(flags) # type: ignore
|
||||
|
||||
new_content = urlstr
|
||||
|
||||
|
|
@ -257,26 +252,16 @@ class HintActions:
|
|||
urlstr)
|
||||
message.info(msg)
|
||||
|
||||
def run_cmd(self, url, context):
|
||||
"""Run the command based on a hint URL.
|
||||
|
||||
Args:
|
||||
url: The URL to open as a QUrl.
|
||||
context: The HintContext to use.
|
||||
"""
|
||||
urlstr = url.toString(QUrl.FullyEncoded)
|
||||
def run_cmd(self, url: QUrl, context: HintContext) -> None:
|
||||
"""Run the command based on a hint URL."""
|
||||
urlstr = url.toString(QUrl.FullyEncoded) # type: ignore
|
||||
args = context.get_args(urlstr)
|
||||
commandrunner = runners.CommandRunner(self._win_id)
|
||||
commandrunner.run_safely(' '.join(args))
|
||||
|
||||
def preset_cmd_text(self, url, context):
|
||||
"""Preset a commandline text based on a hint URL.
|
||||
|
||||
Args:
|
||||
url: The URL to open as a QUrl.
|
||||
context: The HintContext to use.
|
||||
"""
|
||||
urlstr = url.toDisplayString(QUrl.FullyEncoded)
|
||||
def preset_cmd_text(self, url: QUrl, context: HintContext) -> None:
|
||||
"""Preset a commandline text based on a hint URL."""
|
||||
urlstr = url.toDisplayString(QUrl.FullyEncoded) # type: ignore
|
||||
args = context.get_args(urlstr)
|
||||
text = ' '.join(args)
|
||||
if text[0] not in modeparsers.STARTCHARS:
|
||||
|
|
@ -285,7 +270,8 @@ class HintActions:
|
|||
cmd = objreg.get('status-command', scope='window', window=self._win_id)
|
||||
cmd.set_cmd_text(text)
|
||||
|
||||
def download(self, elem, context):
|
||||
def download(self, elem: webelem.AbstractWebElement,
|
||||
context: HintContext) -> None:
|
||||
"""Download a hint URL.
|
||||
|
||||
Args:
|
||||
|
|
@ -305,7 +291,8 @@ class HintActions:
|
|||
download_manager.get(url, qnam=qnam, user_agent=user_agent,
|
||||
prompt_download_directory=prompt)
|
||||
|
||||
def call_userscript(self, elem, context):
|
||||
def call_userscript(self, elem: webelem.AbstractWebElement,
|
||||
context: HintContext) -> None:
|
||||
"""Call a userscript from a hint.
|
||||
|
||||
Args:
|
||||
|
|
@ -321,7 +308,7 @@ class HintActions:
|
|||
}
|
||||
url = elem.resolve_url(context.baseurl)
|
||||
if url is not None:
|
||||
env['QUTE_URL'] = url.toString(QUrl.FullyEncoded)
|
||||
env['QUTE_URL'] = url.toString(QUrl.FullyEncoded) # type: ignore
|
||||
|
||||
try:
|
||||
userscripts.run_async(context.tab, cmd, *args, win_id=self._win_id,
|
||||
|
|
@ -329,22 +316,28 @@ class HintActions:
|
|||
except userscripts.Error as e:
|
||||
raise HintingError(str(e))
|
||||
|
||||
def delete(self, elem, _context):
|
||||
def delete(self, elem: webelem.AbstractWebElement,
|
||||
_context: HintContext) -> None:
|
||||
elem.delete()
|
||||
|
||||
def spawn(self, url, context):
|
||||
def spawn(self, url: QUrl, context: HintContext) -> None:
|
||||
"""Spawn a simple command from a hint.
|
||||
|
||||
Args:
|
||||
url: The URL to open as a QUrl.
|
||||
context: The HintContext to use.
|
||||
"""
|
||||
urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
|
||||
urlstr = url.toString(
|
||||
QUrl.FullyEncoded | QUrl.RemovePassword) # type: ignore
|
||||
args = context.get_args(urlstr)
|
||||
commandrunner = runners.CommandRunner(self._win_id)
|
||||
commandrunner.run_safely('spawn ' + ' '.join(args))
|
||||
|
||||
|
||||
_ElemsType = typing.Sequence[webelem.AbstractWebElement]
|
||||
_HintStringsType = typing.MutableSequence[str]
|
||||
|
||||
|
||||
class HintManager(QObject):
|
||||
|
||||
"""Manage drawing hints over links or other elements.
|
||||
|
|
@ -379,12 +372,11 @@ class HintManager(QObject):
|
|||
Target.delete: "Delete an element",
|
||||
}
|
||||
|
||||
def __init__(self, win_id, tab_id, parent=None):
|
||||
def __init__(self, win_id: int, parent: QObject = None) -> None:
|
||||
"""Constructor."""
|
||||
super().__init__(parent)
|
||||
self._win_id = win_id
|
||||
self._tab_id = tab_id
|
||||
self._context = None
|
||||
self._context = None # type: typing.Optional[HintContext]
|
||||
self._word_hinter = WordHinter()
|
||||
|
||||
self._actions = HintActions(win_id)
|
||||
|
|
@ -393,16 +385,18 @@ class HintManager(QObject):
|
|||
window=win_id)
|
||||
mode_manager.left.connect(self.on_mode_left)
|
||||
|
||||
def _get_text(self):
|
||||
def _get_text(self) -> str:
|
||||
"""Get a hint text based on the current context."""
|
||||
assert self._context is not None
|
||||
text = self.HINT_TEXTS[self._context.target]
|
||||
if self._context.rapid:
|
||||
text += ' (rapid mode)'
|
||||
text += '...'
|
||||
return text
|
||||
|
||||
def _cleanup(self):
|
||||
def _cleanup(self) -> None:
|
||||
"""Clean up after hinting."""
|
||||
assert self._context is not None
|
||||
for label in self._context.all_labels:
|
||||
label.cleanup()
|
||||
|
||||
|
|
@ -412,7 +406,7 @@ class HintManager(QObject):
|
|||
message_bridge.maybe_reset_text(text)
|
||||
self._context = None
|
||||
|
||||
def _hint_strings(self, elems):
|
||||
def _hint_strings(self, elems: _ElemsType) -> _HintStringsType:
|
||||
"""Calculate the hint strings for elems.
|
||||
|
||||
Inspired by Vimium.
|
||||
|
|
@ -425,6 +419,8 @@ class HintManager(QObject):
|
|||
"""
|
||||
if not elems:
|
||||
return []
|
||||
|
||||
assert self._context is not None
|
||||
hint_mode = self._context.hint_mode
|
||||
if hint_mode == 'word':
|
||||
try:
|
||||
|
|
@ -442,7 +438,9 @@ class HintManager(QObject):
|
|||
else:
|
||||
return self._hint_linear(min_chars, chars, elems)
|
||||
|
||||
def _hint_scattered(self, min_chars, chars, elems):
|
||||
def _hint_scattered(self, min_chars: int,
|
||||
chars: str,
|
||||
elems: _ElemsType) -> _HintStringsType:
|
||||
"""Produce scattered hint labels with variable length (like Vimium).
|
||||
|
||||
Args:
|
||||
|
|
@ -479,7 +477,9 @@ class HintManager(QObject):
|
|||
|
||||
return self._shuffle_hints(strings, len(chars))
|
||||
|
||||
def _hint_linear(self, min_chars, chars, elems):
|
||||
def _hint_linear(self, min_chars: int,
|
||||
chars: str,
|
||||
elems: _ElemsType) -> _HintStringsType:
|
||||
"""Produce linear hint labels with constant length (like dwb).
|
||||
|
||||
Args:
|
||||
|
|
@ -493,7 +493,8 @@ class HintManager(QObject):
|
|||
strings.append(self._number_to_hint_str(i, chars, needed))
|
||||
return strings
|
||||
|
||||
def _shuffle_hints(self, hints, length):
|
||||
def _shuffle_hints(self, hints: _HintStringsType,
|
||||
length: int) -> _HintStringsType:
|
||||
"""Shuffle the given set of hints so that they're scattered.
|
||||
|
||||
Hints starting with the same character will be spread evenly throughout
|
||||
|
|
@ -508,15 +509,19 @@ class HintManager(QObject):
|
|||
Return:
|
||||
A list of shuffled hint strings.
|
||||
"""
|
||||
buckets = [[] for i in range(length)]
|
||||
buckets = [
|
||||
[] for i in range(length)
|
||||
] # type: typing.Sequence[_HintStringsType]
|
||||
for i, hint in enumerate(hints):
|
||||
buckets[i % len(buckets)].append(hint)
|
||||
result = []
|
||||
result = [] # type: _HintStringsType
|
||||
for bucket in buckets:
|
||||
result += bucket
|
||||
return result
|
||||
|
||||
def _number_to_hint_str(self, number, chars, digits=0):
|
||||
def _number_to_hint_str(self, number: int,
|
||||
chars: str,
|
||||
digits: int = 0) -> str:
|
||||
"""Convert a number like "8" into a hint string like "JK".
|
||||
|
||||
This is used to sequentially generate all of the hint text.
|
||||
|
|
@ -534,7 +539,7 @@ class HintManager(QObject):
|
|||
A hint string.
|
||||
"""
|
||||
base = len(chars)
|
||||
hintstr = []
|
||||
hintstr = [] # type: typing.MutableSequence[str]
|
||||
remainder = 0
|
||||
while True:
|
||||
remainder = number % base
|
||||
|
|
@ -548,7 +553,7 @@ class HintManager(QObject):
|
|||
hintstr.insert(0, chars[0])
|
||||
return ''.join(hintstr)
|
||||
|
||||
def _check_args(self, target, *args):
|
||||
def _check_args(self, target: Target, *args: str) -> None:
|
||||
"""Check the arguments passed to start() and raise if they're wrong.
|
||||
|
||||
Args:
|
||||
|
|
@ -568,7 +573,7 @@ class HintManager(QObject):
|
|||
raise cmdutils.CommandError(
|
||||
"'args' is only allowed with target userscript/spawn.")
|
||||
|
||||
def _filter_matches(self, filterstr, elemstr):
|
||||
def _filter_matches(self, filterstr: str, elemstr: str) -> bool:
|
||||
"""Return True if `filterstr` matches `elemstr`."""
|
||||
# Empty string and None always match
|
||||
if not filterstr:
|
||||
|
|
@ -578,7 +583,7 @@ class HintManager(QObject):
|
|||
# Do multi-word matching
|
||||
return all(word in elemstr for word in filterstr.split())
|
||||
|
||||
def _filter_matches_exactly(self, filterstr, elemstr):
|
||||
def _filter_matches_exactly(self, filterstr: str, elemstr: str) -> bool:
|
||||
"""Return True if `filterstr` exactly matches `elemstr`."""
|
||||
# Empty string and None never match
|
||||
if not filterstr:
|
||||
|
|
@ -587,7 +592,7 @@ class HintManager(QObject):
|
|||
elemstr = elemstr.casefold()
|
||||
return filterstr == elemstr
|
||||
|
||||
def _start_cb(self, elems):
|
||||
def _start_cb(self, elems: _ElemsType) -> None:
|
||||
"""Initialize the elements and labels based on the context set."""
|
||||
if self._context is None:
|
||||
log.hints.debug("In _start_cb without context!")
|
||||
|
|
@ -603,10 +608,10 @@ class HintManager(QObject):
|
|||
tabbed_browser = objreg.get('tabbed-browser', default=None,
|
||||
scope='window', window=self._win_id)
|
||||
tab = tabbed_browser.widget.currentWidget()
|
||||
if tab.tab_id != self._tab_id:
|
||||
if tab.tab_id != self._context.tab.tab_id:
|
||||
log.hints.debug(
|
||||
"Current tab changed ({} -> {}) before _start_cb is run."
|
||||
.format(self._tab_id, tab.tab_id))
|
||||
.format(self._context.tab.tab_id, tab.tab_id))
|
||||
return
|
||||
|
||||
strings = self._hint_strings(elems)
|
||||
|
|
@ -635,11 +640,16 @@ class HintManager(QObject):
|
|||
# to make auto_follow == 'always' work
|
||||
self._handle_auto_follow()
|
||||
|
||||
@cmdutils.register(instance='hintmanager', scope='tab', name='hint',
|
||||
@cmdutils.register(instance='hintmanager', scope='window', name='hint',
|
||||
star_args_optional=True, maxsplit=2)
|
||||
def start(self, # pylint: disable=keyword-arg-before-vararg
|
||||
group='all', target=Target.normal, *args, mode=None,
|
||||
add_history=False, rapid=False, first=False):
|
||||
group: str = 'all',
|
||||
target: Target = Target.normal,
|
||||
*args: str,
|
||||
mode: str = None,
|
||||
add_history: bool = False,
|
||||
rapid: bool = False,
|
||||
first: bool = False) -> None:
|
||||
"""Start hinting.
|
||||
|
||||
Args:
|
||||
|
|
@ -755,7 +765,7 @@ class HintManager(QObject):
|
|||
error_cb=lambda err: message.error(str(err)),
|
||||
only_visible=True)
|
||||
|
||||
def _get_hint_mode(self, mode):
|
||||
def _get_hint_mode(self, mode: typing.Optional[str]) -> str:
|
||||
"""Get the hinting mode to use based on a mode argument."""
|
||||
if mode is None:
|
||||
return config.val.hints.mode
|
||||
|
|
@ -767,15 +777,22 @@ class HintManager(QObject):
|
|||
raise cmdutils.CommandError("Invalid mode: {}".format(e))
|
||||
return mode
|
||||
|
||||
def current_mode(self):
|
||||
def current_mode(self) -> typing.Optional[str]:
|
||||
"""Return the currently active hinting mode (or None otherwise)."""
|
||||
if self._context is None:
|
||||
return None
|
||||
|
||||
return self._context.hint_mode
|
||||
|
||||
def _handle_auto_follow(self, keystr="", filterstr="", visible=None):
|
||||
def _handle_auto_follow(
|
||||
self,
|
||||
keystr: str = "",
|
||||
filterstr: str = "",
|
||||
visible: typing.Mapping[str, HintLabel] = None
|
||||
) -> None:
|
||||
"""Handle the auto_follow option."""
|
||||
assert self._context is not None
|
||||
|
||||
if visible is None:
|
||||
visible = {string: label
|
||||
for string, label in self._context.labels.items()
|
||||
|
|
@ -789,7 +806,7 @@ class HintManager(QObject):
|
|||
if auto_follow == "always":
|
||||
follow = True
|
||||
elif auto_follow == "unique-match":
|
||||
follow = keystr or filterstr
|
||||
follow = bool(keystr or filterstr)
|
||||
elif auto_follow == "full-match":
|
||||
elemstr = str(list(visible.values())[0].elem)
|
||||
filter_match = self._filter_matches_exactly(filterstr, elemstr)
|
||||
|
|
@ -810,7 +827,8 @@ class HintManager(QObject):
|
|||
# unpacking gets us the first (and only) key in the dict.
|
||||
self._fire(*visible)
|
||||
|
||||
def handle_partial_key(self, keystr):
|
||||
@pyqtSlot(str)
|
||||
def handle_partial_key(self, keystr: str) -> None:
|
||||
"""Handle a new partial keypress."""
|
||||
if self._context is None:
|
||||
log.hints.debug("Got key without context!")
|
||||
|
|
@ -834,7 +852,7 @@ class HintManager(QObject):
|
|||
pass
|
||||
self._handle_auto_follow(keystr=keystr)
|
||||
|
||||
def filter_hints(self, filterstr):
|
||||
def filter_hints(self, filterstr: typing.Optional[str]) -> None:
|
||||
"""Filter displayed hints according to a text.
|
||||
|
||||
Args:
|
||||
|
|
@ -844,6 +862,8 @@ class HintManager(QObject):
|
|||
and `self._context.filterstr` are None, all hints are
|
||||
shown.
|
||||
"""
|
||||
assert self._context is not None
|
||||
|
||||
if filterstr is None:
|
||||
filterstr = self._context.filterstr
|
||||
else:
|
||||
|
|
@ -889,12 +909,13 @@ class HintManager(QObject):
|
|||
self._handle_auto_follow(filterstr=filterstr,
|
||||
visible=self._context.labels)
|
||||
|
||||
def _fire(self, keystr):
|
||||
def _fire(self, keystr: str) -> None:
|
||||
"""Fire a completed hint.
|
||||
|
||||
Args:
|
||||
keystr: The keychain string to follow.
|
||||
"""
|
||||
assert self._context is not None
|
||||
# Handlers which take a QWebElement
|
||||
elem_handlers = {
|
||||
Target.normal: self._actions.click,
|
||||
|
|
@ -956,15 +977,16 @@ class HintManager(QObject):
|
|||
if self._context is not None:
|
||||
self._context.first_run = False
|
||||
|
||||
@cmdutils.register(instance='hintmanager', scope='tab',
|
||||
@cmdutils.register(instance='hintmanager', scope='window',
|
||||
modes=[usertypes.KeyMode.hint])
|
||||
def follow_hint(self, select=False, keystring=None):
|
||||
def follow_hint(self, select: bool = False, keystring: str = None) -> None:
|
||||
"""Follow a hint.
|
||||
|
||||
Args:
|
||||
select: Only select the given hint, don't necessarily follow it.
|
||||
keystring: The hint to follow, or None.
|
||||
"""
|
||||
assert self._context is not None
|
||||
if keystring is None:
|
||||
if self._context.to_follow is None:
|
||||
raise cmdutils.CommandError("No hint to follow")
|
||||
|
|
@ -980,7 +1002,7 @@ class HintManager(QObject):
|
|||
self._fire(keystring)
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def on_mode_left(self, mode):
|
||||
def on_mode_left(self, mode: usertypes.KeyMode) -> None:
|
||||
"""Stop hinting when hinting mode was left."""
|
||||
if mode != usertypes.KeyMode.hint or self._context is None:
|
||||
# We have one HintManager per tab, so when this gets called,
|
||||
|
|
@ -999,12 +1021,12 @@ class WordHinter:
|
|||
derived from the hinted element.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
# will be initialized on first use.
|
||||
self.words = set()
|
||||
self.words = set() # type: typing.Set[str]
|
||||
self.dictionary = None
|
||||
|
||||
def ensure_initialized(self):
|
||||
def ensure_initialized(self) -> None:
|
||||
"""Generate the used words if yet uninitialized."""
|
||||
dictionary = config.val.hints.dictionary
|
||||
if not self.words or self.dictionary != dictionary:
|
||||
|
|
@ -1031,8 +1053,11 @@ class WordHinter:
|
|||
error = "Word hints requires reading the file at {}: {}"
|
||||
raise HintingError(error.format(dictionary, str(e)))
|
||||
|
||||
def extract_tag_words(self, elem):
|
||||
def extract_tag_words(
|
||||
self, elem: webelem.AbstractWebElement
|
||||
) -> typing.Iterator[str]:
|
||||
"""Extract tag words form the given element."""
|
||||
_extractor_type = typing.Callable[[webelem.AbstractWebElement], str]
|
||||
attr_extractors = {
|
||||
"alt": lambda elem: elem["alt"],
|
||||
"name": lambda elem: elem["name"],
|
||||
|
|
@ -1041,7 +1066,7 @@ class WordHinter:
|
|||
"src": lambda elem: elem["src"].split('/')[-1],
|
||||
"href": lambda elem: elem["href"].split('/')[-1],
|
||||
"text": str,
|
||||
}
|
||||
} # type: typing.Mapping[str, _extractor_type]
|
||||
|
||||
extractable_attrs = collections.defaultdict(list, {
|
||||
"img": ["alt", "title", "src"],
|
||||
|
|
@ -1055,7 +1080,10 @@ class WordHinter:
|
|||
for attr in extractable_attrs[elem.tag_name()]
|
||||
if attr in elem or attr == "text")
|
||||
|
||||
def tag_words_to_hints(self, words):
|
||||
def tag_words_to_hints(
|
||||
self,
|
||||
words: typing.Iterable[str]
|
||||
) -> typing.Iterator[str]:
|
||||
"""Take words and transform them to proper hints if possible."""
|
||||
for candidate in words:
|
||||
if not candidate:
|
||||
|
|
@ -1066,13 +1094,20 @@ class WordHinter:
|
|||
if 4 < match.end() - match.start() < 8:
|
||||
yield candidate[match.start():match.end()].lower()
|
||||
|
||||
def any_prefix(self, hint, existing):
|
||||
def any_prefix(self, hint: str, existing: typing.Iterable[str]) -> bool:
|
||||
return any(hint.startswith(e) or e.startswith(hint) for e in existing)
|
||||
|
||||
def filter_prefixes(self, hints, existing):
|
||||
def filter_prefixes(
|
||||
self,
|
||||
hints: typing.Iterable[str],
|
||||
existing: typing.Iterable[str]
|
||||
) -> typing.Iterator[str]:
|
||||
"""Filter hints which don't start with the given prefix."""
|
||||
return (h for h in hints if not self.any_prefix(h, existing))
|
||||
|
||||
def new_hint_for(self, elem, existing, fallback):
|
||||
def new_hint_for(self, elem: webelem.AbstractWebElement,
|
||||
existing: typing.Iterable[str],
|
||||
fallback: typing.Iterable[str]) -> typing.Optional[str]:
|
||||
"""Return a hint for elem, not conflicting with the existing."""
|
||||
new = self.tag_words_to_hints(self.extract_tag_words(elem))
|
||||
new_no_prefixes = self.filter_prefixes(new, existing)
|
||||
|
|
@ -1081,7 +1116,7 @@ class WordHinter:
|
|||
return (next(new_no_prefixes, None) or
|
||||
next(fallback_no_prefixes, None))
|
||||
|
||||
def hint(self, elems):
|
||||
def hint(self, elems: _ElemsType) -> _HintStringsType:
|
||||
"""Produce hint labels based on the html tags.
|
||||
|
||||
Produce hint words based on the link text and random words
|
||||
|
|
@ -1096,7 +1131,7 @@ class WordHinter:
|
|||
"""
|
||||
self.ensure_initialized()
|
||||
hints = []
|
||||
used_hints = set()
|
||||
used_hints = set() # type: typing.Set[str]
|
||||
words = iter(self.words)
|
||||
for elem in elems:
|
||||
hint = self.new_hint_for(elem, used_hints, words)
|
||||
|
|
|
|||
|
|
@ -19,21 +19,25 @@
|
|||
|
||||
"""Handling of proxies."""
|
||||
|
||||
import typing
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtCore import QUrl, pyqtSlot
|
||||
from PyQt5.QtNetwork import QNetworkProxy, QNetworkProxyFactory
|
||||
|
||||
from qutebrowser.config import config, configtypes
|
||||
from qutebrowser.utils import objreg, message, usertypes, urlutils
|
||||
from qutebrowser.utils import message, usertypes, urlutils
|
||||
from qutebrowser.misc import objects
|
||||
from qutebrowser.browser.network import pac
|
||||
|
||||
|
||||
application_factory = None
|
||||
|
||||
|
||||
def init():
|
||||
"""Set the application wide proxy factory."""
|
||||
proxy_factory = ProxyFactory()
|
||||
objreg.register('proxy-factory', proxy_factory)
|
||||
QNetworkProxyFactory.setApplicationProxyFactory(proxy_factory)
|
||||
global application_factory
|
||||
application_factory = ProxyFactory()
|
||||
QNetworkProxyFactory.setApplicationProxyFactory(application_factory)
|
||||
|
||||
config.instance.changed.connect(_warn_for_pac)
|
||||
_warn_for_pac()
|
||||
|
|
@ -48,6 +52,7 @@ def _warn_for_pac():
|
|||
message.error("PAC support isn't implemented for QtWebEngine yet!")
|
||||
|
||||
|
||||
@pyqtSlot()
|
||||
def shutdown():
|
||||
QNetworkProxyFactory.setApplicationProxyFactory(None)
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ from PyQt5.QtGui import QMouseEvent
|
|||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.mainwindow import mainwindow
|
||||
from qutebrowser.utils import log, usertypes, utils, qtutils, objreg
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
|
|
@ -89,7 +88,8 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
|||
|
||||
def __repr__(self) -> str:
|
||||
try:
|
||||
html = utils.compact_text(self.outer_xml(), 500)
|
||||
html = utils.compact_text(
|
||||
self.outer_xml(), 500) # type: typing.Optional[str]
|
||||
except Error:
|
||||
html = None
|
||||
return utils.get_repr(self, html=html)
|
||||
|
|
@ -385,6 +385,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
|||
background = click_target == usertypes.ClickTarget.tab_bg
|
||||
tabbed_browser.tabopen(url, background=background)
|
||||
elif click_target == usertypes.ClickTarget.window:
|
||||
from qutebrowser.mainwindow import mainwindow
|
||||
window = mainwindow.MainWindow(private=tabbed_browser.is_private)
|
||||
window.show()
|
||||
window.tabbed_browser.tabopen(url)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,10 @@ import os.path
|
|||
from PyQt5.QtNetwork import QNetworkDiskCache
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import utils, qtutils
|
||||
from qutebrowser.utils import utils, qtutils, standarddir
|
||||
|
||||
|
||||
diskcache = None
|
||||
|
||||
|
||||
class DiskCache(QNetworkDiskCache):
|
||||
|
|
@ -52,3 +55,9 @@ class DiskCache(QNetworkDiskCache):
|
|||
if not qtutils.version_check('5.9', compiled=False):
|
||||
size = 0 # pragma: no cover
|
||||
self.setMaximumCacheSize(size)
|
||||
|
||||
|
||||
def init(parent):
|
||||
"""Initialize the global cache."""
|
||||
global diskcache
|
||||
diskcache = DiskCache(standarddir.cache(), parent=parent)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ from qutebrowser.utils import utils, standarddir, objreg
|
|||
from qutebrowser.misc import lineparser
|
||||
|
||||
|
||||
cookie_jar = None
|
||||
ram_cookie_jar = None
|
||||
|
||||
|
||||
class RAMCookieJar(QNetworkCookieJar):
|
||||
|
||||
"""An in-RAM cookie jar.
|
||||
|
|
@ -112,3 +116,10 @@ class CookieJar(RAMCookieJar):
|
|||
self._lineparser.data = []
|
||||
self._lineparser.save()
|
||||
self.changed.emit()
|
||||
|
||||
|
||||
def init(qapp):
|
||||
"""Initialize the global cookie jars."""
|
||||
global cookie_jar, ram_cookie_jar
|
||||
cookie_jar = CookieJar(qapp)
|
||||
ram_cookie_jar = RAMCookieJar(qapp)
|
||||
|
|
|
|||
|
|
@ -32,8 +32,9 @@ from qutebrowser.config import config
|
|||
from qutebrowser.utils import (message, log, usertypes, utils, objreg,
|
||||
urlutils, debug)
|
||||
from qutebrowser.browser import shared
|
||||
from qutebrowser.browser.network import proxy as proxymod
|
||||
from qutebrowser.extensions import interceptors
|
||||
from qutebrowser.browser.webkit import certificateerror
|
||||
from qutebrowser.browser.webkit import certificateerror, cookies, cache
|
||||
from qutebrowser.browser.webkit.network import (webkitqutescheme, networkreply,
|
||||
filescheme)
|
||||
from qutebrowser.misc import objects
|
||||
|
|
@ -174,9 +175,10 @@ class NetworkManager(QNetworkAccessManager):
|
|||
def _set_cookiejar(self):
|
||||
"""Set the cookie jar of the NetworkManager correctly."""
|
||||
if self._private:
|
||||
cookie_jar = objreg.get('ram-cookie-jar')
|
||||
cookie_jar = cookies.ram_cookie_jar
|
||||
else:
|
||||
cookie_jar = objreg.get('cookie-jar')
|
||||
cookie_jar = cookies.cookie_jar
|
||||
assert cookie_jar is not None
|
||||
|
||||
# We have a shared cookie jar - we restore its parent so we don't
|
||||
# take ownership of it.
|
||||
|
|
@ -191,9 +193,8 @@ class NetworkManager(QNetworkAccessManager):
|
|||
# We have a shared cache - we restore its parent so we don't take
|
||||
# ownership of it.
|
||||
app = QCoreApplication.instance()
|
||||
cache = objreg.get('cache')
|
||||
self.setCache(cache)
|
||||
cache.setParent(app)
|
||||
self.setCache(cache.diskcache)
|
||||
cache.diskcache.setParent(app)
|
||||
|
||||
def _get_abort_signals(self, owner=None):
|
||||
"""Get a list of signals which should abort a question."""
|
||||
|
|
@ -375,9 +376,8 @@ class NetworkManager(QNetworkAccessManager):
|
|||
Return:
|
||||
A QNetworkReply.
|
||||
"""
|
||||
proxy_factory = objreg.get('proxy-factory', None)
|
||||
if proxy_factory is not None:
|
||||
proxy_error = proxy_factory.get_error()
|
||||
if proxymod.application_factory is not None:
|
||||
proxy_error = proxymod.application_factory.get_error()
|
||||
if proxy_error is not None:
|
||||
return networkreply.ErrorNetworkReply(
|
||||
req, proxy_error, QNetworkReply.UnknownProxyError,
|
||||
|
|
|
|||
|
|
@ -359,7 +359,8 @@ class Command:
|
|||
tab_id = None
|
||||
else:
|
||||
raise ValueError("Invalid scope {}!".format(scope))
|
||||
return objreg.get(name, scope=scope, window=win_id, tab=tab_id)
|
||||
return objreg.get(name, scope=scope, window=win_id, tab=tab_id,
|
||||
from_command=True)
|
||||
|
||||
def _add_special_arg(self, *, value, param, args, kwargs):
|
||||
"""Add a special argument value to a function call.
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ from qutebrowser.config import config
|
|||
from qutebrowser.commands import cmdexc
|
||||
from qutebrowser.utils import message, objreg, qtutils, usertypes, utils
|
||||
from qutebrowser.misc import split, objects
|
||||
from qutebrowser.keyinput import macros
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from qutebrowser.mainwindow import tabbedbrowser
|
||||
|
|
@ -303,7 +304,21 @@ class CommandParser:
|
|||
return split_args
|
||||
|
||||
|
||||
class CommandRunner(QObject):
|
||||
class AbstractCommandRunner(QObject):
|
||||
|
||||
"""Abstract base class for CommandRunner."""
|
||||
|
||||
def run(self, text, count=None, *, safely=False):
|
||||
raise NotImplementedError
|
||||
|
||||
@pyqtSlot(str, int)
|
||||
@pyqtSlot(str)
|
||||
def run_safely(self, text, count=None):
|
||||
"""Run a command and display exceptions in the statusbar."""
|
||||
self.run(text, count, safely=True)
|
||||
|
||||
|
||||
class CommandRunner(AbstractCommandRunner):
|
||||
|
||||
"""Parse and run qutebrowser commandline commands.
|
||||
|
||||
|
|
@ -369,11 +384,4 @@ class CommandRunner(QObject):
|
|||
last_command[cur_mode] = (text, count)
|
||||
|
||||
if record_macro and cur_mode == usertypes.KeyMode.normal:
|
||||
macro_recorder = objreg.get('macro-recorder')
|
||||
macro_recorder.record_command(text, count)
|
||||
|
||||
@pyqtSlot(str, int)
|
||||
@pyqtSlot(str)
|
||||
def run_safely(self, text, count=None):
|
||||
"""Run a command and display exceptions in the statusbar."""
|
||||
self.run(text, count, safely=True)
|
||||
macros.macro_recorder.record_command(text, count)
|
||||
|
|
|
|||
|
|
@ -25,14 +25,15 @@ subclasses to provide completions.
|
|||
|
||||
import typing
|
||||
|
||||
|
||||
from PyQt5.QtWidgets import QTreeView, QSizePolicy, QStyleFactory, QWidget
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, QSize
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.completion import completiondelegate
|
||||
from qutebrowser.utils import utils, usertypes, debug, log, objreg
|
||||
from qutebrowser.utils import utils, usertypes, debug, log
|
||||
from qutebrowser.api import cmdutils
|
||||
if typing.TYPE_CHECKING:
|
||||
from qutebrowser.mainwindow.statusbar import command
|
||||
|
||||
|
||||
class CompletionView(QTreeView):
|
||||
|
|
@ -50,6 +51,7 @@ class CompletionView(QTreeView):
|
|||
_delegate: The item delegate used.
|
||||
_column_widths: A list of column widths, in percent.
|
||||
_active: Whether a selection is active.
|
||||
_cmd: The statusbar Command object.
|
||||
|
||||
Signals:
|
||||
update_geometry: Emitted when the completion should be resized.
|
||||
|
|
@ -108,14 +110,18 @@ class CompletionView(QTreeView):
|
|||
update_geometry = pyqtSignal()
|
||||
selection_changed = pyqtSignal(str)
|
||||
|
||||
def __init__(self, win_id: int, parent: QWidget = None) -> None:
|
||||
def __init__(self, *,
|
||||
cmd: 'command.Command',
|
||||
win_id: int,
|
||||
parent: QWidget = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.pattern = None # type: typing.Optional[str]
|
||||
self._win_id = win_id
|
||||
config.instance.changed.connect(self._on_config_changed)
|
||||
|
||||
self._cmd = cmd
|
||||
self._active = False
|
||||
|
||||
config.instance.changed.connect(self._on_config_changed)
|
||||
|
||||
self._delegate = completiondelegate.CompletionItemDelegate(self)
|
||||
self.setItemDelegate(self._delegate)
|
||||
self.setStyle(QStyleFactory.create('Fusion'))
|
||||
|
|
@ -243,15 +249,13 @@ class CompletionView(QTreeView):
|
|||
history: Navigate through command history if no text was typed.
|
||||
"""
|
||||
if history:
|
||||
status = objreg.get('status-command', scope='window',
|
||||
window=self._win_id)
|
||||
if (status.text() == ':' or status.history.is_browsing() or
|
||||
if (self._cmd.text() == ':' or self._cmd.history.is_browsing() or
|
||||
not self._active):
|
||||
if which == 'next':
|
||||
status.command_history_next()
|
||||
self._cmd.command_history_next()
|
||||
return
|
||||
elif which == 'prev':
|
||||
status.command_history_prev()
|
||||
self._cmd.command_history_prev()
|
||||
return
|
||||
else:
|
||||
raise cmdutils.CommandError("Can't combine --history with "
|
||||
|
|
@ -412,9 +416,7 @@ class CompletionView(QTreeView):
|
|||
Args:
|
||||
sel: Use the primary selection instead of the clipboard.
|
||||
"""
|
||||
status = objreg.get('status-command', scope='window',
|
||||
window=self._win_id)
|
||||
text = status.selectedText()
|
||||
text = self._cmd.selectedText()
|
||||
if not text:
|
||||
index = self.currentIndex()
|
||||
if not index.isValid():
|
||||
|
|
|
|||
|
|
@ -85,10 +85,11 @@ def bookmark(*, info=None): # pylint: disable=unused-argument
|
|||
|
||||
def session(*, info=None): # pylint: disable=unused-argument
|
||||
"""A CompletionModel filled with session names."""
|
||||
from qutebrowser.misc import sessions
|
||||
model = completionmodel.CompletionModel()
|
||||
try:
|
||||
manager = objreg.get('session-manager')
|
||||
sessions = ((name,) for name in manager.list_sessions()
|
||||
sessions = ((name,) for name
|
||||
in sessions.session_manager.list_sessions()
|
||||
if not name.startswith('_'))
|
||||
model.add_category(listcategory.ListCategory("Sessions", sessions))
|
||||
except OSError:
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
import typing
|
||||
|
||||
from qutebrowser.utils import objreg, usertypes
|
||||
from qutebrowser.utils import usertypes
|
||||
from qutebrowser.misc import objects
|
||||
|
||||
|
||||
|
|
@ -43,7 +43,7 @@ def get_cmd_completions(info, include_hidden, include_aliases, prefix=''):
|
|||
cmdlist = []
|
||||
cmd_to_keys = info.keyconf.get_reverse_bindings_for('normal')
|
||||
for obj in set(objects.commands.values()):
|
||||
hide_debug = obj.debug and not objreg.get('args').debug
|
||||
hide_debug = obj.debug and not objects.args.debug
|
||||
hide_mode = (usertypes.KeyMode.normal not in obj.modes and
|
||||
not include_hidden)
|
||||
if not (hide_debug or hide_mode or obj.deprecated):
|
||||
|
|
|
|||
|
|
@ -1789,6 +1789,14 @@ tabs.focus_stack_size:
|
|||
zero_ok: false
|
||||
desc: Maximum stack size to remember for tab switches (-1 for no maximum).
|
||||
|
||||
tabs.tooltips:
|
||||
default: true
|
||||
type: Bool
|
||||
desc: >-
|
||||
Show tooltips on tabs.
|
||||
|
||||
Note this setting only affects windows opened after it has been set.
|
||||
|
||||
## url
|
||||
|
||||
url.auto_search:
|
||||
|
|
@ -2768,7 +2776,7 @@ bindings.default:
|
|||
tIu: config-cycle -p -u {url} content.images ;; reload
|
||||
insert:
|
||||
<Ctrl-E>: open-editor
|
||||
<Shift-Ins>: insert-text {primary}
|
||||
<Shift-Ins>: insert-text -- {primary}
|
||||
<Escape>: leave-mode
|
||||
hint:
|
||||
<Return>: follow-hint
|
||||
|
|
|
|||
|
|
@ -141,11 +141,13 @@ class ConfigFileErrors(Error):
|
|||
|
||||
def __init__(self,
|
||||
basename: str,
|
||||
errors: typing.Sequence[ConfigErrorDesc]) -> None:
|
||||
errors: typing.Sequence[ConfigErrorDesc], *,
|
||||
fatal: bool = False) -> None:
|
||||
super().__init__("Errors occurred while reading {}:\n{}".format(
|
||||
basename, '\n'.join(' {}'.format(e) for e in errors)))
|
||||
self.basename = basename
|
||||
self.errors = errors
|
||||
self.fatal = fatal
|
||||
for err in errors:
|
||||
if err.traceback:
|
||||
log.config.debug("Config error stack:")
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ class StateConfig(configparser.ConfigParser):
|
|||
super().__init__()
|
||||
self._filename = os.path.join(standarddir.data(), 'state')
|
||||
self.read(self._filename, encoding='utf-8')
|
||||
|
||||
qt_version = qVersion()
|
||||
# We handle this here, so we can avoid setting qt_version_changed if
|
||||
# the config is brand new, but can still set it when qt_version wasn't
|
||||
|
|
@ -159,7 +158,7 @@ class YamlConfig(QObject):
|
|||
# Instead, create a config.py - see :help for details.
|
||||
|
||||
""".lstrip('\n')))
|
||||
utils.yaml_dump(data, f)
|
||||
utils.yaml_dump(data, f) # type: ignore
|
||||
|
||||
def _pop_object(self,
|
||||
yaml_data: typing.Any,
|
||||
|
|
@ -441,11 +440,21 @@ class ConfigAPI:
|
|||
urlpattern = urlmatch.UrlPattern(pattern) if pattern else None
|
||||
self._config.set_obj(name, value, pattern=urlpattern)
|
||||
|
||||
def bind(self, key: str, command: str, mode: str = 'normal') -> None:
|
||||
def bind(self, key: str,
|
||||
command: typing.Optional[str],
|
||||
mode: str = 'normal') -> None:
|
||||
"""Bind a key to a command, with an optional key mode."""
|
||||
with self._handle_error('binding', key):
|
||||
seq = keyutils.KeySequence.parse(key)
|
||||
self._keyconfig.bind(seq, command, mode=mode)
|
||||
if command is None:
|
||||
text = ("Unbinding commands with config.bind('{key}', None) "
|
||||
"is deprecated. Use config.unbind('{key}') instead."
|
||||
.format(key=key))
|
||||
self.errors.append(configexc.ConfigErrorDesc(
|
||||
"While unbinding '{}'".format(key), text))
|
||||
self._keyconfig.unbind(seq, mode=mode)
|
||||
else:
|
||||
self._keyconfig.bind(seq, command, mode=mode)
|
||||
|
||||
def unbind(self, key: str, mode: str = 'normal') -> None:
|
||||
"""Unbind a key from a command, with an optional key mode."""
|
||||
|
|
@ -679,7 +688,13 @@ def saved_sys_properties() -> typing.Iterator[None]:
|
|||
def init() -> None:
|
||||
"""Initialize config storage not related to the main config."""
|
||||
global state
|
||||
state = StateConfig()
|
||||
|
||||
try:
|
||||
state = StateConfig()
|
||||
except configparser.Error as e:
|
||||
msg = "While loading state file from {}".format(standarddir.data())
|
||||
desc = configexc.ConfigErrorDesc(msg, e)
|
||||
raise configexc.ConfigFileErrors('state', [desc], fatal=True)
|
||||
|
||||
# Set the QSettings path to something like
|
||||
# ~/.config/qutebrowser/qsettings/qutebrowser/qutebrowser.conf so it
|
||||
|
|
|
|||
|
|
@ -57,9 +57,10 @@ def early_init(args: argparse.Namespace) -> None:
|
|||
|
||||
config_commands = configcommands.ConfigCommands(
|
||||
config.instance, config.key_instance)
|
||||
objreg.register('config-commands', config_commands)
|
||||
objreg.register('config-commands', config_commands, command_only=True)
|
||||
|
||||
config_file = standarddir.config_py()
|
||||
global _init_errors
|
||||
|
||||
try:
|
||||
if os.path.exists(config_file):
|
||||
|
|
@ -68,10 +69,12 @@ def early_init(args: argparse.Namespace) -> None:
|
|||
configfiles.read_autoconfig()
|
||||
except configexc.ConfigFileErrors as e:
|
||||
log.config.exception("Error while loading {}".format(e.basename))
|
||||
global _init_errors
|
||||
_init_errors = e
|
||||
|
||||
configfiles.init()
|
||||
try:
|
||||
configfiles.init()
|
||||
except configexc.ConfigFileErrors as e:
|
||||
_init_errors = e
|
||||
|
||||
for opt, val in args.temp_settings:
|
||||
try:
|
||||
|
|
@ -149,6 +152,10 @@ def late_init(save_manager: savemanager.SaveManager) -> None:
|
|||
icon=QMessageBox.Warning,
|
||||
plain_text=False)
|
||||
errbox.exec_()
|
||||
|
||||
if _init_errors.fatal:
|
||||
sys.exit(usertypes.Exit.err_init)
|
||||
|
||||
_init_errors = None
|
||||
|
||||
config.instance.init_save_manager(save_manager)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
import typing
|
||||
import argparse
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtCore import QUrl, pyqtSlot
|
||||
from PyQt5.QtGui import QFont
|
||||
|
||||
from qutebrowser.config import config, configutils
|
||||
|
|
@ -198,6 +198,7 @@ def init(args: argparse.Namespace) -> None:
|
|||
pattern=urlmatch.UrlPattern(pattern))
|
||||
|
||||
|
||||
@pyqtSlot()
|
||||
def shutdown() -> None:
|
||||
"""Shut down QWeb(Engine)Settings."""
|
||||
if objects.backend == usertypes.Backend.QtWebEngine:
|
||||
|
|
|
|||
|
|
@ -32,7 +32,8 @@ from PyQt5.QtCore import pyqtSlot
|
|||
|
||||
from qutebrowser import components
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import log, standarddir, objreg
|
||||
from qutebrowser.utils import log, standarddir
|
||||
from qutebrowser.misc import objects
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
import argparse
|
||||
|
|
@ -137,7 +138,7 @@ def _get_init_context() -> InitContext:
|
|||
"""Get an InitContext object."""
|
||||
return InitContext(data_dir=pathlib.Path(standarddir.data()),
|
||||
config_dir=pathlib.Path(standarddir.config()),
|
||||
args=objreg.get('args'))
|
||||
args=objects.args)
|
||||
|
||||
|
||||
def _load_component(info: ExtensionInfo, *,
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ class MatchResult:
|
|||
command = attr.ib() # type: typing.Optional[str]
|
||||
sequence = attr.ib() # type: keyutils.KeySequence
|
||||
|
||||
def __attrs_post_init__(self):
|
||||
def __attrs_post_init__(self) -> None:
|
||||
if self.match_type == QKeySequence.ExactMatch:
|
||||
assert self.command is not None
|
||||
else:
|
||||
|
|
@ -151,13 +151,13 @@ class BaseKeyParser(QObject):
|
|||
do_log: Whether to log keypresses or not.
|
||||
passthrough: Whether unbound keys should be passed through with this
|
||||
handler.
|
||||
supports_count: Whether count is supported.
|
||||
|
||||
Attributes:
|
||||
bindings: Bound key bindings
|
||||
_win_id: The window ID this keyparser is associated with.
|
||||
_sequence: The currently entered key sequence
|
||||
_modename: The name of the input mode associated with this keyparser.
|
||||
_supports_count: Whether count is supported
|
||||
|
||||
Signals:
|
||||
keystring_updated: Emitted when the keystring is updated.
|
||||
|
|
@ -172,20 +172,19 @@ class BaseKeyParser(QObject):
|
|||
request_leave = pyqtSignal(usertypes.KeyMode, str, bool)
|
||||
do_log = True
|
||||
passthrough = False
|
||||
supports_count = True
|
||||
|
||||
def __init__(self, win_id: int, parent: QWidget = None,
|
||||
supports_count: bool = True) -> None:
|
||||
def __init__(self, win_id: int, parent: QWidget = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._win_id = win_id
|
||||
self._modename = None
|
||||
self._sequence = keyutils.KeySequence()
|
||||
self._count = ''
|
||||
self._supports_count = supports_count
|
||||
self.bindings = BindingTrie()
|
||||
config.instance.changed.connect(self._on_config_changed)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return utils.get_repr(self, supports_count=self._supports_count)
|
||||
return utils.get_repr(self)
|
||||
|
||||
def _debug_log(self, message: str) -> None:
|
||||
"""Log a message to the debug log if logging is active.
|
||||
|
|
@ -237,7 +236,7 @@ class BaseKeyParser(QObject):
|
|||
dry_run: bool) -> bool:
|
||||
"""Try to match a key as count."""
|
||||
txt = str(sequence[-1]) # To account for sequences changed above.
|
||||
if (txt in string.digits and self._supports_count and
|
||||
if (txt in string.digits and self.supports_count and
|
||||
not (not self._count and txt == '0')):
|
||||
self._debug_log("Trying match as count")
|
||||
assert len(txt) == 1, txt
|
||||
|
|
@ -281,6 +280,8 @@ class BaseKeyParser(QObject):
|
|||
return QKeySequence.NoMatch
|
||||
|
||||
result = self._match_key(sequence)
|
||||
del sequence # Enforce code below to use the modified result.sequence
|
||||
|
||||
if result.match_type == QKeySequence.NoMatch:
|
||||
result = self._match_without_modifiers(result.sequence)
|
||||
if result.match_type == QKeySequence.NoMatch:
|
||||
|
|
@ -305,7 +306,7 @@ class BaseKeyParser(QObject):
|
|||
elif result.match_type == QKeySequence.PartialMatch:
|
||||
self._debug_log("No match for '{}' (added {})".format(
|
||||
result.sequence, txt))
|
||||
self.keystring_updated.emit(self._count + str(sequence))
|
||||
self.keystring_updated.emit(self._count + str(result.sequence))
|
||||
elif result.match_type == QKeySequence.NoMatch:
|
||||
self._debug_log("Giving up with '{}', no matches".format(
|
||||
result.sequence))
|
||||
|
|
|
|||
|
|
@ -103,18 +103,18 @@ def _build_special_names() -> typing.Mapping[Qt.Key, str]:
|
|||
'Dead_Hook': 'Hook',
|
||||
'Dead_Horn': 'Horn',
|
||||
|
||||
'Dead_Stroke': '̵',
|
||||
'Dead_Abovecomma': '̓',
|
||||
'Dead_Abovereversedcomma': '̔',
|
||||
'Dead_Doublegrave': '̏',
|
||||
'Dead_Belowring': '̥',
|
||||
'Dead_Belowmacron': '̱',
|
||||
'Dead_Belowcircumflex': '̭',
|
||||
'Dead_Belowtilde': '̰',
|
||||
'Dead_Belowbreve': '̮',
|
||||
'Dead_Belowdiaeresis': '̤',
|
||||
'Dead_Invertedbreve': '̑',
|
||||
'Dead_Belowcomma': '̦',
|
||||
'Dead_Stroke': '\u0335', # '̵'
|
||||
'Dead_Abovecomma': '\u0313', # '̓'
|
||||
'Dead_Abovereversedcomma': '\u0314', # '̔'
|
||||
'Dead_Doublegrave': '\u030f', # '̏'
|
||||
'Dead_Belowring': '\u0325', # '̥'
|
||||
'Dead_Belowmacron': '\u0331', # '̱'
|
||||
'Dead_Belowcircumflex': '\u032d', # '̭'
|
||||
'Dead_Belowtilde': '\u0330', # '̰'
|
||||
'Dead_Belowbreve': '\u032e', # '̮'
|
||||
'Dead_Belowdiaeresis': '\u0324', # '̤'
|
||||
'Dead_Invertedbreve': '\u0311', # '̑'
|
||||
'Dead_Belowcomma': '\u0326', # '̦'
|
||||
'Dead_Currency': '¤',
|
||||
'Dead_a': 'a',
|
||||
'Dead_A': 'A',
|
||||
|
|
@ -129,10 +129,10 @@ def _build_special_names() -> typing.Mapping[Qt.Key, str]:
|
|||
'Dead_Small_Schwa': 'ə',
|
||||
'Dead_Capital_Schwa': 'Ə',
|
||||
'Dead_Greek': 'Greek',
|
||||
'Dead_Lowline': '̲',
|
||||
'Dead_Aboveverticalline': '̍',
|
||||
'Dead_Lowline': '\u0332', # '̲'
|
||||
'Dead_Aboveverticalline': '\u030d', # '̍'
|
||||
'Dead_Belowverticalline': '\u0329',
|
||||
'Dead_Longsolidusoverlay': '̸',
|
||||
'Dead_Longsolidusoverlay': '\u0338', # '̸'
|
||||
|
||||
'Memo': 'Memo',
|
||||
'ToDoList': 'To Do List',
|
||||
|
|
@ -554,7 +554,9 @@ class KeySequence:
|
|||
def __getitem__(self, item: slice) -> 'KeySequence':
|
||||
...
|
||||
|
||||
def __getitem__(self, item):
|
||||
def __getitem__(
|
||||
self, item: typing.Union[int, slice]
|
||||
) -> typing.Union[KeyInfo, 'KeySequence']:
|
||||
if isinstance(item, slice):
|
||||
keys = list(self._iter_keys())
|
||||
return self.__class__(*keys[item])
|
||||
|
|
|
|||
|
|
@ -20,12 +20,19 @@
|
|||
|
||||
"""Keyboard macro system."""
|
||||
|
||||
import typing
|
||||
|
||||
from qutebrowser.commands import runners
|
||||
from qutebrowser.api import cmdutils
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.utils import message, objreg, usertypes
|
||||
|
||||
|
||||
_CommandType = typing.Tuple[str, int] # command, type
|
||||
|
||||
macro_recorder = typing.cast('MacroRecorder', None)
|
||||
|
||||
|
||||
class MacroRecorder:
|
||||
|
||||
"""An object for recording and running keyboard macros.
|
||||
|
|
@ -39,15 +46,15 @@ class MacroRecorder:
|
|||
_last_register: The macro which did run last.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._macros = {}
|
||||
self._recording_macro = None
|
||||
self._macro_count = {}
|
||||
self._last_register = None
|
||||
def __init__(self) -> None:
|
||||
self._macros = {} # type: typing.Dict[str, typing.List[_CommandType]]
|
||||
self._recording_macro = None # type: typing.Optional[str]
|
||||
self._macro_count = {} # type: typing.Dict[int, int]
|
||||
self._last_register = None # type: typing.Optional[str]
|
||||
|
||||
@cmdutils.register(instance='macro-recorder', name='record-macro')
|
||||
@cmdutils.argument('win_id', value=cmdutils.Value.win_id)
|
||||
def record_macro_command(self, win_id, register=None):
|
||||
def record_macro_command(self, win_id: int, register: str = None) -> None:
|
||||
"""Start or stop recording a macro.
|
||||
|
||||
Args:
|
||||
|
|
@ -64,7 +71,7 @@ class MacroRecorder:
|
|||
message.info("Macro '{}' recorded.".format(self._recording_macro))
|
||||
self._recording_macro = None
|
||||
|
||||
def record_macro(self, register):
|
||||
def record_macro(self, register: str) -> None:
|
||||
"""Start recording a macro."""
|
||||
message.info("Recording macro '{}'...".format(register))
|
||||
self._macros[register] = []
|
||||
|
|
@ -73,7 +80,9 @@ class MacroRecorder:
|
|||
@cmdutils.register(instance='macro-recorder', name='run-macro')
|
||||
@cmdutils.argument('win_id', value=cmdutils.Value.win_id)
|
||||
@cmdutils.argument('count', value=cmdutils.Value.count)
|
||||
def run_macro_command(self, win_id, count=1, register=None):
|
||||
def run_macro_command(self, win_id: int,
|
||||
count: int = 1,
|
||||
register: str = None) -> None:
|
||||
"""Run a recorded macro.
|
||||
|
||||
Args:
|
||||
|
|
@ -87,7 +96,7 @@ class MacroRecorder:
|
|||
else:
|
||||
self.run_macro(win_id, register)
|
||||
|
||||
def run_macro(self, win_id, register):
|
||||
def run_macro(self, win_id: int, register: str) -> None:
|
||||
"""Run a recorded macro."""
|
||||
if register == '@':
|
||||
if self._last_register is None:
|
||||
|
|
@ -104,13 +113,14 @@ class MacroRecorder:
|
|||
for cmd in self._macros[register]:
|
||||
commandrunner.run_safely(*cmd)
|
||||
|
||||
def record_command(self, text, count):
|
||||
def record_command(self, text: str, count: int) -> None:
|
||||
"""Record a command if a macro is being recorded."""
|
||||
if self._recording_macro is not None:
|
||||
self._macros[self._recording_macro].append((text, count))
|
||||
|
||||
|
||||
def init():
|
||||
def init() -> None:
|
||||
"""Initialize the MacroRecorder."""
|
||||
global macro_recorder
|
||||
macro_recorder = MacroRecorder()
|
||||
objreg.register('macro-recorder', macro_recorder)
|
||||
objreg.register('macro-recorder', macro_recorder, command_only=True)
|
||||
|
|
|
|||
|
|
@ -20,19 +20,26 @@
|
|||
"""Mode manager singleton which handles the current keyboard mode."""
|
||||
|
||||
import functools
|
||||
import typing
|
||||
|
||||
import attr
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QObject, QEvent
|
||||
from PyQt5.QtGui import QKeyEvent
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
from qutebrowser.keyinput import modeparsers
|
||||
from qutebrowser.commands import runners
|
||||
from qutebrowser.keyinput import modeparsers, basekeyparser
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.api import cmdutils
|
||||
from qutebrowser.utils import usertypes, log, objreg, utils
|
||||
from qutebrowser.browser import hints
|
||||
|
||||
INPUT_MODES = [usertypes.KeyMode.insert, usertypes.KeyMode.passthrough]
|
||||
PROMPT_MODES = [usertypes.KeyMode.prompt, usertypes.KeyMode.yesno]
|
||||
|
||||
_ParserDictType = typing.MutableMapping[
|
||||
usertypes.KeyMode, basekeyparser.BaseKeyParser]
|
||||
|
||||
|
||||
@attr.s(frozen=True)
|
||||
class KeyEvent:
|
||||
|
|
@ -48,13 +55,13 @@ class KeyEvent:
|
|||
text: A string (QKeyEvent::text).
|
||||
"""
|
||||
|
||||
key = attr.ib()
|
||||
text = attr.ib()
|
||||
key = attr.ib() # type: Qt.Key
|
||||
text = attr.ib() # type: str
|
||||
|
||||
@classmethod
|
||||
def from_event(cls, event):
|
||||
def from_event(cls, event: QKeyEvent) -> 'KeyEvent':
|
||||
"""Initialize a KeyEvent from a QKeyEvent."""
|
||||
return cls(event.key(), event.text())
|
||||
return cls(Qt.Key(event.key()), event.text())
|
||||
|
||||
|
||||
class NotInModeError(Exception):
|
||||
|
|
@ -62,57 +69,129 @@ class NotInModeError(Exception):
|
|||
"""Exception raised when we want to leave a mode we're not in."""
|
||||
|
||||
|
||||
def init(win_id, parent):
|
||||
def init(win_id: int, parent: QObject) -> 'ModeManager':
|
||||
"""Initialize the mode manager and the keyparsers for the given win_id."""
|
||||
KM = usertypes.KeyMode # noqa: N806
|
||||
modeman = ModeManager(win_id, parent)
|
||||
objreg.register('mode-manager', modeman, scope='window', window=win_id)
|
||||
|
||||
commandrunner = runners.CommandRunner(win_id)
|
||||
|
||||
hintmanager = hints.HintManager(win_id, parent=parent)
|
||||
objreg.register('hintmanager', hintmanager, scope='window',
|
||||
window=win_id, command_only=True)
|
||||
|
||||
keyparsers = {
|
||||
KM.normal:
|
||||
modeparsers.NormalKeyParser(win_id, modeman),
|
||||
KM.hint:
|
||||
modeparsers.HintKeyParser(win_id, modeman),
|
||||
KM.insert:
|
||||
modeparsers.PassthroughKeyParser(win_id, 'insert', modeman),
|
||||
KM.passthrough:
|
||||
modeparsers.PassthroughKeyParser(win_id, 'passthrough', modeman),
|
||||
KM.command:
|
||||
modeparsers.PassthroughKeyParser(win_id, 'command', modeman),
|
||||
KM.prompt:
|
||||
modeparsers.PassthroughKeyParser(win_id, 'prompt', modeman),
|
||||
KM.yesno:
|
||||
modeparsers.PromptKeyParser(win_id, modeman),
|
||||
KM.caret:
|
||||
modeparsers.CaretKeyParser(win_id, modeman),
|
||||
KM.set_mark:
|
||||
modeparsers.RegisterKeyParser(win_id, KM.set_mark, modeman),
|
||||
KM.jump_mark:
|
||||
modeparsers.RegisterKeyParser(win_id, KM.jump_mark, modeman),
|
||||
KM.record_macro:
|
||||
modeparsers.RegisterKeyParser(win_id, KM.record_macro, modeman),
|
||||
KM.run_macro:
|
||||
modeparsers.RegisterKeyParser(win_id, KM.run_macro, modeman),
|
||||
}
|
||||
usertypes.KeyMode.normal:
|
||||
modeparsers.NormalKeyParser(
|
||||
win_id=win_id,
|
||||
commandrunner=commandrunner,
|
||||
parent=modeman),
|
||||
|
||||
usertypes.KeyMode.hint:
|
||||
modeparsers.HintKeyParser(
|
||||
win_id=win_id,
|
||||
commandrunner=commandrunner,
|
||||
hintmanager=hintmanager,
|
||||
parent=modeman),
|
||||
|
||||
usertypes.KeyMode.insert:
|
||||
modeparsers.PassthroughKeyParser(
|
||||
win_id=win_id,
|
||||
mode=usertypes.KeyMode.insert,
|
||||
commandrunner=commandrunner,
|
||||
parent=modeman),
|
||||
|
||||
usertypes.KeyMode.passthrough:
|
||||
modeparsers.PassthroughKeyParser(
|
||||
win_id=win_id,
|
||||
mode=usertypes.KeyMode.passthrough,
|
||||
commandrunner=commandrunner,
|
||||
parent=modeman),
|
||||
|
||||
usertypes.KeyMode.command:
|
||||
modeparsers.PassthroughKeyParser(
|
||||
win_id=win_id,
|
||||
mode=usertypes.KeyMode.command,
|
||||
commandrunner=commandrunner,
|
||||
parent=modeman),
|
||||
|
||||
usertypes.KeyMode.prompt:
|
||||
modeparsers.PassthroughKeyParser(
|
||||
win_id=win_id,
|
||||
mode=usertypes.KeyMode.prompt,
|
||||
commandrunner=commandrunner,
|
||||
parent=modeman),
|
||||
|
||||
usertypes.KeyMode.yesno:
|
||||
modeparsers.PromptKeyParser(
|
||||
win_id=win_id,
|
||||
commandrunner=commandrunner,
|
||||
parent=modeman),
|
||||
|
||||
usertypes.KeyMode.caret:
|
||||
modeparsers.CaretKeyParser(
|
||||
win_id=win_id,
|
||||
commandrunner=commandrunner,
|
||||
parent=modeman),
|
||||
|
||||
usertypes.KeyMode.set_mark:
|
||||
modeparsers.RegisterKeyParser(
|
||||
win_id=win_id,
|
||||
mode=usertypes.KeyMode.set_mark,
|
||||
commandrunner=commandrunner,
|
||||
parent=modeman),
|
||||
|
||||
usertypes.KeyMode.jump_mark:
|
||||
modeparsers.RegisterKeyParser(
|
||||
win_id=win_id,
|
||||
mode=usertypes.KeyMode.jump_mark,
|
||||
commandrunner=commandrunner,
|
||||
parent=modeman),
|
||||
|
||||
usertypes.KeyMode.record_macro:
|
||||
modeparsers.RegisterKeyParser(
|
||||
win_id=win_id,
|
||||
mode=usertypes.KeyMode.record_macro,
|
||||
commandrunner=commandrunner,
|
||||
parent=modeman),
|
||||
|
||||
usertypes.KeyMode.run_macro:
|
||||
modeparsers.RegisterKeyParser(
|
||||
win_id=win_id,
|
||||
mode=usertypes.KeyMode.run_macro,
|
||||
commandrunner=commandrunner,
|
||||
parent=modeman),
|
||||
} # type: _ParserDictType
|
||||
|
||||
objreg.register('keyparsers', keyparsers, scope='window', window=win_id)
|
||||
modeman.destroyed.connect(
|
||||
|
||||
modeman.destroyed.connect( # type: ignore
|
||||
functools.partial(objreg.delete, 'keyparsers', scope='window',
|
||||
window=win_id))
|
||||
|
||||
for mode, parser in keyparsers.items():
|
||||
modeman.register(mode, parser)
|
||||
|
||||
return modeman
|
||||
|
||||
|
||||
def instance(win_id):
|
||||
def instance(win_id: int) -> 'ModeManager':
|
||||
"""Get a modemanager object."""
|
||||
return objreg.get('mode-manager', scope='window', window=win_id)
|
||||
|
||||
|
||||
def enter(win_id, mode, reason=None, only_if_normal=False):
|
||||
def enter(win_id: int,
|
||||
mode: usertypes.KeyMode,
|
||||
reason: str = None,
|
||||
only_if_normal: bool = False) -> None:
|
||||
"""Enter the mode 'mode'."""
|
||||
instance(win_id).enter(mode, reason, only_if_normal)
|
||||
|
||||
|
||||
def leave(win_id, mode, reason=None, *, maybe=False):
|
||||
def leave(win_id: int,
|
||||
mode: usertypes.KeyMode,
|
||||
reason: str = None, *,
|
||||
maybe: bool = False) -> None:
|
||||
"""Leave the mode 'mode'."""
|
||||
instance(win_id).leave(mode, reason, maybe=maybe)
|
||||
|
||||
|
|
@ -144,18 +223,19 @@ class ModeManager(QObject):
|
|||
entered = pyqtSignal(usertypes.KeyMode, int)
|
||||
left = pyqtSignal(usertypes.KeyMode, usertypes.KeyMode, int)
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
def __init__(self, win_id: int, parent: QObject = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._win_id = win_id
|
||||
self._parsers = {}
|
||||
self._parsers = {} # type: _ParserDictType
|
||||
self._prev_mode = usertypes.KeyMode.normal
|
||||
self.mode = usertypes.KeyMode.normal
|
||||
self._releaseevents_to_pass = set()
|
||||
self._releaseevents_to_pass = set() # type: typing.Set[KeyEvent]
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return utils.get_repr(self, mode=self.mode)
|
||||
|
||||
def _handle_keypress(self, event, *, dry_run=False):
|
||||
def _handle_keypress(self, event: QKeyEvent, *,
|
||||
dry_run: bool = False) -> bool:
|
||||
"""Handle filtering of KeyPress events.
|
||||
|
||||
Args:
|
||||
|
|
@ -199,7 +279,7 @@ class ModeManager(QObject):
|
|||
filter_this, focus_widget))
|
||||
return filter_this
|
||||
|
||||
def _handle_keyrelease(self, event):
|
||||
def _handle_keyrelease(self, event: QKeyEvent) -> bool:
|
||||
"""Handle filtering of KeyRelease events.
|
||||
|
||||
Args:
|
||||
|
|
@ -219,19 +299,16 @@ class ModeManager(QObject):
|
|||
log.modes.debug("filter: {}".format(filter_this))
|
||||
return filter_this
|
||||
|
||||
def register(self, mode, parser):
|
||||
"""Register a new mode.
|
||||
|
||||
Args:
|
||||
mode: The name of the mode.
|
||||
parser: The KeyParser which should be used.
|
||||
"""
|
||||
assert isinstance(mode, usertypes.KeyMode)
|
||||
def register(self, mode: usertypes.KeyMode,
|
||||
parser: basekeyparser.BaseKeyParser) -> None:
|
||||
"""Register a new mode."""
|
||||
assert parser is not None
|
||||
self._parsers[mode] = parser
|
||||
parser.request_leave.connect(self.leave)
|
||||
|
||||
def enter(self, mode, reason=None, only_if_normal=False):
|
||||
def enter(self, mode: usertypes.KeyMode,
|
||||
reason: str = None,
|
||||
only_if_normal: bool = False) -> None:
|
||||
"""Enter a new mode.
|
||||
|
||||
Args:
|
||||
|
|
@ -239,9 +316,6 @@ class ModeManager(QObject):
|
|||
reason: Why the mode was entered.
|
||||
only_if_normal: Only enter the new mode if we're in normal mode.
|
||||
"""
|
||||
if not isinstance(mode, usertypes.KeyMode):
|
||||
raise TypeError("Mode {} is no KeyMode member!".format(mode))
|
||||
|
||||
if mode == usertypes.KeyMode.normal:
|
||||
self.leave(self.mode, reason='enter normal: {}'.format(reason))
|
||||
return
|
||||
|
|
@ -273,7 +347,7 @@ class ModeManager(QObject):
|
|||
self.entered.emit(mode, self._win_id)
|
||||
|
||||
@cmdutils.register(instance='mode-manager', scope='window')
|
||||
def enter_mode(self, mode):
|
||||
def enter_mode(self, mode: str) -> None:
|
||||
"""Enter a key mode.
|
||||
|
||||
Args:
|
||||
|
|
@ -294,7 +368,9 @@ class ModeManager(QObject):
|
|||
self.enter(m, 'command')
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode, str, bool)
|
||||
def leave(self, mode, reason=None, maybe=False):
|
||||
def leave(self, mode: usertypes.KeyMode,
|
||||
reason: str = None,
|
||||
maybe: bool = False) -> None:
|
||||
"""Leave a key mode.
|
||||
|
||||
Args:
|
||||
|
|
@ -324,13 +400,13 @@ class ModeManager(QObject):
|
|||
|
||||
@cmdutils.register(instance='mode-manager', name='leave-mode',
|
||||
not_modes=[usertypes.KeyMode.normal], scope='window')
|
||||
def leave_current_mode(self):
|
||||
def leave_current_mode(self) -> None:
|
||||
"""Leave the mode we're currently in."""
|
||||
if self.mode == usertypes.KeyMode.normal:
|
||||
raise ValueError("Can't leave normal mode!")
|
||||
self.leave(self.mode, 'leave current')
|
||||
|
||||
def handle_event(self, event):
|
||||
def handle_event(self, event: QEvent) -> bool:
|
||||
"""Filter all events based on the currently set mode.
|
||||
|
||||
Also calls the real keypress handler.
|
||||
|
|
@ -341,20 +417,16 @@ class ModeManager(QObject):
|
|||
Return:
|
||||
True if event should be filtered, False otherwise.
|
||||
"""
|
||||
if self.mode is None:
|
||||
# We got events before mode is set, so just pass them through.
|
||||
return False
|
||||
|
||||
handlers = {
|
||||
QEvent.KeyPress: self._handle_keypress,
|
||||
QEvent.KeyRelease: self._handle_keyrelease,
|
||||
QEvent.ShortcutOverride:
|
||||
functools.partial(self._handle_keypress, dry_run=True),
|
||||
}
|
||||
} # type: typing.Mapping[QEvent.Type, typing.Callable[[QEvent], bool]]
|
||||
handler = handlers[event.type()]
|
||||
return handler(event)
|
||||
|
||||
@cmdutils.register(instance='mode-manager', scope='window')
|
||||
def clear_keychain(self):
|
||||
def clear_keychain(self) -> None:
|
||||
"""Clear the currently entered key chain."""
|
||||
self._parsers[self.mode].clear_keystring()
|
||||
|
|
|
|||
|
|
@ -23,16 +23,20 @@ Module attributes:
|
|||
STARTCHARS: Possible chars for starting a commandline input.
|
||||
"""
|
||||
|
||||
import typing
|
||||
import traceback
|
||||
import enum
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, Qt
|
||||
from PyQt5.QtGui import QKeySequence
|
||||
from PyQt5.QtCore import pyqtSlot, Qt, QObject
|
||||
from PyQt5.QtGui import QKeySequence, QKeyEvent
|
||||
|
||||
from qutebrowser.commands import runners, cmdexc
|
||||
from qutebrowser.browser import hints
|
||||
from qutebrowser.commands import cmdexc
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.keyinput import basekeyparser, keyutils
|
||||
from qutebrowser.keyinput import basekeyparser, keyutils, macros
|
||||
from qutebrowser.utils import usertypes, log, message, objreg, utils
|
||||
if typing.TYPE_CHECKING:
|
||||
from qutebrowser.commands import runners
|
||||
|
||||
|
||||
STARTCHARS = ":/?"
|
||||
|
|
@ -47,11 +51,13 @@ class CommandKeyParser(basekeyparser.BaseKeyParser):
|
|||
_commandrunner: CommandRunner instance.
|
||||
"""
|
||||
|
||||
def __init__(self, win_id, parent=None, supports_count=None):
|
||||
super().__init__(win_id, parent, supports_count)
|
||||
self._commandrunner = runners.CommandRunner(win_id)
|
||||
def __init__(self, win_id: int,
|
||||
commandrunner: 'runners.CommandRunner',
|
||||
parent: QObject = None) -> None:
|
||||
super().__init__(win_id, parent)
|
||||
self._commandrunner = commandrunner
|
||||
|
||||
def execute(self, cmdstr, count=None):
|
||||
def execute(self, cmdstr: str, count: int = None) -> None:
|
||||
try:
|
||||
self._commandrunner.run(cmdstr, count)
|
||||
except cmdexc.Error as e:
|
||||
|
|
@ -66,8 +72,10 @@ class NormalKeyParser(CommandKeyParser):
|
|||
_partial_timer: Timer to clear partial keypresses.
|
||||
"""
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(win_id, parent, supports_count=True)
|
||||
def __init__(self, win_id: int,
|
||||
commandrunner: 'runners.CommandRunner',
|
||||
parent: QObject = None) -> None:
|
||||
super().__init__(win_id, commandrunner, parent)
|
||||
self._read_config('normal')
|
||||
self._partial_timer = usertypes.Timer(self, 'partial-match')
|
||||
self._partial_timer.setSingleShot(True)
|
||||
|
|
@ -76,20 +84,12 @@ class NormalKeyParser(CommandKeyParser):
|
|||
self._inhibited_timer = usertypes.Timer(self, 'normal-inhibited')
|
||||
self._inhibited_timer.setSingleShot(True)
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return utils.get_repr(self)
|
||||
|
||||
def handle(self, e, *, dry_run=False):
|
||||
"""Override to abort if the key is a startchar.
|
||||
|
||||
Args:
|
||||
e: the KeyPressEvent from Qt.
|
||||
dry_run: Don't actually execute anything, only check whether there
|
||||
would be a match.
|
||||
|
||||
Return:
|
||||
A self.Match member.
|
||||
"""
|
||||
def handle(self, e: QKeyEvent, *,
|
||||
dry_run: bool = False) -> QKeySequence.SequenceMatch:
|
||||
"""Override to abort if the key is a startchar."""
|
||||
txt = e.text().strip()
|
||||
if self._inhibited:
|
||||
self._debug_log("Ignoring key '{}', because the normal mode is "
|
||||
|
|
@ -105,7 +105,7 @@ class NormalKeyParser(CommandKeyParser):
|
|||
self._partial_timer.start()
|
||||
return match
|
||||
|
||||
def set_inhibited_timeout(self, timeout):
|
||||
def set_inhibited_timeout(self, timeout: int) -> None:
|
||||
"""Ignore keypresses for the given duration."""
|
||||
if timeout != 0:
|
||||
self._debug_log("Inhibiting the normal mode for {}ms.".format(
|
||||
|
|
@ -116,7 +116,7 @@ class NormalKeyParser(CommandKeyParser):
|
|||
self._inhibited_timer.start()
|
||||
|
||||
@pyqtSlot()
|
||||
def _clear_partial_match(self):
|
||||
def _clear_partial_match(self) -> None:
|
||||
"""Clear a partial keystring after a timeout."""
|
||||
self._debug_log("Clearing partial keystring {}".format(
|
||||
self._sequence))
|
||||
|
|
@ -124,7 +124,7 @@ class NormalKeyParser(CommandKeyParser):
|
|||
self.keystring_updated.emit(str(self._sequence))
|
||||
|
||||
@pyqtSlot()
|
||||
def _clear_inhibited(self):
|
||||
def _clear_inhibited(self) -> None:
|
||||
"""Reset inhibition state after a timeout."""
|
||||
self._debug_log("Releasing inhibition state of normal mode.")
|
||||
self._inhibited = False
|
||||
|
|
@ -142,8 +142,12 @@ class PassthroughKeyParser(CommandKeyParser):
|
|||
|
||||
do_log = False
|
||||
passthrough = True
|
||||
supports_count = False
|
||||
|
||||
def __init__(self, win_id, mode, parent=None):
|
||||
def __init__(self, win_id: int,
|
||||
mode: usertypes.KeyMode,
|
||||
commandrunner: 'runners.CommandRunner',
|
||||
parent: QObject = None) -> None:
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
|
|
@ -151,11 +155,11 @@ class PassthroughKeyParser(CommandKeyParser):
|
|||
parent: Qt parent.
|
||||
warn: Whether to warn if an ignored key was bound.
|
||||
"""
|
||||
super().__init__(win_id, parent)
|
||||
self._read_config(mode)
|
||||
super().__init__(win_id, commandrunner, parent)
|
||||
self._read_config(mode.name)
|
||||
self._mode = mode
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return utils.get_repr(self, mode=self._mode)
|
||||
|
||||
|
||||
|
|
@ -163,11 +167,15 @@ class PromptKeyParser(CommandKeyParser):
|
|||
|
||||
"""KeyParser for yes/no prompts."""
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(win_id, parent, supports_count=False)
|
||||
supports_count = False
|
||||
|
||||
def __init__(self, win_id: int,
|
||||
commandrunner: 'runners.CommandRunner',
|
||||
parent: QObject = None) -> None:
|
||||
super().__init__(win_id, commandrunner, parent)
|
||||
self._read_config('yesno')
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return utils.get_repr(self)
|
||||
|
||||
|
||||
|
|
@ -177,31 +185,27 @@ class HintKeyParser(CommandKeyParser):
|
|||
|
||||
Attributes:
|
||||
_filtertext: The text to filter with.
|
||||
_hintmanager: The HintManager to use.
|
||||
_last_press: The nature of the last keypress, a LastPress member.
|
||||
"""
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(win_id, parent, supports_count=False)
|
||||
supports_count = False
|
||||
|
||||
def __init__(self, win_id: int,
|
||||
commandrunner: 'runners.CommandRunner',
|
||||
hintmanager: hints.HintManager,
|
||||
parent: QObject = None) -> None:
|
||||
super().__init__(win_id, commandrunner, parent)
|
||||
self._hintmanager = hintmanager
|
||||
self._filtertext = ''
|
||||
self._last_press = LastPress.none
|
||||
self._read_config('hint')
|
||||
self.keystring_updated.connect(self.on_keystring_updated)
|
||||
self.keystring_updated.connect(self._hintmanager.handle_partial_key)
|
||||
|
||||
def _handle_filter_key(self, e):
|
||||
"""Handle keys for string filtering.
|
||||
|
||||
Return True if the keypress has been handled, and False if not.
|
||||
|
||||
Args:
|
||||
e: the KeyPressEvent from Qt.
|
||||
|
||||
Return:
|
||||
A QKeySequence match.
|
||||
"""
|
||||
def _handle_filter_key(self, e: QKeyEvent) -> QKeySequence.SequenceMatch:
|
||||
"""Handle keys for string filtering."""
|
||||
log.keyboard.debug("Got filter key 0x{:x} text {}".format(
|
||||
e.key(), e.text()))
|
||||
hintmanager = objreg.get('hintmanager', scope='tab',
|
||||
window=self._win_id, tab='current')
|
||||
if e.key() == Qt.Key_Backspace:
|
||||
log.keyboard.debug("Got backspace, mode {}, filtertext '{}', "
|
||||
"sequence '{}'".format(self._last_press,
|
||||
|
|
@ -209,7 +213,7 @@ class HintKeyParser(CommandKeyParser):
|
|||
self._sequence))
|
||||
if self._last_press != LastPress.keystring and self._filtertext:
|
||||
self._filtertext = self._filtertext[:-1]
|
||||
hintmanager.filter_hints(self._filtertext)
|
||||
self._hintmanager.filter_hints(self._filtertext)
|
||||
return QKeySequence.ExactMatch
|
||||
elif self._last_press == LastPress.keystring and self._sequence:
|
||||
self._sequence = self._sequence[:-1]
|
||||
|
|
@ -217,36 +221,28 @@ class HintKeyParser(CommandKeyParser):
|
|||
if not self._sequence and self._filtertext:
|
||||
# Switch back to hint filtering mode (this can happen only
|
||||
# in numeric mode after the number has been deleted).
|
||||
hintmanager.filter_hints(self._filtertext)
|
||||
self._hintmanager.filter_hints(self._filtertext)
|
||||
self._last_press = LastPress.filtertext
|
||||
return QKeySequence.ExactMatch
|
||||
else:
|
||||
return QKeySequence.NoMatch
|
||||
elif hintmanager.current_mode() != 'number':
|
||||
elif self._hintmanager.current_mode() != 'number':
|
||||
return QKeySequence.NoMatch
|
||||
elif not e.text():
|
||||
return QKeySequence.NoMatch
|
||||
else:
|
||||
self._filtertext += e.text()
|
||||
hintmanager.filter_hints(self._filtertext)
|
||||
self._hintmanager.filter_hints(self._filtertext)
|
||||
self._last_press = LastPress.filtertext
|
||||
return QKeySequence.ExactMatch
|
||||
|
||||
def handle(self, e, *, dry_run=False):
|
||||
"""Handle a new keypress and call the respective handlers.
|
||||
|
||||
Args:
|
||||
e: the KeyPressEvent from Qt
|
||||
dry_run: Don't actually execute anything, only check whether there
|
||||
would be a match.
|
||||
|
||||
Returns:
|
||||
True if the match has been handled, False otherwise.
|
||||
"""
|
||||
def handle(self, e: QKeyEvent, *,
|
||||
dry_run: bool = False) -> QKeySequence.SequenceMatch:
|
||||
"""Handle a new keypress and call the respective handlers."""
|
||||
if dry_run:
|
||||
return super().handle(e, dry_run=True)
|
||||
|
||||
if keyutils.is_special_hint_mode(e.key(), e.modifiers()):
|
||||
if keyutils.is_special_hint_mode(Qt.Key(e.key()), e.modifiers()):
|
||||
log.keyboard.debug("Got special key, clearing keychain")
|
||||
self.clear_keystring()
|
||||
|
||||
|
|
@ -265,7 +261,8 @@ class HintKeyParser(CommandKeyParser):
|
|||
|
||||
return match
|
||||
|
||||
def update_bindings(self, strings, preserve_filter=False):
|
||||
def update_bindings(self, strings: typing.Sequence[str],
|
||||
preserve_filter: bool = False) -> None:
|
||||
"""Update bindings when the hint strings changed.
|
||||
|
||||
Args:
|
||||
|
|
@ -279,13 +276,6 @@ class HintKeyParser(CommandKeyParser):
|
|||
if not preserve_filter:
|
||||
self._filtertext = ''
|
||||
|
||||
@pyqtSlot(str)
|
||||
def on_keystring_updated(self, keystr):
|
||||
"""Update hintmanager when the keystring was updated."""
|
||||
hintmanager = objreg.get('hintmanager', scope='tab',
|
||||
window=self._win_id, tab='current')
|
||||
hintmanager.handle_partial_key(keystr)
|
||||
|
||||
|
||||
class CaretKeyParser(CommandKeyParser):
|
||||
|
||||
|
|
@ -293,8 +283,10 @@ class CaretKeyParser(CommandKeyParser):
|
|||
|
||||
passthrough = True
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(win_id, parent, supports_count=True)
|
||||
def __init__(self, win_id: int,
|
||||
commandrunner: 'runners.CommandRunner',
|
||||
parent: QObject = None) -> None:
|
||||
super().__init__(win_id, commandrunner, parent)
|
||||
self._read_config('caret')
|
||||
|
||||
|
||||
|
|
@ -307,27 +299,24 @@ class RegisterKeyParser(CommandKeyParser):
|
|||
and KeyMode.run_macro.
|
||||
"""
|
||||
|
||||
def __init__(self, win_id, mode, parent=None):
|
||||
super().__init__(win_id, parent, supports_count=False)
|
||||
supports_count = False
|
||||
|
||||
def __init__(self, win_id: int,
|
||||
mode: usertypes.KeyMode,
|
||||
commandrunner: 'runners.CommandRunner',
|
||||
parent: QObject = None) -> None:
|
||||
super().__init__(win_id, commandrunner, parent)
|
||||
self._mode = mode
|
||||
self._read_config('register')
|
||||
|
||||
def handle(self, e, *, dry_run=False):
|
||||
"""Override handle to always match the next key and use the register.
|
||||
|
||||
Args:
|
||||
e: the KeyPressEvent from Qt.
|
||||
dry_run: Don't actually execute anything, only check whether there
|
||||
would be a match.
|
||||
|
||||
Return:
|
||||
True if event has been handled, False otherwise.
|
||||
"""
|
||||
def handle(self, e: QKeyEvent, *,
|
||||
dry_run: bool = False) -> QKeySequence.SequenceMatch:
|
||||
"""Override to always match the next key and use the register."""
|
||||
match = super().handle(e, dry_run=dry_run)
|
||||
if match or dry_run:
|
||||
return match
|
||||
|
||||
if keyutils.is_special(e.key(), e.modifiers()):
|
||||
if keyutils.is_special(Qt.Key(e.key()), e.modifiers()):
|
||||
# this is not a proper register key, let it pass and keep going
|
||||
return QKeySequence.NoMatch
|
||||
|
||||
|
|
@ -335,7 +324,6 @@ class RegisterKeyParser(CommandKeyParser):
|
|||
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._win_id)
|
||||
macro_recorder = objreg.get('macro-recorder')
|
||||
|
||||
try:
|
||||
if self._mode == usertypes.KeyMode.set_mark:
|
||||
|
|
@ -343,9 +331,9 @@ class RegisterKeyParser(CommandKeyParser):
|
|||
elif self._mode == usertypes.KeyMode.jump_mark:
|
||||
tabbed_browser.jump_mark(key)
|
||||
elif self._mode == usertypes.KeyMode.record_macro:
|
||||
macro_recorder.record_macro(key)
|
||||
macros.macro_recorder.record_macro(key)
|
||||
elif self._mode == usertypes.KeyMode.run_macro:
|
||||
macro_recorder.run_macro(self._win_id, key)
|
||||
macros.macro_recorder.run_macro(self._win_id, key)
|
||||
else:
|
||||
raise ValueError(
|
||||
"{} is not a valid register mode".format(self._mode))
|
||||
|
|
|
|||
|
|
@ -37,7 +37,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
|
||||
from qutebrowser.misc import crashsignal, keyhintwidget, sessions
|
||||
|
||||
|
||||
win_id_gen = itertools.count(0)
|
||||
|
|
@ -139,6 +139,7 @@ class MainWindow(QWidget):
|
|||
tabbed_browser: The TabbedBrowser widget.
|
||||
state_before_fullscreen: window state before activation of fullscreen.
|
||||
_downloadview: The DownloadView widget.
|
||||
_download_model: The DownloadModel instance.
|
||||
_vbox: The main QVBoxLayout.
|
||||
_commandrunner: The main CommandRunner instance.
|
||||
_overlays: Widgets shown as overlay for the current webpage.
|
||||
|
|
@ -193,7 +194,8 @@ class MainWindow(QWidget):
|
|||
self._vbox.setSpacing(0)
|
||||
|
||||
self._init_downloadmanager()
|
||||
self._downloadview = downloadview.DownloadView(self.win_id)
|
||||
self._downloadview = downloadview.DownloadView(
|
||||
model=self._download_model)
|
||||
|
||||
if config.val.content.private_browsing:
|
||||
# This setting always trumps what's passed in.
|
||||
|
|
@ -220,7 +222,7 @@ class MainWindow(QWidget):
|
|||
self._init_completion()
|
||||
|
||||
log.init.debug("Initializing modes...")
|
||||
modeman.init(self.win_id, self)
|
||||
modeman.init(win_id=self.win_id, parent=self)
|
||||
|
||||
self._commandrunner = runners.CommandRunner(self.win_id,
|
||||
partial_match=True)
|
||||
|
|
@ -233,7 +235,7 @@ class MainWindow(QWidget):
|
|||
self._prompt_container.update_geometry,
|
||||
centered=True, padding=10)
|
||||
objreg.register('prompt-container', self._prompt_container,
|
||||
scope='window', window=self.win_id)
|
||||
scope='window', window=self.win_id, command_only=True)
|
||||
self._prompt_container.hide()
|
||||
|
||||
self._messageview = messageview.MessageView(parent=self)
|
||||
|
|
@ -248,7 +250,7 @@ class MainWindow(QWidget):
|
|||
QTimer.singleShot(0, self._connect_overlay_signals)
|
||||
config.instance.changed.connect(self._on_config_changed)
|
||||
|
||||
objreg.get("app").new_window.emit(self)
|
||||
QApplication.instance().new_window.emit(self)
|
||||
self._set_decoration(config.val.window.hide_decoration)
|
||||
|
||||
self.state_before_fullscreen = self.windowState()
|
||||
|
|
@ -329,27 +331,32 @@ class MainWindow(QWidget):
|
|||
except KeyError:
|
||||
webengine_download_manager = None
|
||||
|
||||
download_model = downloads.DownloadModel(qtnetwork_download_manager,
|
||||
webengine_download_manager)
|
||||
objreg.register('download-model', download_model, scope='window',
|
||||
window=self.win_id)
|
||||
self._download_model = downloads.DownloadModel(
|
||||
qtnetwork_download_manager, webengine_download_manager)
|
||||
objreg.register('download-model', self._download_model,
|
||||
scope='window', window=self.win_id,
|
||||
command_only=True)
|
||||
|
||||
def _init_completion(self):
|
||||
self._completion = completionwidget.CompletionView(self.win_id, self)
|
||||
cmd = objreg.get('status-command', scope='window', window=self.win_id)
|
||||
completer_obj = completer.Completer(cmd=cmd, win_id=self.win_id,
|
||||
self._completion = completionwidget.CompletionView(cmd=self.status.cmd,
|
||||
win_id=self.win_id,
|
||||
parent=self)
|
||||
completer_obj = completer.Completer(cmd=self.status.cmd,
|
||||
win_id=self.win_id,
|
||||
parent=self._completion)
|
||||
self._completion.selection_changed.connect(
|
||||
completer_obj.on_selection_changed)
|
||||
objreg.register('completion', self._completion, scope='window',
|
||||
window=self.win_id)
|
||||
window=self.win_id, command_only=True)
|
||||
self._add_overlay(self._completion, self._completion.update_geometry)
|
||||
|
||||
def _init_command_dispatcher(self):
|
||||
dispatcher = commands.CommandDispatcher(self.win_id,
|
||||
self.tabbed_browser)
|
||||
objreg.register('command-dispatcher', dispatcher, scope='window',
|
||||
window=self.win_id)
|
||||
self._command_dispatcher = commands.CommandDispatcher(
|
||||
self.win_id, self.tabbed_browser)
|
||||
objreg.register('command-dispatcher',
|
||||
self._command_dispatcher,
|
||||
command_only=True,
|
||||
scope='window', window=self.win_id)
|
||||
self.tabbed_browser.widget.destroyed.connect(
|
||||
functools.partial(objreg.delete, 'command-dispatcher',
|
||||
scope='window', window=self.win_id))
|
||||
|
|
@ -445,10 +452,7 @@ class MainWindow(QWidget):
|
|||
|
||||
def _connect_signals(self):
|
||||
"""Connect all mainwindow signals."""
|
||||
status = self._get_object('statusbar')
|
||||
keyparsers = self._get_object('keyparsers')
|
||||
completion_obj = self._get_object('completion')
|
||||
cmd = self._get_object('status-command')
|
||||
message_bridge = self._get_object('message-bridge')
|
||||
mode_manager = self._get_object('mode-manager')
|
||||
|
||||
|
|
@ -457,17 +461,22 @@ class MainWindow(QWidget):
|
|||
mode_manager.entered.connect(hints.on_mode_entered)
|
||||
|
||||
# status bar
|
||||
mode_manager.entered.connect(status.on_mode_entered)
|
||||
mode_manager.left.connect(status.on_mode_left)
|
||||
mode_manager.left.connect(cmd.on_mode_left)
|
||||
mode_manager.entered.connect(self.status.on_mode_entered)
|
||||
mode_manager.left.connect(self.status.on_mode_left)
|
||||
mode_manager.left.connect(self.status.cmd.on_mode_left)
|
||||
mode_manager.left.connect(message.global_bridge.mode_left)
|
||||
|
||||
# commands
|
||||
keyparsers[usertypes.KeyMode.normal].keystring_updated.connect(
|
||||
status.keystring.setText)
|
||||
cmd.got_cmd[str].connect(self._commandrunner.run_safely)
|
||||
cmd.got_cmd[str, int].connect(self._commandrunner.run_safely)
|
||||
cmd.returnPressed.connect(self.tabbed_browser.on_cmd_return_pressed)
|
||||
self.status.keystring.setText)
|
||||
self.status.cmd.got_cmd[str].connect(
|
||||
self._commandrunner.run_safely)
|
||||
self.status.cmd.got_cmd[str, int].connect(
|
||||
self._commandrunner.run_safely)
|
||||
self.status.cmd.returnPressed.connect(
|
||||
self.tabbed_browser.on_cmd_return_pressed)
|
||||
self.status.cmd.got_search.connect(
|
||||
self._command_dispatcher.search)
|
||||
|
||||
# key hint popup
|
||||
for mode, parser in keyparsers.items():
|
||||
|
|
@ -481,42 +490,51 @@ class MainWindow(QWidget):
|
|||
message.global_bridge.clear_messages.connect(
|
||||
self._messageview.clear_messages)
|
||||
|
||||
message_bridge.s_set_text.connect(status.set_text)
|
||||
message_bridge.s_maybe_reset_text.connect(status.txt.maybe_reset_text)
|
||||
message_bridge.s_set_text.connect(self.status.set_text)
|
||||
message_bridge.s_maybe_reset_text.connect(
|
||||
self.status.txt.maybe_reset_text)
|
||||
|
||||
# statusbar
|
||||
self.tabbed_browser.current_tab_changed.connect(status.on_tab_changed)
|
||||
self.tabbed_browser.current_tab_changed.connect(
|
||||
self.status.on_tab_changed)
|
||||
|
||||
self.tabbed_browser.cur_progress.connect(status.prog.on_load_progress)
|
||||
self.tabbed_browser.cur_progress.connect(
|
||||
self.status.prog.on_load_progress)
|
||||
self.tabbed_browser.cur_load_started.connect(
|
||||
status.prog.on_load_started)
|
||||
self.status.prog.on_load_started)
|
||||
|
||||
self.tabbed_browser.cur_scroll_perc_changed.connect(
|
||||
status.percentage.set_perc)
|
||||
self.status.percentage.set_perc)
|
||||
self.tabbed_browser.widget.tab_index_changed.connect(
|
||||
status.tabindex.on_tab_index_changed)
|
||||
self.status.tabindex.on_tab_index_changed)
|
||||
|
||||
self.tabbed_browser.cur_url_changed.connect(status.url.set_url)
|
||||
self.tabbed_browser.cur_url_changed.connect(
|
||||
self.status.url.set_url)
|
||||
self.tabbed_browser.cur_url_changed.connect(functools.partial(
|
||||
status.backforward.on_tab_cur_url_changed,
|
||||
self.status.backforward.on_tab_cur_url_changed,
|
||||
tabs=self.tabbed_browser))
|
||||
self.tabbed_browser.cur_link_hovered.connect(status.url.set_hover_url)
|
||||
self.tabbed_browser.cur_link_hovered.connect(
|
||||
self.status.url.set_hover_url)
|
||||
self.tabbed_browser.cur_load_status_changed.connect(
|
||||
status.url.on_load_status_changed)
|
||||
self.status.url.on_load_status_changed)
|
||||
|
||||
self.tabbed_browser.cur_caret_selection_toggled.connect(
|
||||
status.on_caret_selection_toggled)
|
||||
self.status.on_caret_selection_toggled)
|
||||
|
||||
self.tabbed_browser.cur_fullscreen_requested.connect(
|
||||
self._on_fullscreen_requested)
|
||||
self.tabbed_browser.cur_fullscreen_requested.connect(status.maybe_hide)
|
||||
self.tabbed_browser.cur_fullscreen_requested.connect(
|
||||
self.status.maybe_hide)
|
||||
|
||||
# command input / completion
|
||||
mode_manager.entered.connect(self.tabbed_browser.on_mode_entered)
|
||||
mode_manager.left.connect(self.tabbed_browser.on_mode_left)
|
||||
cmd.clear_completion_selection.connect(
|
||||
completion_obj.on_clear_completion_selection)
|
||||
cmd.hide_completion.connect(completion_obj.hide)
|
||||
mode_manager.entered.connect(
|
||||
self.tabbed_browser.on_mode_entered)
|
||||
mode_manager.left.connect(
|
||||
self.tabbed_browser.on_mode_left)
|
||||
self.status.cmd.clear_completion_selection.connect(
|
||||
self._completion.on_clear_completion_selection)
|
||||
self.status.cmd.hide_completion.connect(
|
||||
self._completion.hide)
|
||||
|
||||
def _set_decoration(self, hidden):
|
||||
"""Set the visibility of the window decoration via Qt."""
|
||||
|
|
@ -579,7 +597,7 @@ class MainWindow(QWidget):
|
|||
objreg.delete('last-visible-main-window')
|
||||
except KeyError:
|
||||
pass
|
||||
objreg.get('session-manager').save_last_window_session()
|
||||
sessions.session_manager.save_last_window_session()
|
||||
self._save_geometry()
|
||||
log.destroy.debug("Closing window {}".format(self.win_id))
|
||||
self.tabbed_browser.shutdown()
|
||||
|
|
@ -590,9 +608,7 @@ class MainWindow(QWidget):
|
|||
e.accept()
|
||||
return
|
||||
tab_count = self.tabbed_browser.widget.count()
|
||||
download_model = objreg.get('download-model', scope='window',
|
||||
window=self.win_id)
|
||||
download_count = download_model.running_downloads()
|
||||
download_count = self._download_model.running_downloads()
|
||||
quit_texts = []
|
||||
# Ask if multiple-tabs are open
|
||||
if 'multiple-tabs' in config.val.confirm_quit and tab_count > 1:
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import os.path
|
|||
import html
|
||||
import collections
|
||||
import functools
|
||||
import typing
|
||||
|
||||
import attr
|
||||
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelIndex,
|
||||
|
|
@ -39,7 +40,7 @@ from qutebrowser.api import cmdutils
|
|||
from qutebrowser.utils import urlmatch
|
||||
|
||||
|
||||
prompt_queue = None
|
||||
prompt_queue = typing.cast('PromptQueue', None)
|
||||
|
||||
|
||||
@attr.s
|
||||
|
|
@ -949,6 +950,5 @@ def init():
|
|||
"""Initialize global prompt objects."""
|
||||
global prompt_queue
|
||||
prompt_queue = PromptQueue()
|
||||
objreg.register('prompt-queue', prompt_queue) # for commands
|
||||
message.global_bridge.ask_question.connect(
|
||||
prompt_queue.ask_question, Qt.DirectConnection)
|
||||
|
|
|
|||
|
|
@ -157,7 +157,6 @@ class StatusBar(QWidget):
|
|||
|
||||
def __init__(self, *, win_id, private, parent=None):
|
||||
super().__init__(parent)
|
||||
objreg.register('statusbar', self, scope='window', window=win_id)
|
||||
self.setObjectName(self.__class__.__name__)
|
||||
self.setAttribute(Qt.WA_StyledBackground)
|
||||
config.set_register_stylesheet(self)
|
||||
|
|
|
|||
|
|
@ -19,10 +19,10 @@
|
|||
|
||||
"""The commandline in the statusbar."""
|
||||
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QSize
|
||||
from PyQt5.QtWidgets import QSizePolicy
|
||||
from PyQt5.QtGui import QKeyEvent
|
||||
from PyQt5.QtWidgets import QSizePolicy, QWidget
|
||||
|
||||
from qutebrowser.keyinput import modeman, modeparsers
|
||||
from qutebrowser.api import cmdutils
|
||||
|
|
@ -42,6 +42,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
|||
Signals:
|
||||
got_cmd: Emitted when a command is triggered by the user.
|
||||
arg: The command string and also potentially the count.
|
||||
got_search: Emitted when a search should happen.
|
||||
clear_completion_selection: Emitted before the completion widget is
|
||||
hidden.
|
||||
hide_completion: Emitted when the completion widget should be hidden.
|
||||
|
|
@ -51,13 +52,16 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
|||
"""
|
||||
|
||||
got_cmd = pyqtSignal([str], [str, int])
|
||||
got_search = pyqtSignal(str, bool) # text, reverse
|
||||
clear_completion_selection = pyqtSignal()
|
||||
hide_completion = pyqtSignal()
|
||||
update_completion = pyqtSignal()
|
||||
show_cmd = pyqtSignal()
|
||||
hide_cmd = pyqtSignal()
|
||||
|
||||
def __init__(self, *, win_id, private, parent=None):
|
||||
def __init__(self, *, win_id: int,
|
||||
private: bool,
|
||||
parent: QWidget = None) -> None:
|
||||
misc.CommandLineEdit.__init__(self, parent=parent)
|
||||
misc.MinimalLineEditMixin.__init__(self)
|
||||
self._win_id = win_id
|
||||
|
|
@ -66,32 +70,29 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
|||
self.history.history = command_history.data
|
||||
self.history.changed.connect(command_history.changed)
|
||||
self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Ignored)
|
||||
self.cursorPositionChanged.connect(self.update_completion)
|
||||
self.textChanged.connect(self.update_completion)
|
||||
|
||||
self.cursorPositionChanged.connect(
|
||||
self.update_completion) # type: ignore
|
||||
self.textChanged.connect(self.update_completion) # type: ignore
|
||||
self.textChanged.connect(self.updateGeometry)
|
||||
self.textChanged.connect(self._incremental_search)
|
||||
|
||||
self._command_dispatcher = objreg.get(
|
||||
'command-dispatcher', scope='window', window=self._win_id)
|
||||
|
||||
def _handle_search(self):
|
||||
def _handle_search(self) -> bool:
|
||||
"""Check if the currently entered text is a search, and if so, run it.
|
||||
|
||||
Return:
|
||||
True if a search was executed, False otherwise.
|
||||
"""
|
||||
search_prefixes = {
|
||||
'/': self._command_dispatcher.search,
|
||||
'?': functools.partial(
|
||||
self._command_dispatcher.search, reverse=True)
|
||||
}
|
||||
if self.prefix() in search_prefixes:
|
||||
search_fn = search_prefixes[self.prefix()]
|
||||
search_fn(self.text()[1:])
|
||||
if self.prefix() == '/':
|
||||
self.got_search.emit(self.text()[1:], False)
|
||||
return True
|
||||
return False
|
||||
elif self.prefix() == '?':
|
||||
self.got_search.emit(self.text()[1:], True)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def prefix(self):
|
||||
def prefix(self) -> str:
|
||||
"""Get the currently entered command prefix."""
|
||||
text = self.text()
|
||||
if not text:
|
||||
|
|
@ -101,7 +102,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
|||
else:
|
||||
return ''
|
||||
|
||||
def set_cmd_text(self, text):
|
||||
def set_cmd_text(self, text: str) -> None:
|
||||
"""Preset the statusbar to some text.
|
||||
|
||||
Args:
|
||||
|
|
@ -116,8 +117,11 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
|||
@cmdutils.register(instance='status-command', name='set-cmd-text',
|
||||
scope='window', maxsplit=0)
|
||||
@cmdutils.argument('count', value=cmdutils.Value.count)
|
||||
def set_cmd_text_command(self, text, count=None, space=False, append=False,
|
||||
run_on_count=False):
|
||||
def set_cmd_text_command(self, text: str,
|
||||
count: int = None,
|
||||
space: bool = False,
|
||||
append: bool = False,
|
||||
run_on_count: bool = False) -> None:
|
||||
"""Preset the statusbar to some text.
|
||||
|
||||
//
|
||||
|
|
@ -144,13 +148,13 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
|||
raise cmdutils.CommandError(
|
||||
"Invalid command text '{}'.".format(text))
|
||||
if run_on_count and count is not None:
|
||||
self.got_cmd[str, int].emit(text, count)
|
||||
self.got_cmd[str, int].emit(text, count) # type: ignore
|
||||
else:
|
||||
self.set_cmd_text(text)
|
||||
|
||||
@cmdutils.register(instance='status-command',
|
||||
modes=[usertypes.KeyMode.command], scope='window')
|
||||
def command_history_prev(self):
|
||||
def command_history_prev(self) -> None:
|
||||
"""Go back in the commandline history."""
|
||||
try:
|
||||
if not self.history.is_browsing():
|
||||
|
|
@ -165,7 +169,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
|||
|
||||
@cmdutils.register(instance='status-command',
|
||||
modes=[usertypes.KeyMode.command], scope='window')
|
||||
def command_history_next(self):
|
||||
def command_history_next(self) -> None:
|
||||
"""Go forward in the commandline history."""
|
||||
if not self.history.is_browsing():
|
||||
return
|
||||
|
|
@ -178,7 +182,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
|||
|
||||
@cmdutils.register(instance='status-command',
|
||||
modes=[usertypes.KeyMode.command], scope='window')
|
||||
def command_accept(self, rapid=False):
|
||||
def command_accept(self, rapid: bool = False) -> None:
|
||||
"""Execute the command currently in the commandline.
|
||||
|
||||
Args:
|
||||
|
|
@ -194,10 +198,10 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
|||
'cmd accept')
|
||||
|
||||
if not was_search:
|
||||
self.got_cmd[str].emit(text[1:])
|
||||
self.got_cmd[str].emit(text[1:]) # type: ignore
|
||||
|
||||
@cmdutils.register(instance='status-command', scope='window')
|
||||
def edit_command(self, run=False):
|
||||
def edit_command(self, run: bool = False) -> None:
|
||||
"""Open an editor to modify the current command.
|
||||
|
||||
Args:
|
||||
|
|
@ -205,7 +209,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
|||
"""
|
||||
ed = editor.ExternalEditor(parent=self)
|
||||
|
||||
def callback(text):
|
||||
def callback(text: str) -> None:
|
||||
"""Set the commandline to the edited text."""
|
||||
if not text or text[0] not in modeparsers.STARTCHARS:
|
||||
message.error('command must start with one of {}'
|
||||
|
|
@ -219,7 +223,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
|||
ed.edit(self.text())
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def on_mode_left(self, mode):
|
||||
def on_mode_left(self, mode: usertypes.KeyMode) -> None:
|
||||
"""Clear up when command mode was left.
|
||||
|
||||
- Clear the statusbar text if it's explicitly unfocused.
|
||||
|
|
@ -236,7 +240,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
|||
self.clear_completion_selection.emit()
|
||||
self.hide_completion.emit()
|
||||
|
||||
def setText(self, text):
|
||||
def setText(self, text: str) -> None:
|
||||
"""Extend setText to set prefix and make sure the prompt is ok."""
|
||||
if not text:
|
||||
pass
|
||||
|
|
@ -247,7 +251,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
|||
"'{}'!".format(text))
|
||||
super().setText(text)
|
||||
|
||||
def keyPressEvent(self, e):
|
||||
def keyPressEvent(self, e: QKeyEvent) -> None:
|
||||
"""Override keyPressEvent to ignore Return key presses.
|
||||
|
||||
If this widget is focused, we are in passthrough key mode, and
|
||||
|
|
@ -266,7 +270,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
|||
else:
|
||||
super().keyPressEvent(e)
|
||||
|
||||
def sizeHint(self):
|
||||
def sizeHint(self) -> QSize:
|
||||
"""Dynamically calculate the needed size."""
|
||||
height = super().sizeHint().height()
|
||||
text = self.text()
|
||||
|
|
@ -276,7 +280,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
|||
return QSize(width, height)
|
||||
|
||||
@pyqtSlot()
|
||||
def _incremental_search(self):
|
||||
def _incremental_search(self) -> None:
|
||||
if not config.val.search.incremental:
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -153,8 +153,9 @@ class TabWidget(QTabWidget):
|
|||
if tabbar.tabText(idx) != title:
|
||||
tabbar.setTabText(idx, title)
|
||||
|
||||
# always show only plain title in tooltips
|
||||
tabbar.setTabToolTip(idx, fields['current_title'])
|
||||
if config.cache['tabs.tooltips']:
|
||||
# always show only plain title in tooltips
|
||||
tabbar.setTabToolTip(idx, fields['current_title'])
|
||||
|
||||
def get_tab_fields(self, idx):
|
||||
"""Get the tab field data."""
|
||||
|
|
|
|||
|
|
@ -27,17 +27,21 @@ import ctypes
|
|||
import ctypes.util
|
||||
import enum
|
||||
import shutil
|
||||
import typing
|
||||
import argparse
|
||||
|
||||
import attr
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import (QApplication, QDialog, QPushButton, QHBoxLayout,
|
||||
QVBoxLayout, QLabel, QMessageBox)
|
||||
QVBoxLayout, QLabel, QMessageBox, QWidget)
|
||||
from PyQt5.QtNetwork import QSslSocket
|
||||
|
||||
from qutebrowser.config import config, configfiles
|
||||
from qutebrowser.utils import (usertypes, objreg, version, qtutils, log, utils,
|
||||
from qutebrowser.utils import (usertypes, version, qtutils, log, utils,
|
||||
standarddir)
|
||||
from qutebrowser.misc import objects, msgbox
|
||||
from qutebrowser.misc import objects, msgbox, savemanager
|
||||
if typing.TYPE_CHECKING:
|
||||
from qutebrowser import app
|
||||
|
||||
|
||||
class _Result(enum.IntEnum):
|
||||
|
|
@ -55,13 +59,15 @@ class _Button:
|
|||
|
||||
"""A button passed to BackendProblemDialog."""
|
||||
|
||||
text = attr.ib()
|
||||
setting = attr.ib()
|
||||
value = attr.ib()
|
||||
default = attr.ib(default=False)
|
||||
text = attr.ib() # type: str
|
||||
setting = attr.ib() # type: str
|
||||
value = attr.ib() # type: str
|
||||
default = attr.ib(default=False) # type: bool
|
||||
|
||||
|
||||
def _other_backend(backend):
|
||||
def _other_backend(
|
||||
backend: usertypes.Backend
|
||||
) -> typing.Tuple[usertypes.Backend, str]:
|
||||
"""Get the other backend enum/setting for a given backend."""
|
||||
other_backend = {
|
||||
usertypes.Backend.QtWebKit: usertypes.Backend.QtWebEngine,
|
||||
|
|
@ -71,7 +77,7 @@ def _other_backend(backend):
|
|||
return (other_backend, other_setting)
|
||||
|
||||
|
||||
def _error_text(because, text, backend):
|
||||
def _error_text(because: str, text: str, backend: usertypes.Backend) -> str:
|
||||
"""Get an error text for the given information."""
|
||||
other_backend, other_setting = _other_backend(backend)
|
||||
if other_backend == usertypes.Backend.QtWebKit:
|
||||
|
|
@ -96,14 +102,19 @@ class _Dialog(QDialog):
|
|||
|
||||
"""A dialog which gets shown if there are issues with the backend."""
|
||||
|
||||
def __init__(self, because, text, backend, buttons=None, parent=None):
|
||||
def __init__(self, *, because: str,
|
||||
text: str,
|
||||
backend: usertypes.Backend,
|
||||
buttons: typing.Sequence[_Button] = None,
|
||||
parent: QWidget = None) -> None:
|
||||
super().__init__(parent)
|
||||
vbox = QVBoxLayout(self)
|
||||
|
||||
other_backend, other_setting = _other_backend(backend)
|
||||
text = _error_text(because, text, backend)
|
||||
|
||||
label = QLabel(text, wordWrap=True)
|
||||
label = QLabel(text)
|
||||
label.setWordWrap(True)
|
||||
label.setTextFormat(Qt.RichText)
|
||||
vbox.addWidget(label)
|
||||
|
||||
|
|
@ -123,18 +134,17 @@ class _Dialog(QDialog):
|
|||
hbox.addWidget(backend_button)
|
||||
|
||||
for button in buttons:
|
||||
btn = QPushButton(button.text, default=button.default)
|
||||
btn = QPushButton(button.text)
|
||||
btn.setDefault(button.default)
|
||||
btn.clicked.connect(functools.partial(
|
||||
self._change_setting, button.setting, button.value))
|
||||
hbox.addWidget(btn)
|
||||
|
||||
vbox.addLayout(hbox)
|
||||
|
||||
def _change_setting(self, setting, value):
|
||||
def _change_setting(self, setting: str, value: str) -> None:
|
||||
"""Change the given setting and restart."""
|
||||
config.instance.set_obj(setting, value, save_yaml=True)
|
||||
save_manager = objreg.get('save-manager')
|
||||
save_manager.save_all(is_exit=True)
|
||||
|
||||
if setting == 'backend' and value == 'webkit':
|
||||
self.done(_Result.restart_webkit)
|
||||
|
|
@ -144,296 +154,318 @@ class _Dialog(QDialog):
|
|||
self.done(_Result.restart)
|
||||
|
||||
|
||||
def _show_dialog(*args, **kwargs):
|
||||
"""Show a dialog for a backend problem."""
|
||||
cmd_args = objreg.get('args')
|
||||
if cmd_args.no_err_windows:
|
||||
text = _error_text(*args, **kwargs)
|
||||
print(text, file=sys.stderr)
|
||||
sys.exit(usertypes.Exit.err_init)
|
||||
|
||||
dialog = _Dialog(*args, **kwargs)
|
||||
|
||||
status = dialog.exec_()
|
||||
quitter = objreg.get('quitter')
|
||||
|
||||
if status in [_Result.quit, QDialog.Rejected]:
|
||||
pass
|
||||
elif status == _Result.restart_webkit:
|
||||
quitter.restart(override_args={'backend': 'webkit'})
|
||||
elif status == _Result.restart_webengine:
|
||||
quitter.restart(override_args={'backend': 'webengine'})
|
||||
elif status == _Result.restart:
|
||||
quitter.restart()
|
||||
else:
|
||||
raise utils.Unreachable(status)
|
||||
|
||||
sys.exit(usertypes.Exit.err_init)
|
||||
|
||||
|
||||
def _nvidia_shader_workaround():
|
||||
"""Work around QOpenGLShaderProgram issues.
|
||||
|
||||
NOTE: This needs to be called before _handle_nouveau_graphics, or some
|
||||
setups will segfault in version.opengl_vendor().
|
||||
|
||||
See https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826
|
||||
"""
|
||||
assert objects.backend == usertypes.Backend.QtWebEngine, objects.backend
|
||||
|
||||
if os.environ.get('QUTE_SKIP_LIBGL_WORKAROUND'):
|
||||
return
|
||||
|
||||
libgl = ctypes.util.find_library("GL")
|
||||
if libgl is not None:
|
||||
ctypes.CDLL(libgl, mode=ctypes.RTLD_GLOBAL)
|
||||
|
||||
|
||||
def _handle_nouveau_graphics():
|
||||
"""Force software rendering when using the Nouveau driver.
|
||||
|
||||
WORKAROUND for https://bugreports.qt.io/browse/QTBUG-41242
|
||||
Should be fixed in Qt 5.10 via https://codereview.qt-project.org/#/c/208664/
|
||||
"""
|
||||
assert objects.backend == usertypes.Backend.QtWebEngine, objects.backend
|
||||
|
||||
if os.environ.get('QUTE_SKIP_NOUVEAU_CHECK'):
|
||||
return
|
||||
|
||||
if qtutils.version_check('5.10', compiled=False):
|
||||
return
|
||||
|
||||
if version.opengl_vendor() != 'nouveau':
|
||||
return
|
||||
|
||||
if (os.environ.get('LIBGL_ALWAYS_SOFTWARE') == '1' or
|
||||
# qt.force_software_rendering = 'software-opengl'
|
||||
'QT_XCB_FORCE_SOFTWARE_OPENGL' in os.environ or
|
||||
# qt.force_software_rendering = 'chromium', also see:
|
||||
# https://build.opensuse.org/package/view_file/openSUSE:Factory/libqt5-qtwebengine/disable-gpu-when-using-nouveau-boo-1005323.diff?expand=1
|
||||
'QT_WEBENGINE_DISABLE_NOUVEAU_WORKAROUND' in os.environ):
|
||||
return
|
||||
|
||||
button = _Button("Force software rendering", 'qt.force_software_rendering',
|
||||
'chromium')
|
||||
_show_dialog(
|
||||
backend=usertypes.Backend.QtWebEngine,
|
||||
because="you're using Nouveau graphics",
|
||||
text="<p>There are two ways to fix this:</p>"
|
||||
"<p><b>Forcing software rendering</b></p>"
|
||||
"<p>This allows you to use the newer QtWebEngine backend (based "
|
||||
"on Chromium) but could have noticeable performance impact "
|
||||
"(depending on your hardware). "
|
||||
"This sets the <i>qt.force_software_rendering = 'chromium'</i> "
|
||||
"option (if you have a <i>config.py</i> file, you'll need to set "
|
||||
"this manually).</p>",
|
||||
buttons=[button],
|
||||
)
|
||||
|
||||
raise utils.Unreachable
|
||||
|
||||
|
||||
def _handle_wayland():
|
||||
assert objects.backend == usertypes.Backend.QtWebEngine, objects.backend
|
||||
|
||||
if os.environ.get('QUTE_SKIP_WAYLAND_CHECK'):
|
||||
return
|
||||
|
||||
platform = QApplication.instance().platformName()
|
||||
if platform not in ['wayland', 'wayland-egl']:
|
||||
return
|
||||
|
||||
has_qt511 = qtutils.version_check('5.11', compiled=False)
|
||||
if has_qt511 and config.val.qt.force_software_rendering == 'chromium':
|
||||
return
|
||||
|
||||
if qtutils.version_check('5.11.2', compiled=False):
|
||||
return
|
||||
|
||||
buttons = []
|
||||
text = "<p>You can work around this in one of the following ways:</p>"
|
||||
|
||||
if 'DISPLAY' in os.environ:
|
||||
# XWayland is available, but QT_QPA_PLATFORM=wayland is set
|
||||
buttons.append(_Button("Force XWayland", 'qt.force_platform', 'xcb'))
|
||||
text += ("<p><b>Force Qt to use XWayland</b></p>"
|
||||
"<p>This allows you to use the newer QtWebEngine backend "
|
||||
"(based on Chromium). "
|
||||
"This sets the <i>qt.force_platform = 'xcb'</i> option "
|
||||
"(if you have a <i>config.py</i> file, you'll need to set "
|
||||
"this manually).</p>")
|
||||
else:
|
||||
text += ("<p><b>Set up XWayland</b></p>"
|
||||
"<p>This allows you to use the newer QtWebEngine backend "
|
||||
"(based on Chromium). ")
|
||||
|
||||
if has_qt511:
|
||||
buttons.append(_Button("Force software rendering",
|
||||
'qt.force_software_rendering',
|
||||
'chromium'))
|
||||
text += ("<p><b>Forcing software rendering</b></p>"
|
||||
"<p>This allows you to use the newer QtWebEngine backend "
|
||||
"(based on Chromium) but could have noticeable performance "
|
||||
"impact (depending on your hardware). This sets the "
|
||||
"<i>qt.force_software_rendering = 'chromium'</i> option "
|
||||
"(if you have a <i>config.py</i> file, you'll need to set "
|
||||
"this manually).</p>")
|
||||
|
||||
_show_dialog(backend=usertypes.Backend.QtWebEngine,
|
||||
because="you're using Wayland", text=text, buttons=buttons)
|
||||
|
||||
|
||||
@attr.s
|
||||
class BackendImports:
|
||||
class _BackendImports:
|
||||
|
||||
"""Whether backend modules could be imported."""
|
||||
|
||||
webkit_available = attr.ib(default=None)
|
||||
webengine_available = attr.ib(default=None)
|
||||
webkit_error = attr.ib(default=None)
|
||||
webengine_error = attr.ib(default=None)
|
||||
webkit_available = attr.ib(default=None) # type: bool
|
||||
webengine_available = attr.ib(default=None) # type: bool
|
||||
webkit_error = attr.ib(default=None) # type: str
|
||||
webengine_error = attr.ib(default=None) # type: str
|
||||
|
||||
|
||||
def _try_import_backends():
|
||||
"""Check whether backends can be imported and return BackendImports."""
|
||||
# pylint: disable=unused-import
|
||||
results = BackendImports()
|
||||
class _BackendProblemChecker:
|
||||
|
||||
try:
|
||||
from PyQt5 import QtWebKit
|
||||
from PyQt5 import QtWebKitWidgets
|
||||
except ImportError as e:
|
||||
results.webkit_available = False
|
||||
results.webkit_error = str(e)
|
||||
else:
|
||||
if qtutils.is_new_qtwebkit():
|
||||
results.webkit_available = True
|
||||
"""Check for various backend-specific issues."""
|
||||
|
||||
def __init__(self, *,
|
||||
quitter: 'app.Quitter',
|
||||
no_err_windows: bool,
|
||||
save_manager: savemanager.SaveManager) -> None:
|
||||
self._quitter = quitter
|
||||
self._save_manager = save_manager
|
||||
self._no_err_windows = no_err_windows
|
||||
|
||||
def _show_dialog(self, *args: typing.Any, **kwargs: typing.Any) -> None:
|
||||
"""Show a dialog for a backend problem."""
|
||||
if self._no_err_windows:
|
||||
text = _error_text(*args, **kwargs)
|
||||
print(text, file=sys.stderr)
|
||||
sys.exit(usertypes.Exit.err_init)
|
||||
|
||||
dialog = _Dialog(*args, **kwargs)
|
||||
|
||||
status = dialog.exec_()
|
||||
self._save_manager.save_all(is_exit=True)
|
||||
|
||||
if status in [_Result.quit, QDialog.Rejected]:
|
||||
pass
|
||||
elif status == _Result.restart_webkit:
|
||||
self._quitter.restart(override_args={'backend': 'webkit'})
|
||||
elif status == _Result.restart_webengine:
|
||||
self._quitter.restart(override_args={'backend': 'webengine'})
|
||||
elif status == _Result.restart:
|
||||
self._quitter.restart()
|
||||
else:
|
||||
results.webkit_available = False
|
||||
results.webkit_error = "Unsupported legacy QtWebKit found"
|
||||
raise utils.Unreachable(status)
|
||||
|
||||
try:
|
||||
from PyQt5 import QtWebEngineWidgets
|
||||
except ImportError 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
|
||||
|
||||
|
||||
def _handle_ssl_support(fatal=False):
|
||||
"""Check for full SSL availability.
|
||||
|
||||
If "fatal" is given, show an error and exit.
|
||||
"""
|
||||
text = ("Could not initialize QtNetwork SSL support. If you use "
|
||||
"OpenSSL 1.1 with a PyQt package from PyPI (e.g. on Archlinux "
|
||||
"or Debian Stretch), you need to set LD_LIBRARY_PATH to the path "
|
||||
"of OpenSSL 1.0. This only affects downloads.")
|
||||
|
||||
if QSslSocket.supportsSsl():
|
||||
return
|
||||
|
||||
if fatal:
|
||||
errbox = msgbox.msgbox(parent=None,
|
||||
title="SSL error",
|
||||
text="Could not initialize SSL support.",
|
||||
icon=QMessageBox.Critical,
|
||||
plain_text=False)
|
||||
errbox.exec_()
|
||||
sys.exit(usertypes.Exit.err_init)
|
||||
|
||||
assert not fatal
|
||||
log.init.warning(text)
|
||||
def _nvidia_shader_workaround(self) -> None:
|
||||
"""Work around QOpenGLShaderProgram issues.
|
||||
|
||||
NOTE: This needs to be called before _handle_nouveau_graphics, or some
|
||||
setups will segfault in version.opengl_vendor().
|
||||
|
||||
def _check_backend_modules():
|
||||
"""Check for the modules needed for QtWebKit/QtWebEngine."""
|
||||
imports = _try_import_backends()
|
||||
See https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826
|
||||
"""
|
||||
self._assert_backend(usertypes.Backend.QtWebEngine)
|
||||
|
||||
if imports.webkit_available and imports.webengine_available:
|
||||
return
|
||||
elif not imports.webkit_available and not imports.webengine_available:
|
||||
text = ("<p>qutebrowser needs QtWebKit or QtWebEngine, but neither "
|
||||
"could be imported!</p>"
|
||||
"<p>The errors encountered were:<ul>"
|
||||
"<li><b>QtWebKit:</b> {webkit_error}"
|
||||
"<li><b>QtWebEngine:</b> {webengine_error}"
|
||||
"</ul></p>".format(
|
||||
webkit_error=html.escape(imports.webkit_error),
|
||||
webengine_error=html.escape(imports.webengine_error)))
|
||||
errbox = msgbox.msgbox(parent=None,
|
||||
title="No backend library found!",
|
||||
text=text,
|
||||
icon=QMessageBox.Critical,
|
||||
plain_text=False)
|
||||
errbox.exec_()
|
||||
sys.exit(usertypes.Exit.err_init)
|
||||
elif objects.backend == usertypes.Backend.QtWebKit:
|
||||
if imports.webkit_available:
|
||||
if os.environ.get('QUTE_SKIP_LIBGL_WORKAROUND'):
|
||||
return
|
||||
assert imports.webengine_available
|
||||
_show_dialog(
|
||||
backend=usertypes.Backend.QtWebKit,
|
||||
because="QtWebKit could not be imported",
|
||||
text="<p><b>The error encountered was:</b><br/>{}</p>".format(
|
||||
html.escape(imports.webkit_error))
|
||||
)
|
||||
elif objects.backend == usertypes.Backend.QtWebEngine:
|
||||
if imports.webengine_available:
|
||||
|
||||
libgl = ctypes.util.find_library("GL")
|
||||
if libgl is not None:
|
||||
ctypes.CDLL(libgl, mode=ctypes.RTLD_GLOBAL)
|
||||
|
||||
def _handle_nouveau_graphics(self) -> None:
|
||||
"""Force software rendering when using the Nouveau driver.
|
||||
|
||||
WORKAROUND for
|
||||
https://bugreports.qt.io/browse/QTBUG-41242
|
||||
Should be fixed in Qt 5.10 via
|
||||
https://codereview.qt-project.org/#/c/208664/
|
||||
"""
|
||||
self._assert_backend(usertypes.Backend.QtWebEngine)
|
||||
|
||||
if os.environ.get('QUTE_SKIP_NOUVEAU_CHECK'):
|
||||
return
|
||||
assert imports.webkit_available
|
||||
_show_dialog(
|
||||
|
||||
if qtutils.version_check('5.10', compiled=False):
|
||||
return
|
||||
|
||||
if version.opengl_vendor() != 'nouveau':
|
||||
return
|
||||
|
||||
if (os.environ.get('LIBGL_ALWAYS_SOFTWARE') == '1' or
|
||||
# qt.force_software_rendering = 'software-opengl'
|
||||
'QT_XCB_FORCE_SOFTWARE_OPENGL' in os.environ or
|
||||
# qt.force_software_rendering = 'chromium', also see:
|
||||
# https://build.opensuse.org/package/view_file/openSUSE:Factory/libqt5-qtwebengine/disable-gpu-when-using-nouveau-boo-1005323.diff?expand=1
|
||||
'QT_WEBENGINE_DISABLE_NOUVEAU_WORKAROUND' in os.environ):
|
||||
return
|
||||
|
||||
button = _Button("Force software rendering",
|
||||
'qt.force_software_rendering',
|
||||
'chromium')
|
||||
self._show_dialog(
|
||||
backend=usertypes.Backend.QtWebEngine,
|
||||
because="QtWebEngine could not be imported",
|
||||
text="<p><b>The error encountered was:</b><br/>{}</p>".format(
|
||||
html.escape(imports.webengine_error))
|
||||
because="you're using Nouveau graphics",
|
||||
text=("<p>There are two ways to fix this:</p>"
|
||||
"<p><b>Forcing software rendering</b></p>"
|
||||
"<p>This allows you to use the newer QtWebEngine backend "
|
||||
"(based on Chromium) but could have noticeable performance "
|
||||
"impact (depending on your hardware). This sets the "
|
||||
"<i>qt.force_software_rendering = 'chromium'</i> option "
|
||||
"(if you have a <i>config.py</i> file, you'll need to set "
|
||||
"this manually).</p>"),
|
||||
buttons=[button],
|
||||
)
|
||||
|
||||
raise utils.Unreachable
|
||||
raise utils.Unreachable
|
||||
|
||||
def _handle_wayland(self) -> None:
|
||||
self._assert_backend(usertypes.Backend.QtWebEngine)
|
||||
|
||||
if os.environ.get('QUTE_SKIP_WAYLAND_CHECK'):
|
||||
return
|
||||
|
||||
platform = QApplication.instance().platformName()
|
||||
if platform not in ['wayland', 'wayland-egl']:
|
||||
return
|
||||
|
||||
has_qt511 = qtutils.version_check('5.11', compiled=False)
|
||||
if has_qt511 and config.val.qt.force_software_rendering == 'chromium':
|
||||
return
|
||||
|
||||
if qtutils.version_check('5.11.2', compiled=False):
|
||||
return
|
||||
|
||||
buttons = []
|
||||
text = "<p>You can work around this in one of the following ways:</p>"
|
||||
|
||||
if 'DISPLAY' in os.environ:
|
||||
# XWayland is available, but QT_QPA_PLATFORM=wayland is set
|
||||
buttons.append(
|
||||
_Button("Force XWayland", 'qt.force_platform', 'xcb'))
|
||||
text += ("<p><b>Force Qt to use XWayland</b></p>"
|
||||
"<p>This allows you to use the newer QtWebEngine backend "
|
||||
"(based on Chromium). "
|
||||
"This sets the <i>qt.force_platform = 'xcb'</i> option "
|
||||
"(if you have a <i>config.py</i> file, you'll need to "
|
||||
"set this manually).</p>")
|
||||
else:
|
||||
text += ("<p><b>Set up XWayland</b></p>"
|
||||
"<p>This allows you to use the newer QtWebEngine backend "
|
||||
"(based on Chromium). ")
|
||||
|
||||
if has_qt511:
|
||||
buttons.append(_Button("Force software rendering",
|
||||
'qt.force_software_rendering',
|
||||
'chromium'))
|
||||
text += ("<p><b>Forcing software rendering</b></p>"
|
||||
"<p>This allows you to use the newer QtWebEngine backend "
|
||||
"(based on Chromium) but could have noticeable "
|
||||
"performance impact (depending on your hardware). This "
|
||||
"sets the <i>qt.force_software_rendering = "
|
||||
"'chromium'</i> option (if you have a <i>config.py</i> "
|
||||
"file, you'll need to set this manually).</p>")
|
||||
|
||||
self._show_dialog(backend=usertypes.Backend.QtWebEngine,
|
||||
because="you're using Wayland",
|
||||
text=text,
|
||||
buttons=buttons)
|
||||
|
||||
def _try_import_backends(self) -> _BackendImports:
|
||||
"""Check whether backends can be imported and return BackendImports."""
|
||||
# pylint: disable=unused-import
|
||||
results = _BackendImports()
|
||||
|
||||
try:
|
||||
from PyQt5 import QtWebKit
|
||||
from PyQt5 import QtWebKitWidgets
|
||||
except ImportError 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
|
||||
results.webkit_error = "Unsupported legacy QtWebKit found"
|
||||
|
||||
try:
|
||||
from PyQt5 import QtWebEngineWidgets
|
||||
except ImportError 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
|
||||
|
||||
def _handle_ssl_support(self, fatal: bool = False) -> None:
|
||||
"""Check for full SSL availability.
|
||||
|
||||
If "fatal" is given, show an error and exit.
|
||||
"""
|
||||
text = ("Could not initialize QtNetwork SSL support. If you use "
|
||||
"OpenSSL 1.1 with a PyQt package from PyPI (e.g. on Archlinux "
|
||||
"or Debian Stretch), you need to set LD_LIBRARY_PATH to the "
|
||||
"path of OpenSSL 1.0. This only affects downloads.")
|
||||
|
||||
if QSslSocket.supportsSsl():
|
||||
return
|
||||
|
||||
if fatal:
|
||||
errbox = msgbox.msgbox(parent=None,
|
||||
title="SSL error",
|
||||
text="Could not initialize SSL support.",
|
||||
icon=QMessageBox.Critical,
|
||||
plain_text=False)
|
||||
errbox.exec_()
|
||||
sys.exit(usertypes.Exit.err_init)
|
||||
|
||||
assert not fatal
|
||||
log.init.warning(text)
|
||||
|
||||
def _check_backend_modules(self) -> None:
|
||||
"""Check for the modules needed for QtWebKit/QtWebEngine."""
|
||||
imports = self._try_import_backends()
|
||||
|
||||
if imports.webkit_available and imports.webengine_available:
|
||||
return
|
||||
elif not imports.webkit_available and not imports.webengine_available:
|
||||
text = ("<p>qutebrowser needs QtWebKit or QtWebEngine, but "
|
||||
"neither could be imported!</p>"
|
||||
"<p>The errors encountered were:<ul>"
|
||||
"<li><b>QtWebKit:</b> {webkit_error}"
|
||||
"<li><b>QtWebEngine:</b> {webengine_error}"
|
||||
"</ul></p>".format(
|
||||
webkit_error=html.escape(imports.webkit_error),
|
||||
webengine_error=html.escape(imports.webengine_error)))
|
||||
errbox = msgbox.msgbox(parent=None,
|
||||
title="No backend library found!",
|
||||
text=text,
|
||||
icon=QMessageBox.Critical,
|
||||
plain_text=False)
|
||||
errbox.exec_()
|
||||
sys.exit(usertypes.Exit.err_init)
|
||||
elif objects.backend == usertypes.Backend.QtWebKit:
|
||||
if imports.webkit_available:
|
||||
return
|
||||
assert imports.webengine_available
|
||||
self._show_dialog(
|
||||
backend=usertypes.Backend.QtWebKit,
|
||||
because="QtWebKit could not be imported",
|
||||
text="<p><b>The error encountered was:</b><br/>{}</p>".format(
|
||||
html.escape(imports.webkit_error))
|
||||
)
|
||||
elif objects.backend == usertypes.Backend.QtWebEngine:
|
||||
if imports.webengine_available:
|
||||
return
|
||||
assert imports.webkit_available
|
||||
self._show_dialog(
|
||||
backend=usertypes.Backend.QtWebEngine,
|
||||
because="QtWebEngine could not be imported",
|
||||
text="<p><b>The error encountered was:</b><br/>{}</p>".format(
|
||||
html.escape(imports.webengine_error))
|
||||
)
|
||||
|
||||
raise utils.Unreachable
|
||||
|
||||
def _handle_cache_nuking(self) -> None:
|
||||
"""Nuke the QtWebEngine cache if the Qt version changed.
|
||||
|
||||
WORKAROUND for https://bugreports.qt.io/browse/QTBUG-72532
|
||||
"""
|
||||
if not configfiles.state.qt_version_changed:
|
||||
return
|
||||
|
||||
# Only nuke the cache in cases where we know there are problems.
|
||||
# It seems these issues started with Qt 5.12.
|
||||
# They should be fixed with Qt 5.12.5:
|
||||
# https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/265408
|
||||
affected = (qtutils.version_check('5.12', compiled=False) and not
|
||||
qtutils.version_check('5.12.5', compiled=False))
|
||||
if not affected:
|
||||
return
|
||||
|
||||
log.init.info("Qt version changed, nuking QtWebEngine cache")
|
||||
cache_dir = os.path.join(standarddir.cache(), 'webengine')
|
||||
if os.path.exists(cache_dir):
|
||||
shutil.rmtree(cache_dir)
|
||||
|
||||
def _assert_backend(self, backend: usertypes.Backend) -> None:
|
||||
assert objects.backend == backend, objects.backend
|
||||
|
||||
def check(self) -> None:
|
||||
"""Run all checks."""
|
||||
self._check_backend_modules()
|
||||
if objects.backend == usertypes.Backend.QtWebEngine:
|
||||
self._handle_ssl_support()
|
||||
self._handle_wayland()
|
||||
self._nvidia_shader_workaround()
|
||||
self._handle_nouveau_graphics()
|
||||
self._handle_cache_nuking()
|
||||
else:
|
||||
self._assert_backend(usertypes.Backend.QtWebKit)
|
||||
self._handle_ssl_support(fatal=True)
|
||||
|
||||
|
||||
def _handle_cache_nuking():
|
||||
"""Nuke the QtWebEngine cache if the Qt version changed.
|
||||
|
||||
WORKAROUND for https://bugreports.qt.io/browse/QTBUG-72532
|
||||
"""
|
||||
if not configfiles.state.qt_version_changed:
|
||||
return
|
||||
|
||||
# Only nuke the cache in cases where we know there are problems.
|
||||
# It seems these issues started with Qt 5.12.
|
||||
# They should be fixed with Qt 5.12.5:
|
||||
# https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/265408
|
||||
affected = (qtutils.version_check('5.12', compiled=False) and not
|
||||
qtutils.version_check('5.12.5', compiled=False))
|
||||
if not affected:
|
||||
return
|
||||
|
||||
log.init.info("Qt version changed, nuking QtWebEngine cache")
|
||||
cache_dir = os.path.join(standarddir.cache(), 'webengine')
|
||||
if os.path.exists(cache_dir):
|
||||
shutil.rmtree(cache_dir)
|
||||
|
||||
|
||||
def init():
|
||||
"""Check for various issues related to QtWebKit/QtWebEngine."""
|
||||
_check_backend_modules()
|
||||
if objects.backend == usertypes.Backend.QtWebEngine:
|
||||
_handle_ssl_support()
|
||||
_handle_wayland()
|
||||
_nvidia_shader_workaround()
|
||||
_handle_nouveau_graphics()
|
||||
_handle_cache_nuking()
|
||||
else:
|
||||
assert objects.backend == usertypes.Backend.QtWebKit, objects.backend
|
||||
_handle_ssl_support(fatal=True)
|
||||
def init(*, quitter: 'app.Quitter',
|
||||
args: argparse.Namespace,
|
||||
save_manager: savemanager.SaveManager) -> None:
|
||||
"""Run all checks."""
|
||||
checker = _BackendProblemChecker(quitter=quitter,
|
||||
no_err_windows=args.no_err_windows,
|
||||
save_manager=save_manager)
|
||||
checker.check()
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
|
||||
import sys
|
||||
import code
|
||||
import typing
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt
|
||||
from PyQt5.QtWidgets import QTextEdit, QWidget, QVBoxLayout, QApplication
|
||||
|
|
@ -31,6 +32,9 @@ from qutebrowser.misc import cmdhistory, miscwidgets
|
|||
from qutebrowser.utils import utils, objreg
|
||||
|
||||
|
||||
console_widget = None
|
||||
|
||||
|
||||
class ConsoleLineEdit(miscwidgets.CommandLineEdit):
|
||||
|
||||
"""A QLineEdit which executes entered code and provides a history.
|
||||
|
|
@ -211,3 +215,9 @@ class ConsoleWidget(QWidget):
|
|||
def _curprompt(self):
|
||||
"""Get the prompt which is visible currently."""
|
||||
return sys.ps2 if self._more else sys.ps1
|
||||
|
||||
|
||||
def init():
|
||||
"""Initialize a global console."""
|
||||
global console_widget
|
||||
console_widget = ConsoleWidget()
|
||||
|
|
|
|||
|
|
@ -162,6 +162,10 @@ class CrashHandler(QObject):
|
|||
all_objects)
|
||||
self._crash_dialog.show()
|
||||
|
||||
@pyqtSlot()
|
||||
def shutdown(self):
|
||||
self.destroy_crashlogfile()
|
||||
|
||||
def destroy_crashlogfile(self):
|
||||
"""Clean up the crash log file and delete it."""
|
||||
if self._crash_log_file is None:
|
||||
|
|
|
|||
|
|
@ -395,6 +395,7 @@ class IPCServer(QObject):
|
|||
log.ipc.debug("Touching {}".format(path))
|
||||
os.utime(path)
|
||||
|
||||
@pyqtSlot()
|
||||
def shutdown(self):
|
||||
"""Shut down the IPC server cleanly."""
|
||||
log.ipc.debug("Shutting down IPC (socket 0x{:x})".format(
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import sys
|
|||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from qutebrowser.utils import objreg
|
||||
from qutebrowser.misc import objects
|
||||
|
||||
|
||||
class DummyBox:
|
||||
|
|
@ -51,8 +51,7 @@ def msgbox(parent, title, text, *, icon, buttons=QMessageBox.Ok,
|
|||
Return:
|
||||
A new QMessageBox.
|
||||
"""
|
||||
args = objreg.get('args')
|
||||
if args.no_err_windows:
|
||||
if objects.args.no_err_windows:
|
||||
print('Message box: {}; {}'.format(title, text), file=sys.stderr)
|
||||
return DummyBox()
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
# earlyinit.
|
||||
|
||||
import typing
|
||||
import argparse
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from qutebrowser.utils import usertypes
|
||||
|
|
@ -40,3 +41,4 @@ class NoBackend:
|
|||
backend = NoBackend() # type: typing.Union[usertypes.Backend, NoBackend]
|
||||
commands = {} # type: typing.Dict[str, command.Command]
|
||||
debug_flags = set() # type: typing.Set[str]
|
||||
args = typing.cast(argparse.Namespace, None)
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ from PyQt5.QtCore import pyqtSlot, QObject, QTimer
|
|||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.api import cmdutils
|
||||
from qutebrowser.utils import utils, log, message, usertypes
|
||||
from qutebrowser.utils import utils, log, message, usertypes, error
|
||||
|
||||
|
||||
class Saveable:
|
||||
|
|
@ -201,3 +201,14 @@ class SaveManager(QObject):
|
|||
except OSError as e:
|
||||
message.error("Could not save {}: {}".format(key, e))
|
||||
log.save.debug(":save saved {}".format(', '.join(what)))
|
||||
|
||||
@pyqtSlot()
|
||||
def shutdown(self):
|
||||
"""Save all saveables when shutting down."""
|
||||
for key in self.saveables:
|
||||
try:
|
||||
self.save(key, is_exit=True)
|
||||
except OSError as e:
|
||||
error.handle_fatal_exc(
|
||||
e, self._args, "Error while saving!",
|
||||
pre_text="Error while saving {}".format(key))
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import itertools
|
|||
import urllib
|
||||
import typing
|
||||
|
||||
from PyQt5.QtCore import QUrl, QObject, QPoint, QTimer
|
||||
from PyQt5.QtCore import QUrl, QObject, QPoint, QTimer, pyqtSlot
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
import yaml
|
||||
|
||||
|
|
@ -44,6 +44,9 @@ class Sentinel:
|
|||
|
||||
|
||||
default = Sentinel()
|
||||
session_manager = typing.cast('SessionManager', None)
|
||||
|
||||
ArgType = typing.Union[str, Sentinel]
|
||||
|
||||
|
||||
def init(parent=None):
|
||||
|
|
@ -58,8 +61,14 @@ def init(parent=None):
|
|||
except FileExistsError:
|
||||
pass
|
||||
|
||||
global session_manager
|
||||
session_manager = SessionManager(base_path, parent)
|
||||
objreg.register('session-manager', session_manager)
|
||||
objreg.register('session-manager', session_manager, command_only=True)
|
||||
|
||||
|
||||
@pyqtSlot()
|
||||
def shutdown():
|
||||
session_manager.delete_autosave()
|
||||
|
||||
|
||||
class SessionError(Exception):
|
||||
|
|
@ -518,7 +527,7 @@ class SessionManager(QObject):
|
|||
@cmdutils.argument('name', completion=miscmodels.session)
|
||||
@cmdutils.argument('win_id', value=cmdutils.Value.win_id)
|
||||
@cmdutils.argument('with_private', flag='p')
|
||||
def session_save(self, name: typing.Union[str, Sentinel] = default,
|
||||
def session_save(self, name: ArgType = default,
|
||||
current: bool = False,
|
||||
quiet: bool = False,
|
||||
force: bool = False,
|
||||
|
|
|
|||
|
|
@ -64,8 +64,9 @@ class Throttle(QObject):
|
|||
self._timer = usertypes.Timer(self, 'throttle-timer')
|
||||
self._timer.setSingleShot(True)
|
||||
|
||||
def _call_pending(self):
|
||||
def _call_pending(self) -> None:
|
||||
"""Start a pending call."""
|
||||
assert self._pending_call is not None
|
||||
self._func(*self._pending_call.args, **self._pending_call.kwargs)
|
||||
self._pending_call = None
|
||||
self._last_call_ms = int(time.monotonic() * 1000)
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import os
|
|||
import traceback
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtWidgets import QApplication # pylint: disable=unused-import
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
from qutebrowser.browser import qutescheme
|
||||
from qutebrowser.utils import log, objreg, usertypes, message, debug, utils
|
||||
|
|
@ -50,8 +50,7 @@ def later(ms: int, command: str, win_id: int) -> None:
|
|||
if ms < 0:
|
||||
raise cmdutils.CommandError("I can't run something in the past!")
|
||||
commandrunner = runners.CommandRunner(win_id)
|
||||
app = objreg.get('app')
|
||||
timer = usertypes.Timer(name='later', parent=app)
|
||||
timer = usertypes.Timer(name='later', parent=QApplication.instance())
|
||||
try:
|
||||
timer.setSingleShot(True)
|
||||
try:
|
||||
|
|
@ -107,44 +106,43 @@ def run_with_count(count_arg: int, command: str, win_id: int,
|
|||
|
||||
|
||||
@cmdutils.register()
|
||||
def clear_messages():
|
||||
def clear_messages() -> None:
|
||||
"""Clear all message notifications."""
|
||||
message.global_bridge.clear_messages.emit()
|
||||
|
||||
|
||||
@cmdutils.register(debug=True)
|
||||
def debug_all_objects():
|
||||
def debug_all_objects() -> None:
|
||||
"""Print a list of all objects to the debug log."""
|
||||
s = debug.get_all_objects()
|
||||
log.misc.debug(s)
|
||||
|
||||
|
||||
@cmdutils.register(debug=True)
|
||||
def debug_cache_stats():
|
||||
def debug_cache_stats() -> None:
|
||||
"""Print LRU cache stats."""
|
||||
debugcachestats.debug_cache_stats()
|
||||
|
||||
|
||||
@cmdutils.register(debug=True)
|
||||
def debug_console():
|
||||
def debug_console() -> None:
|
||||
"""Show the debugging console."""
|
||||
try:
|
||||
con_widget = objreg.get('debug-console')
|
||||
except KeyError:
|
||||
if consolewidget.console_widget is None:
|
||||
log.misc.debug('initializing debug console')
|
||||
con_widget = consolewidget.ConsoleWidget()
|
||||
objreg.register('debug-console', con_widget)
|
||||
consolewidget.init()
|
||||
|
||||
if con_widget.isVisible():
|
||||
assert consolewidget.console_widget is not None
|
||||
|
||||
if consolewidget.console_widget.isVisible():
|
||||
log.misc.debug('hiding debug console')
|
||||
con_widget.hide()
|
||||
consolewidget.console_widget.hide()
|
||||
else:
|
||||
log.misc.debug('showing debug console')
|
||||
con_widget.show()
|
||||
consolewidget.console_widget.show()
|
||||
|
||||
|
||||
@cmdutils.register(maxsplit=0, debug=True, no_cmd_split=True)
|
||||
def debug_pyeval(s, file=False, quiet=False):
|
||||
def debug_pyeval(s: str, file: bool = False, quiet: bool = False) -> None:
|
||||
"""Evaluate a python string and display the results as a web page.
|
||||
|
||||
Args:
|
||||
|
|
@ -182,7 +180,7 @@ def debug_pyeval(s, file=False, quiet=False):
|
|||
|
||||
|
||||
@cmdutils.register(debug=True)
|
||||
def debug_set_fake_clipboard(s=None):
|
||||
def debug_set_fake_clipboard(s: str = None) -> None:
|
||||
"""Put data into the fake clipboard and enable logging, used for tests.
|
||||
|
||||
Args:
|
||||
|
|
@ -197,7 +195,7 @@ def debug_set_fake_clipboard(s=None):
|
|||
@cmdutils.register()
|
||||
@cmdutils.argument('win_id', value=cmdutils.Value.win_id)
|
||||
@cmdutils.argument('count', value=cmdutils.Value.count)
|
||||
def repeat_command(win_id, count=None):
|
||||
def repeat_command(win_id: int, count: int = None) -> None:
|
||||
"""Repeat the last executed command.
|
||||
|
||||
Args:
|
||||
|
|
@ -234,8 +232,11 @@ def debug_log_level(level: str) -> None:
|
|||
Args:
|
||||
level: The log level to set.
|
||||
"""
|
||||
if log.console_handler is None:
|
||||
raise cmdutils.CommandError("No log.console_handler. Not attached "
|
||||
"to a console?")
|
||||
|
||||
log.change_console_formatter(log.LOG_LEVELS[level.upper()])
|
||||
assert log.console_handler is not None
|
||||
log.console_handler.setLevel(log.LOG_LEVELS[level.upper()])
|
||||
|
||||
|
||||
|
|
@ -265,7 +266,7 @@ def debug_log_filter(filters: str) -> None:
|
|||
|
||||
@cmdutils.register()
|
||||
@cmdutils.argument('current_win_id', value=cmdutils.Value.win_id)
|
||||
def window_only(current_win_id):
|
||||
def window_only(current_win_id: int) -> None:
|
||||
"""Close all windows except for the current one."""
|
||||
for win_id, window in objreg.window_registry.items():
|
||||
|
||||
|
|
@ -279,7 +280,7 @@ def window_only(current_win_id):
|
|||
|
||||
@cmdutils.register()
|
||||
@cmdutils.argument('win_id', value=cmdutils.Value.win_id)
|
||||
def version(win_id, paste=False):
|
||||
def version(win_id: int, paste: bool = False) -> None:
|
||||
"""Show version information.
|
||||
|
||||
Args:
|
||||
|
|
|
|||
|
|
@ -24,34 +24,36 @@ import inspect
|
|||
import logging
|
||||
import functools
|
||||
import datetime
|
||||
import typing
|
||||
import types
|
||||
|
||||
from PyQt5.QtCore import Qt, QEvent, QMetaMethod, QObject
|
||||
from PyQt5.QtCore import Qt, QEvent, QMetaMethod, QObject, pyqtSignal
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
from qutebrowser.utils import log, utils, qtutils, objreg
|
||||
|
||||
|
||||
def log_events(klass):
|
||||
def log_events(klass: typing.Type) -> typing.Type:
|
||||
"""Class decorator to log Qt events."""
|
||||
old_event = klass.event
|
||||
|
||||
@functools.wraps(old_event)
|
||||
def new_event(self, e, *args, **kwargs):
|
||||
def new_event(self: typing.Any, e: QEvent) -> bool:
|
||||
"""Wrapper for event() which logs events."""
|
||||
log.misc.debug("Event in {}: {}".format(utils.qualname(klass),
|
||||
qenum_key(QEvent, e.type())))
|
||||
return old_event(self, e, *args, **kwargs)
|
||||
return old_event(self, e)
|
||||
|
||||
klass.event = new_event
|
||||
return klass
|
||||
|
||||
|
||||
def log_signals(obj):
|
||||
def log_signals(obj: QObject) -> QObject:
|
||||
"""Log all signals of an object or class.
|
||||
|
||||
Can be used as class decorator.
|
||||
"""
|
||||
def log_slot(obj, signal, *args):
|
||||
def log_slot(obj: QObject, signal: pyqtSignal, *args: typing.Any) -> None:
|
||||
"""Slot connected to a signal to log it."""
|
||||
dbg = dbg_signal(signal, args)
|
||||
try:
|
||||
|
|
@ -60,7 +62,7 @@ def log_signals(obj):
|
|||
r = '<deleted>'
|
||||
log.signals.debug("Signal in {}: {}".format(r, dbg))
|
||||
|
||||
def connect_log_slot(obj):
|
||||
def connect_log_slot(obj: QObject) -> None:
|
||||
"""Helper function to connect all signals to a logging slot."""
|
||||
metaobj = obj.metaObject()
|
||||
for i in range(metaobj.methodCount()):
|
||||
|
|
@ -77,23 +79,27 @@ def log_signals(obj):
|
|||
pass
|
||||
|
||||
if inspect.isclass(obj):
|
||||
old_init = obj.__init__
|
||||
old_init = obj.__init__ # type: ignore
|
||||
|
||||
@functools.wraps(old_init)
|
||||
def new_init(self, *args, **kwargs):
|
||||
def new_init(self: typing.Any,
|
||||
*args: typing.Any,
|
||||
**kwargs: typing.Any) -> None:
|
||||
"""Wrapper for __init__() which logs signals."""
|
||||
ret = old_init(self, *args, **kwargs)
|
||||
old_init(self, *args, **kwargs)
|
||||
connect_log_slot(self)
|
||||
return ret
|
||||
|
||||
obj.__init__ = new_init
|
||||
obj.__init__ = new_init # type: ignore
|
||||
else:
|
||||
connect_log_slot(obj)
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
def qenum_key(base, value, add_base=False, klass=None):
|
||||
def qenum_key(base: typing.Type,
|
||||
value: int,
|
||||
add_base: bool = False,
|
||||
klass: typing.Type = None) -> str:
|
||||
"""Convert a Qt Enum value to its key as a string.
|
||||
|
||||
Args:
|
||||
|
|
@ -132,7 +138,10 @@ def qenum_key(base, value, add_base=False, klass=None):
|
|||
return ret
|
||||
|
||||
|
||||
def qflags_key(base, value, add_base=False, klass=None):
|
||||
def qflags_key(base: typing.Type,
|
||||
value: int,
|
||||
add_base: bool = False,
|
||||
klass: typing.Type = None) -> str:
|
||||
"""Convert a Qt QFlags value to its keys as string.
|
||||
|
||||
Note: Passing a combined value (such as Qt.AlignCenter) will get the names
|
||||
|
|
@ -176,7 +185,7 @@ def qflags_key(base, value, add_base=False, klass=None):
|
|||
return '|'.join(names)
|
||||
|
||||
|
||||
def signal_name(sig):
|
||||
def signal_name(sig: pyqtSignal) -> str:
|
||||
"""Get a cleaned up name of a signal.
|
||||
|
||||
Args:
|
||||
|
|
@ -185,11 +194,13 @@ def signal_name(sig):
|
|||
Return:
|
||||
The cleaned up signal name.
|
||||
"""
|
||||
m = re.fullmatch(r'[0-9]+(.*)\(.*\)', sig.signal)
|
||||
m = re.fullmatch(r'[0-9]+(.*)\(.*\)', sig.signal) # type: ignore
|
||||
assert m is not None
|
||||
return m.group(1)
|
||||
|
||||
|
||||
def format_args(args=None, kwargs=None):
|
||||
def format_args(args: typing.Sequence = None,
|
||||
kwargs: typing.Mapping = None) -> str:
|
||||
"""Format a list of arguments/kwargs to a function-call like string."""
|
||||
if args is not None:
|
||||
arglist = [utils.compact_text(repr(arg), 200) for arg in args]
|
||||
|
|
@ -201,7 +212,7 @@ def format_args(args=None, kwargs=None):
|
|||
return ', '.join(arglist)
|
||||
|
||||
|
||||
def dbg_signal(sig, args):
|
||||
def dbg_signal(sig: pyqtSignal, args: typing.Any) -> str:
|
||||
"""Get a string representation of a signal for debugging.
|
||||
|
||||
Args:
|
||||
|
|
@ -214,7 +225,10 @@ def dbg_signal(sig, args):
|
|||
return '{}({})'.format(signal_name(sig), format_args(args))
|
||||
|
||||
|
||||
def format_call(func, args=None, kwargs=None, full=True):
|
||||
def format_call(func: typing.Callable,
|
||||
args: typing.Sequence = None,
|
||||
kwargs: typing.Mapping = None,
|
||||
full: bool = True) -> str:
|
||||
"""Get a string representation of a function calls with the given args.
|
||||
|
||||
Args:
|
||||
|
|
@ -240,7 +254,8 @@ class log_time: # noqa: N801,N806 pylint: disable=invalid-name
|
|||
Usable as context manager or as decorator.
|
||||
"""
|
||||
|
||||
def __init__(self, logger, action='operation'):
|
||||
def __init__(self, logger: typing.Union[logging.Logger, str],
|
||||
action: str = 'operation') -> None:
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
|
|
@ -251,44 +266,49 @@ class log_time: # noqa: N801,N806 pylint: disable=invalid-name
|
|||
self._logger = logging.getLogger(logger)
|
||||
else:
|
||||
self._logger = logger
|
||||
self._started = None
|
||||
self._started = None # type: typing.Optional[datetime.datetime]
|
||||
self._action = action
|
||||
|
||||
def __enter__(self):
|
||||
def __enter__(self) -> None:
|
||||
self._started = datetime.datetime.now()
|
||||
|
||||
def __exit__(self, _exc_type, _exc_val, _exc_tb):
|
||||
def __exit__(self, _exc_type: typing.Optional[typing.Type[BaseException]],
|
||||
_exc_val: typing.Optional[BaseException],
|
||||
_exc_tb: typing.Optional[types.TracebackType]) -> bool:
|
||||
assert self._started is not None
|
||||
finished = datetime.datetime.now()
|
||||
delta = (finished - self._started).total_seconds()
|
||||
self._logger.debug("{} took {} seconds.".format(
|
||||
self._action.capitalize(), delta))
|
||||
return False
|
||||
|
||||
def __call__(self, func):
|
||||
def __call__(self, func: typing.Callable) -> typing.Callable:
|
||||
@functools.wraps(func)
|
||||
def wrapped(*args, **kwargs):
|
||||
def wrapped(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
|
||||
"""Call the original function."""
|
||||
with self:
|
||||
func(*args, **kwargs)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
def _get_widgets():
|
||||
def _get_widgets() -> typing.Sequence[str]:
|
||||
"""Get a string list of all widgets."""
|
||||
widgets = QApplication.instance().allWidgets()
|
||||
widgets.sort(key=repr)
|
||||
return [repr(w) for w in widgets]
|
||||
|
||||
|
||||
def _get_pyqt_objects(lines, obj, depth=0):
|
||||
def _get_pyqt_objects(lines: typing.MutableSequence[str],
|
||||
obj: QObject,
|
||||
depth: int = 0) -> None:
|
||||
"""Recursive method for get_all_objects to get Qt objects."""
|
||||
for kid in obj.findChildren(QObject, '', Qt.FindDirectChildrenOnly):
|
||||
lines.append(' ' * depth + repr(kid))
|
||||
_get_pyqt_objects(lines, kid, depth + 1)
|
||||
|
||||
|
||||
def get_all_objects(start_obj=None):
|
||||
def get_all_objects(start_obj: QObject = None) -> str:
|
||||
"""Get all children of an object recursively as a string."""
|
||||
output = ['']
|
||||
widget_lines = _get_widgets()
|
||||
|
|
@ -300,7 +320,7 @@ def get_all_objects(start_obj=None):
|
|||
if start_obj is None:
|
||||
start_obj = QApplication.instance()
|
||||
|
||||
pyqt_lines = []
|
||||
pyqt_lines = [] # type: typing.List[str]
|
||||
_get_pyqt_objects(pyqt_lines, start_obj)
|
||||
pyqt_lines = [' ' + e for e in pyqt_lines]
|
||||
pyqt_lines.insert(0, 'Qt objects - {} objects:'.format(len(pyqt_lines)))
|
||||
|
|
|
|||
|
|
@ -25,18 +25,19 @@ import inspect
|
|||
import os.path
|
||||
import collections
|
||||
import enum
|
||||
import typing
|
||||
|
||||
import qutebrowser
|
||||
from qutebrowser.utils import log, utils
|
||||
|
||||
|
||||
def is_git_repo():
|
||||
def is_git_repo() -> bool:
|
||||
"""Check if we're running from a git repository."""
|
||||
gitfolder = os.path.join(qutebrowser.basedir, os.path.pardir, '.git')
|
||||
return os.path.isdir(gitfolder)
|
||||
|
||||
|
||||
def docs_up_to_date(path):
|
||||
def docs_up_to_date(path: str) -> bool:
|
||||
"""Check if the generated html documentation is up to date.
|
||||
|
||||
Args:
|
||||
|
|
@ -79,17 +80,18 @@ class DocstringParser:
|
|||
State = enum.Enum('State', ['short', 'desc', 'desc_hidden',
|
||||
'arg_start', 'arg_inside', 'misc'])
|
||||
|
||||
def __init__(self, func):
|
||||
def __init__(self, func: typing.Callable) -> None:
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
func: The function to parse the docstring for.
|
||||
"""
|
||||
self._state = self.State.short
|
||||
self._cur_arg_name = None
|
||||
self._short_desc_parts = []
|
||||
self._long_desc_parts = []
|
||||
self.arg_descs = collections.OrderedDict()
|
||||
self._cur_arg_name = None # type: typing.Optional[str]
|
||||
self._short_desc_parts = [] # type: typing.List[str]
|
||||
self._long_desc_parts = [] # type: typing.List[str]
|
||||
self.arg_descs = collections.OrderedDict(
|
||||
) # type: typing.Dict[str, typing.Union[str, typing.List[str]]]
|
||||
doc = inspect.getdoc(func)
|
||||
handlers = {
|
||||
self.State.short: self._parse_short,
|
||||
|
|
@ -104,7 +106,8 @@ class DocstringParser:
|
|||
log.commands.warning(
|
||||
"Function {}() from {} has no docstring".format(
|
||||
utils.qualname(func),
|
||||
inspect.getsourcefile(func)))
|
||||
# https://github.com/python/typeshed/pull/3295
|
||||
inspect.getsourcefile(func))) # type: ignore
|
||||
self.long_desc = ""
|
||||
self.short_desc = ""
|
||||
return
|
||||
|
|
@ -121,25 +124,25 @@ class DocstringParser:
|
|||
self.long_desc = ' '.join(self._long_desc_parts)
|
||||
self.short_desc = ' '.join(self._short_desc_parts)
|
||||
|
||||
def _process_arg(self, line):
|
||||
def _process_arg(self, line: str) -> None:
|
||||
"""Helper method to process a line like 'fooarg: Blah blub'."""
|
||||
self._cur_arg_name, argdesc = line.split(':', maxsplit=1)
|
||||
self._cur_arg_name = self._cur_arg_name.strip().lstrip('*')
|
||||
self.arg_descs[self._cur_arg_name] = [argdesc.strip()]
|
||||
|
||||
def _skip(self, line):
|
||||
def _skip(self, line: str) -> None:
|
||||
"""Handler to ignore everything until we get 'Args:'."""
|
||||
if line.startswith('Args:'):
|
||||
self._state = self.State.arg_start
|
||||
|
||||
def _parse_short(self, line):
|
||||
def _parse_short(self, line: str) -> None:
|
||||
"""Parse the short description (first block) in the docstring."""
|
||||
if not line:
|
||||
self._state = self.State.desc
|
||||
else:
|
||||
self._short_desc_parts.append(line.strip())
|
||||
|
||||
def _parse_desc(self, line):
|
||||
def _parse_desc(self, line: str) -> None:
|
||||
"""Parse the long description in the docstring."""
|
||||
if line.startswith('Args:'):
|
||||
self._state = self.State.arg_start
|
||||
|
|
@ -148,22 +151,27 @@ class DocstringParser:
|
|||
elif line.strip():
|
||||
self._long_desc_parts.append(line.strip())
|
||||
|
||||
def _parse_arg_start(self, line):
|
||||
def _parse_arg_start(self, line: str) -> None:
|
||||
"""Parse first argument line."""
|
||||
self._process_arg(line)
|
||||
self._state = self.State.arg_inside
|
||||
|
||||
def _parse_arg_inside(self, line):
|
||||
def _parse_arg_inside(self, line: str) -> bool:
|
||||
"""Parse subsequent argument lines."""
|
||||
argname = self._cur_arg_name
|
||||
assert argname is not None
|
||||
|
||||
descs = self.arg_descs[argname]
|
||||
assert isinstance(descs, list)
|
||||
|
||||
if re.fullmatch(r'[A-Z][a-z]+:', line):
|
||||
if not self.arg_descs[argname][-1].strip():
|
||||
self.arg_descs[argname] = self.arg_descs[argname][:-1]
|
||||
if not descs[-1].strip():
|
||||
del descs[-1]
|
||||
return True
|
||||
elif not line.strip():
|
||||
self.arg_descs[argname].append('\n\n')
|
||||
descs.append('\n\n')
|
||||
elif line[4:].startswith(' '):
|
||||
self.arg_descs[argname].append(line.strip() + '\n')
|
||||
descs.append(line.strip() + '\n')
|
||||
else:
|
||||
self._process_arg(line)
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -19,12 +19,14 @@
|
|||
|
||||
"""Tools related to error printing/displaying."""
|
||||
|
||||
import argparse
|
||||
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from qutebrowser.utils import log, utils
|
||||
|
||||
|
||||
def _get_name(exc):
|
||||
def _get_name(exc: BaseException) -> str:
|
||||
"""Get a suitable exception name as a string."""
|
||||
prefixes = ['qutebrowser', 'builtins']
|
||||
name = utils.qualname(exc.__class__)
|
||||
|
|
@ -35,7 +37,11 @@ def _get_name(exc):
|
|||
return name
|
||||
|
||||
|
||||
def handle_fatal_exc(exc, args, title, *, pre_text='', post_text=''):
|
||||
def handle_fatal_exc(exc: BaseException,
|
||||
args: argparse.Namespace,
|
||||
title: str, *,
|
||||
pre_text: str = '',
|
||||
post_text: str = '') -> None:
|
||||
"""Handle a fatal "expected" exception by displaying an error box.
|
||||
|
||||
If --no-err-windows is given as argument, the text is logged to the error
|
||||
|
|
|
|||
|
|
@ -19,11 +19,15 @@
|
|||
|
||||
"""Utilities related to javascript interaction."""
|
||||
|
||||
import typing
|
||||
|
||||
from qutebrowser.utils import jinja
|
||||
|
||||
_InnerJsArgType = typing.Union[None, str, bool, int, float]
|
||||
_JsArgType = typing.Union[_InnerJsArgType, typing.Sequence[_InnerJsArgType]]
|
||||
|
||||
def string_escape(text):
|
||||
|
||||
def string_escape(text: str) -> str:
|
||||
"""Escape values special to javascript in strings.
|
||||
|
||||
With this we should be able to use something like:
|
||||
|
|
@ -49,7 +53,7 @@ def string_escape(text):
|
|||
return text
|
||||
|
||||
|
||||
def to_js(arg):
|
||||
def to_js(arg: _JsArgType) -> str:
|
||||
"""Convert the given argument so it's the equivalent in JS."""
|
||||
if arg is None:
|
||||
return 'undefined'
|
||||
|
|
@ -66,7 +70,7 @@ def to_js(arg):
|
|||
arg, type(arg).__name__))
|
||||
|
||||
|
||||
def assemble(module, function, *args):
|
||||
def assemble(module: str, function: str, *args: _JsArgType) -> str:
|
||||
"""Assemble a javascript file and a function call."""
|
||||
js_args = ', '.join(to_js(arg) for arg in args)
|
||||
if module == 'window':
|
||||
|
|
@ -77,7 +81,7 @@ def assemble(module, function, *args):
|
|||
return code
|
||||
|
||||
|
||||
def wrap_global(name, *sources):
|
||||
def wrap_global(name: str, *sources: str) -> str:
|
||||
"""Wrap a script using window._qutebrowser."""
|
||||
template = jinja.js_environment.get_template('global_wrapper.js')
|
||||
return template.render(code='\n'.join(sources), name=name)
|
||||
|
|
|
|||
|
|
@ -61,10 +61,14 @@ class Loader(jinja2.BaseLoader):
|
|||
_subdir: The subdirectory to find templates in.
|
||||
"""
|
||||
|
||||
def __init__(self, subdir):
|
||||
def __init__(self, subdir: str) -> None:
|
||||
self._subdir = subdir
|
||||
|
||||
def get_source(self, _env, template):
|
||||
def get_source(
|
||||
self,
|
||||
_env: jinja2.Environment,
|
||||
template: str
|
||||
) -> typing.Tuple[str, str, typing.Callable[[], bool]]:
|
||||
path = os.path.join(self._subdir, template)
|
||||
try:
|
||||
source = utils.read_file(path)
|
||||
|
|
@ -82,7 +86,7 @@ class Environment(jinja2.Environment):
|
|||
|
||||
"""Our own jinja environment which is more strict."""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(loader=Loader('html'),
|
||||
autoescape=lambda _name: self._autoescape,
|
||||
undefined=jinja2.StrictUndefined)
|
||||
|
|
@ -94,29 +98,31 @@ class Environment(jinja2.Environment):
|
|||
self._autoescape = True
|
||||
|
||||
@contextlib.contextmanager
|
||||
def no_autoescape(self):
|
||||
def no_autoescape(self) -> typing.Iterator[None]:
|
||||
"""Context manager to temporarily turn off autoescaping."""
|
||||
self._autoescape = False
|
||||
yield
|
||||
self._autoescape = True
|
||||
|
||||
def _resource_url(self, path):
|
||||
def _resource_url(self, path: str) -> str:
|
||||
"""Load images from a relative path (to qutebrowser).
|
||||
|
||||
Arguments:
|
||||
path: The relative path to the image
|
||||
"""
|
||||
image = utils.resource_filename(path)
|
||||
return QUrl.fromLocalFile(image).toString(QUrl.FullyEncoded)
|
||||
url = QUrl.fromLocalFile(image)
|
||||
urlstr = url.toString(QUrl.FullyEncoded) # type: ignore
|
||||
return urlstr
|
||||
|
||||
def _data_url(self, path):
|
||||
def _data_url(self, path: str) -> str:
|
||||
"""Get a data: url for the broken qutebrowser logo."""
|
||||
data = utils.read_file(path, binary=True)
|
||||
filename = utils.resource_filename(path)
|
||||
mimetype = utils.guess_mimetype(filename)
|
||||
return urlutils.data_url(mimetype, data).toString()
|
||||
|
||||
def getattr(self, obj, attribute):
|
||||
def getattr(self, obj: typing.Any, attribute: str) -> typing.Any:
|
||||
"""Override jinja's getattr() to be less clever.
|
||||
|
||||
This means it doesn't fall back to __getitem__, and it doesn't hide
|
||||
|
|
@ -125,7 +131,7 @@ class Environment(jinja2.Environment):
|
|||
return getattr(obj, attribute)
|
||||
|
||||
|
||||
def render(template, **kwargs):
|
||||
def render(template: str, **kwargs: typing.Any) -> str:
|
||||
"""Render the given template and pass the given arguments to it."""
|
||||
return environment.get_template(template).render(**kwargs)
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ import traceback
|
|||
import warnings
|
||||
import json
|
||||
import inspect
|
||||
import typing
|
||||
import argparse
|
||||
|
||||
from PyQt5 import QtCore
|
||||
# Optional imports
|
||||
|
|
@ -91,7 +93,10 @@ LOG_LEVELS = {
|
|||
}
|
||||
|
||||
|
||||
def vdebug(self, msg, *args, **kwargs):
|
||||
def vdebug(self: logging.Logger,
|
||||
msg: str,
|
||||
*args: typing.Any,
|
||||
**kwargs: typing.Any) -> None:
|
||||
"""Log with a VDEBUG level.
|
||||
|
||||
VDEBUG is used when a debug message is rather verbose, and probably of
|
||||
|
|
@ -100,7 +105,7 @@ def vdebug(self, msg, *args, **kwargs):
|
|||
"""
|
||||
if self.isEnabledFor(VDEBUG_LEVEL):
|
||||
# pylint: disable=protected-access
|
||||
self._log(VDEBUG_LEVEL, msg, args, **kwargs)
|
||||
self._log(VDEBUG_LEVEL, msg, args, **kwargs) # type: ignore
|
||||
# pylint: enable=protected-access
|
||||
|
||||
|
||||
|
|
@ -151,12 +156,12 @@ LOGGER_NAMES = [
|
|||
]
|
||||
|
||||
|
||||
ram_handler = None
|
||||
console_handler = None
|
||||
ram_handler = None # type: typing.Optional[RAMHandler]
|
||||
console_handler = None # type: typing.Optional[logging.Handler]
|
||||
console_filter = None
|
||||
|
||||
|
||||
def stub(suffix=''):
|
||||
def stub(suffix: str = '') -> None:
|
||||
"""Show a STUB: message for the calling function."""
|
||||
try:
|
||||
function = inspect.stack()[1][3]
|
||||
|
|
@ -169,7 +174,7 @@ def stub(suffix=''):
|
|||
misc.warning(text)
|
||||
|
||||
|
||||
def init_log(args):
|
||||
def init_log(args: argparse.Namespace) -> None:
|
||||
"""Init loggers based on the argparse namespace passed."""
|
||||
level = args.loglevel.upper()
|
||||
try:
|
||||
|
|
@ -208,13 +213,18 @@ def init_log(args):
|
|||
root.setLevel(logging.NOTSET)
|
||||
logging.captureWarnings(True)
|
||||
_init_py_warnings()
|
||||
QtCore.qInstallMessageHandler(qt_message_handler)
|
||||
QtCore.qInstallMessageHandler(qt_message_handler) # type: ignore
|
||||
global _log_inited, _args
|
||||
_log_inited = True
|
||||
_args = args
|
||||
|
||||
|
||||
def _init_py_warnings():
|
||||
@QtCore.pyqtSlot()
|
||||
def shutdown_log() -> None:
|
||||
QtCore.qInstallMessageHandler(None)
|
||||
|
||||
|
||||
def _init_py_warnings() -> None:
|
||||
"""Initialize Python warning handling."""
|
||||
warnings.simplefilter('default')
|
||||
warnings.filterwarnings('ignore', module='pdb', category=ResourceWarning)
|
||||
|
|
@ -226,7 +236,7 @@ def _init_py_warnings():
|
|||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def disable_qt_msghandler():
|
||||
def disable_qt_msghandler() -> typing.Iterator[None]:
|
||||
"""Contextmanager which temporarily disables the Qt message handler."""
|
||||
old_handler = QtCore.qInstallMessageHandler(None)
|
||||
try:
|
||||
|
|
@ -236,7 +246,7 @@ def disable_qt_msghandler():
|
|||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def ignore_py_warnings(**kwargs):
|
||||
def ignore_py_warnings(**kwargs: typing.Any) -> typing.Iterator[None]:
|
||||
"""Contextmanager to temporarily disable certain Python warnings."""
|
||||
warnings.filterwarnings('ignore', **kwargs)
|
||||
yield
|
||||
|
|
@ -244,7 +254,13 @@ def ignore_py_warnings(**kwargs):
|
|||
_init_py_warnings()
|
||||
|
||||
|
||||
def _init_handlers(level, color, force_color, json_logging, ram_capacity):
|
||||
def _init_handlers(
|
||||
level: int,
|
||||
color: bool,
|
||||
force_color: bool,
|
||||
json_logging: bool,
|
||||
ram_capacity: int
|
||||
) -> typing.Tuple[logging.StreamHandler, typing.Optional['RAMHandler']]:
|
||||
"""Init log handlers.
|
||||
|
||||
Args:
|
||||
|
|
@ -281,7 +297,7 @@ def _init_handlers(level, color, force_color, json_logging, ram_capacity):
|
|||
return console_handler, ram_handler
|
||||
|
||||
|
||||
def get_console_format(level):
|
||||
def get_console_format(level: int) -> str:
|
||||
"""Get the log format the console logger should use.
|
||||
|
||||
Args:
|
||||
|
|
@ -293,7 +309,13 @@ def get_console_format(level):
|
|||
return EXTENDED_FMT if level <= logging.DEBUG else SIMPLE_FMT
|
||||
|
||||
|
||||
def _init_formatters(level, color, force_color, json_logging):
|
||||
def _init_formatters(
|
||||
level: int,
|
||||
color: bool,
|
||||
force_color: bool,
|
||||
json_logging: bool
|
||||
) -> typing.Tuple[typing.Union['JSONFormatter', 'ColoredFormatter'],
|
||||
'ColoredFormatter', 'HTMLFormatter', bool]:
|
||||
"""Init log formatters.
|
||||
|
||||
Args:
|
||||
|
|
@ -316,8 +338,8 @@ def _init_formatters(level, color, force_color, json_logging):
|
|||
return None, ram_formatter, html_formatter, False # type: ignore
|
||||
|
||||
if json_logging:
|
||||
console_formatter = JSONFormatter()
|
||||
return console_formatter, ram_formatter, html_formatter, False
|
||||
json_formatter = JSONFormatter()
|
||||
return json_formatter, ram_formatter, html_formatter, False
|
||||
|
||||
use_colorama = False
|
||||
color_supported = os.name == 'posix' or colorama
|
||||
|
|
@ -334,24 +356,24 @@ def _init_formatters(level, color, force_color, json_logging):
|
|||
return console_formatter, ram_formatter, html_formatter, use_colorama
|
||||
|
||||
|
||||
def change_console_formatter(level):
|
||||
def change_console_formatter(level: int) -> None:
|
||||
"""Change console formatter based on level.
|
||||
|
||||
Args:
|
||||
level: The numeric logging level
|
||||
"""
|
||||
if not isinstance(console_handler.formatter, ColoredFormatter):
|
||||
# JSON Formatter being used for end2end tests
|
||||
pass
|
||||
assert console_handler is not None
|
||||
|
||||
use_colors = console_handler.formatter.use_colors
|
||||
old_formatter = typing.cast(ColoredFormatter, console_handler.formatter)
|
||||
console_fmt = get_console_format(level)
|
||||
console_formatter = ColoredFormatter(console_fmt, DATEFMT, '{',
|
||||
use_colors=use_colors)
|
||||
use_colors=old_formatter.use_colors)
|
||||
console_handler.setFormatter(console_formatter)
|
||||
|
||||
|
||||
def qt_message_handler(msg_type, context, msg):
|
||||
def qt_message_handler(msg_type: QtCore.QtMsgType,
|
||||
context: QtCore.QMessageLogContext,
|
||||
msg: str) -> None:
|
||||
"""Qt message handler to redirect qWarning etc. to the logging system.
|
||||
|
||||
Args:
|
||||
|
|
@ -456,13 +478,14 @@ def qt_message_handler(msg_type, context, msg):
|
|||
level = qt_to_logging[msg_type]
|
||||
|
||||
if context.function is None:
|
||||
func = 'none'
|
||||
func = 'none' # type: ignore
|
||||
elif ':' in context.function:
|
||||
func = '"{}"'.format(context.function)
|
||||
else:
|
||||
func = context.function
|
||||
|
||||
if context.category is None or context.category == 'default':
|
||||
if (context.category is None or # type: ignore
|
||||
context.category == 'default'):
|
||||
name = 'qt'
|
||||
else:
|
||||
name = 'qt-' + context.category
|
||||
|
|
@ -474,17 +497,19 @@ def qt_message_handler(msg_type, context, msg):
|
|||
" pacman -S libxkbcommon-x11")
|
||||
faulthandler.disable()
|
||||
|
||||
assert _args is not None
|
||||
if _args.debug:
|
||||
stack = ''.join(traceback.format_stack())
|
||||
stack = ''.join(traceback.format_stack()) # type: typing.Optional[str]
|
||||
else:
|
||||
stack = None
|
||||
|
||||
record = qt.makeRecord(name, level, context.file, context.line, msg, (),
|
||||
None, func, sinfo=stack)
|
||||
qt.handle(record)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def hide_qt_warning(pattern, logger='qt'):
|
||||
def hide_qt_warning(pattern: str, logger: str = 'qt') -> typing.Iterator[None]:
|
||||
"""Hide Qt warnings matching the given regex."""
|
||||
log_filter = QtWarningFilter(pattern)
|
||||
logger_obj = logging.getLogger(logger)
|
||||
|
|
@ -503,11 +528,11 @@ class QtWarningFilter(logging.Filter):
|
|||
_pattern: The start of the message.
|
||||
"""
|
||||
|
||||
def __init__(self, pattern):
|
||||
def __init__(self, pattern: str) -> None:
|
||||
super().__init__()
|
||||
self._pattern = pattern
|
||||
|
||||
def filter(self, record):
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
"""Determine if the specified record is to be logged."""
|
||||
do_log = not record.msg.strip().startswith(self._pattern)
|
||||
return do_log
|
||||
|
|
@ -525,12 +550,13 @@ class LogFilter(logging.Filter):
|
|||
negated: Whether names is a list of records to log or to suppress.
|
||||
"""
|
||||
|
||||
def __init__(self, names, negate=False):
|
||||
def __init__(self, names: typing.Optional[typing.Iterable[str]],
|
||||
negate: bool = False) -> None:
|
||||
super().__init__()
|
||||
self.names = names
|
||||
self.negated = negate
|
||||
|
||||
def filter(self, record):
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
"""Determine if the specified record is to be logged."""
|
||||
if self.names is None:
|
||||
return True
|
||||
|
|
@ -558,20 +584,22 @@ class RAMHandler(logging.Handler):
|
|||
_data: A deque containing the logging records.
|
||||
"""
|
||||
|
||||
def __init__(self, capacity):
|
||||
def __init__(self, capacity: int) -> None:
|
||||
super().__init__()
|
||||
self.html_formatter = None
|
||||
self.html_formatter = None # type: typing.Optional[HTMLFormatter]
|
||||
if capacity != -1:
|
||||
self._data = collections.deque(maxlen=capacity)
|
||||
self._data = collections.deque(
|
||||
maxlen=capacity
|
||||
) # type: typing.MutableSequence[logging.LogRecord]
|
||||
else:
|
||||
self._data = collections.deque()
|
||||
|
||||
def emit(self, record):
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
if record.levelno >= logging.DEBUG:
|
||||
# We don't log VDEBUG to RAM.
|
||||
self._data.append(record)
|
||||
|
||||
def dump_log(self, html=False, level='vdebug'):
|
||||
def dump_log(self, html: bool = False, level: str = 'vdebug') -> str:
|
||||
"""Dump the complete formatted log data as string.
|
||||
|
||||
FIXME: We should do all the HTML formatter via jinja2.
|
||||
|
|
@ -579,7 +607,13 @@ class RAMHandler(logging.Handler):
|
|||
https://github.com/qutebrowser/qutebrowser/issues/34
|
||||
"""
|
||||
minlevel = LOG_LEVELS.get(level.upper(), VDEBUG_LEVEL)
|
||||
fmt = self.html_formatter.format if html else self.format
|
||||
|
||||
if html:
|
||||
assert self.html_formatter is not None
|
||||
fmt = self.html_formatter.format
|
||||
else:
|
||||
fmt = self.format
|
||||
|
||||
self.acquire()
|
||||
try:
|
||||
lines = [fmt(record)
|
||||
|
|
@ -589,7 +623,7 @@ class RAMHandler(logging.Handler):
|
|||
self.release()
|
||||
return '\n'.join(lines)
|
||||
|
||||
def change_log_capacity(self, capacity):
|
||||
def change_log_capacity(self, capacity: int) -> None:
|
||||
self._data = collections.deque(self._data, maxlen=capacity)
|
||||
|
||||
|
||||
|
|
@ -601,11 +635,14 @@ class ColoredFormatter(logging.Formatter):
|
|||
use_colors: Whether to do colored logging or not.
|
||||
"""
|
||||
|
||||
def __init__(self, fmt, datefmt, style, *, use_colors):
|
||||
def __init__(self, fmt: str,
|
||||
datefmt: str,
|
||||
style: str, *,
|
||||
use_colors: bool) -> None:
|
||||
super().__init__(fmt, datefmt, style)
|
||||
self.use_colors = use_colors
|
||||
|
||||
def format(self, record):
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
if self.use_colors:
|
||||
color_dict = dict(COLOR_ESCAPES)
|
||||
color_dict['reset'] = RESET_ESCAPE
|
||||
|
|
@ -628,7 +665,9 @@ class HTMLFormatter(logging.Formatter):
|
|||
_colordict: The colordict passed to the logger.
|
||||
"""
|
||||
|
||||
def __init__(self, fmt, datefmt, log_colors):
|
||||
def __init__(self, fmt: str,
|
||||
datefmt: str,
|
||||
log_colors: typing.Mapping[str, str]) -> None:
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
|
|
@ -637,22 +676,22 @@ class HTMLFormatter(logging.Formatter):
|
|||
log_colors: The colors to use for logging levels.
|
||||
"""
|
||||
super().__init__(fmt, datefmt)
|
||||
self._log_colors = log_colors
|
||||
self._colordict = {}
|
||||
self._log_colors = log_colors # type: typing.Mapping[str, str]
|
||||
self._colordict = {} # type: typing.Mapping[str, str]
|
||||
# We could solve this nicer by using CSS, but for this simple case this
|
||||
# works.
|
||||
for color in COLORS:
|
||||
self._colordict[color] = '<font color="{}">'.format(color)
|
||||
self._colordict['reset'] = '</font>'
|
||||
|
||||
def format(self, record):
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
record_clone = copy.copy(record)
|
||||
record_clone.__dict__.update(self._colordict)
|
||||
if record_clone.levelname in self._log_colors:
|
||||
color = self._log_colors[record_clone.levelname]
|
||||
record_clone.log_color = self._colordict[color]
|
||||
record_clone.log_color = self._colordict[color] # type: ignore
|
||||
else:
|
||||
record_clone.log_color = ''
|
||||
record_clone.log_color = '' # type: ignore
|
||||
for field in ['msg', 'filename', 'funcName', 'levelname', 'module',
|
||||
'name', 'pathname', 'processName', 'threadName']:
|
||||
data = str(getattr(record_clone, field))
|
||||
|
|
@ -662,8 +701,10 @@ class HTMLFormatter(logging.Formatter):
|
|||
msg += self._colordict['reset']
|
||||
return msg
|
||||
|
||||
def formatTime(self, record, datefmt=None):
|
||||
out = super().formatTime(record, datefmt)
|
||||
def formatTime(self, record: logging.LogRecord,
|
||||
datefmt: str = None) -> str:
|
||||
# https://github.com/python/typeshed/pull/3343
|
||||
out = super().formatTime(record, datefmt) # type: ignore
|
||||
return pyhtml.escape(out)
|
||||
|
||||
|
||||
|
|
@ -671,7 +712,7 @@ class JSONFormatter(logging.Formatter):
|
|||
|
||||
"""Formatter for JSON-encoded log messages."""
|
||||
|
||||
def format(self, record):
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
obj = {}
|
||||
for field in ['created', 'msecs', 'levelname', 'name', 'module',
|
||||
'funcName', 'lineno', 'levelno']:
|
||||
|
|
|
|||
|
|
@ -23,8 +23,9 @@
|
|||
"""Message singleton so we don't have to define unneeded signals."""
|
||||
|
||||
import traceback
|
||||
import typing
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QObject
|
||||
from PyQt5.QtCore import pyqtSignal, QObject, QUrl
|
||||
|
||||
from qutebrowser.utils import usertypes, log, utils
|
||||
|
||||
|
|
@ -82,11 +83,14 @@ def info(message: str, *, replace: bool = False) -> None:
|
|||
global_bridge.show(usertypes.MessageLevel.info, message, replace)
|
||||
|
||||
|
||||
def _build_question(title, text=None, *, mode, default=None, abort_on=(),
|
||||
url=None, option=None):
|
||||
def _build_question(title: str,
|
||||
text: str = None, *,
|
||||
mode: usertypes.PromptMode,
|
||||
default: typing.Union[None, bool, str] = None,
|
||||
abort_on: typing.Iterable[pyqtSignal] = (),
|
||||
url: QUrl = None,
|
||||
option: bool = None) -> usertypes.Question:
|
||||
"""Common function for ask/ask_async."""
|
||||
if not isinstance(mode, usertypes.PromptMode):
|
||||
raise TypeError("Mode {} is no PromptMode member!".format(mode))
|
||||
question = usertypes.Question()
|
||||
question.title = title
|
||||
question.text = text
|
||||
|
|
@ -106,7 +110,7 @@ def _build_question(title, text=None, *, mode, default=None, abort_on=(),
|
|||
return question
|
||||
|
||||
|
||||
def ask(*args, **kwargs):
|
||||
def ask(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
|
||||
"""Ask a modular question in the statusbar (blocking).
|
||||
|
||||
Args:
|
||||
|
|
@ -128,7 +132,10 @@ def ask(*args, **kwargs):
|
|||
return answer
|
||||
|
||||
|
||||
def ask_async(title, mode, handler, **kwargs):
|
||||
def ask_async(title: str,
|
||||
mode: usertypes.PromptMode,
|
||||
handler: typing.Callable[[typing.Any], None],
|
||||
**kwargs: typing.Any) -> None:
|
||||
"""Ask an async question in the statusbar.
|
||||
|
||||
Args:
|
||||
|
|
@ -144,8 +151,10 @@ def ask_async(title, mode, handler, **kwargs):
|
|||
global_bridge.ask(question, blocking=False)
|
||||
|
||||
|
||||
def confirm_async(*, yes_action, no_action=None, cancel_action=None,
|
||||
**kwargs):
|
||||
def confirm_async(*, yes_action: typing.Callable[[], None],
|
||||
no_action: typing.Callable[[], None] = None,
|
||||
cancel_action: typing.Callable[[], None] = None,
|
||||
**kwargs: typing.Any) -> usertypes.Question:
|
||||
"""Ask a yes/no question to the user and execute the given actions.
|
||||
|
||||
Args:
|
||||
|
|
@ -204,12 +213,15 @@ class GlobalMessageBridge(QObject):
|
|||
mode_left = pyqtSignal(usertypes.KeyMode)
|
||||
clear_messages = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, parent: QObject = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._connected = False
|
||||
self._cache = []
|
||||
self._cache = [
|
||||
] # type: typing.List[typing.Tuple[usertypes.MessageLevel, str, bool]]
|
||||
|
||||
def ask(self, question, blocking, *, log_stack=False):
|
||||
def ask(self, question: usertypes.Question,
|
||||
blocking: bool, *,
|
||||
log_stack: bool = False) -> None:
|
||||
"""Ask a question to the user.
|
||||
|
||||
Note this method doesn't return the answer, it only blocks. The caller
|
||||
|
|
@ -223,14 +235,16 @@ class GlobalMessageBridge(QObject):
|
|||
"""
|
||||
self.ask_question.emit(question, blocking)
|
||||
|
||||
def show(self, level, text, replace=False):
|
||||
def show(self, level: usertypes.MessageLevel,
|
||||
text: str,
|
||||
replace: bool = False) -> None:
|
||||
"""Show the given message."""
|
||||
if self._connected:
|
||||
self.show_message.emit(level, text, replace)
|
||||
else:
|
||||
self._cache.append((level, text, replace))
|
||||
|
||||
def flush(self):
|
||||
def flush(self) -> None:
|
||||
"""Flush messages which accumulated while no handler was connected.
|
||||
|
||||
This is so we don't miss messages shown during some early init phase.
|
||||
|
|
@ -256,10 +270,10 @@ class MessageBridge(QObject):
|
|||
s_set_text = pyqtSignal(str)
|
||||
s_maybe_reset_text = pyqtSignal(str)
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return utils.get_repr(self)
|
||||
|
||||
def set_text(self, text, *, log_stack=False):
|
||||
def set_text(self, text: str, *, log_stack: bool = False) -> None:
|
||||
"""Set the normal text of the statusbar.
|
||||
|
||||
Args:
|
||||
|
|
@ -270,7 +284,7 @@ class MessageBridge(QObject):
|
|||
log.message.debug(text)
|
||||
self.s_set_text.emit(text)
|
||||
|
||||
def maybe_reset_text(self, text, *, log_stack=False):
|
||||
def maybe_reset_text(self, text: str, *, log_stack: bool = False) -> None:
|
||||
"""Reset the text in the statusbar if it matches an expected text.
|
||||
|
||||
Args:
|
||||
|
|
|
|||
|
|
@ -22,20 +22,18 @@
|
|||
|
||||
import collections
|
||||
import functools
|
||||
import typing
|
||||
|
||||
from PyQt5.QtCore import QObject, QTimer
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtWidgets import QWidget # pylint: disable=unused-import
|
||||
|
||||
from qutebrowser.utils import log
|
||||
from qutebrowser.utils import log, usertypes
|
||||
if typing.TYPE_CHECKING:
|
||||
from qutebrowser.mainwindow import mainwindow
|
||||
|
||||
|
||||
class UnsetObject:
|
||||
|
||||
"""Class for an unset object.
|
||||
|
||||
Only used (rather than object) so we can tell pylint to shut up about it.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
_WindowTab = typing.Union[str, int, None]
|
||||
|
||||
|
||||
class RegistryUnavailableError(Exception):
|
||||
|
|
@ -48,7 +46,9 @@ class NoWindow(Exception):
|
|||
"""Exception raised by last_window if no window is available."""
|
||||
|
||||
|
||||
_UNSET = UnsetObject()
|
||||
class CommandOnlyError(Exception):
|
||||
|
||||
"""Raised when an object is requested which is used for commands only."""
|
||||
|
||||
|
||||
class ObjectRegistry(collections.UserDict):
|
||||
|
|
@ -59,13 +59,16 @@ class ObjectRegistry(collections.UserDict):
|
|||
|
||||
Attributes:
|
||||
_partial_objs: A dictionary of the connected partial objects.
|
||||
command_only: Objects which are only registered for commands.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._partial_objs = {}
|
||||
self._partial_objs = {
|
||||
} # type: typing.MutableMapping[str, typing.Callable[[], None]]
|
||||
self.command_only = [] # type: typing.MutableSequence[str]
|
||||
|
||||
def __setitem__(self, name, obj):
|
||||
def __setitem__(self, name: str, obj: typing.Any) -> None:
|
||||
"""Register an object in the object registry.
|
||||
|
||||
Sets a slot to remove QObjects when they are destroyed.
|
||||
|
|
@ -80,17 +83,17 @@ class ObjectRegistry(collections.UserDict):
|
|||
|
||||
if isinstance(obj, QObject):
|
||||
func = functools.partial(self.on_destroyed, name)
|
||||
obj.destroyed.connect(func)
|
||||
obj.destroyed.connect(func) # type: ignore
|
||||
self._partial_objs[name] = func
|
||||
|
||||
super().__setitem__(name, obj)
|
||||
|
||||
def __delitem__(self, name):
|
||||
def __delitem__(self, name: str) -> None:
|
||||
"""Extend __delitem__ to disconnect the destroyed signal."""
|
||||
self._disconnect_destroyed(name)
|
||||
super().__delitem__(name)
|
||||
|
||||
def _disconnect_destroyed(self, name):
|
||||
def _disconnect_destroyed(self, name: str) -> None:
|
||||
"""Disconnect the destroyed slot if it was connected."""
|
||||
try:
|
||||
partial_objs = self._partial_objs
|
||||
|
|
@ -109,7 +112,7 @@ class ObjectRegistry(collections.UserDict):
|
|||
pass
|
||||
del partial_objs[name]
|
||||
|
||||
def on_destroyed(self, name):
|
||||
def on_destroyed(self, name: str) -> None:
|
||||
"""Schedule removing of a destroyed QObject.
|
||||
|
||||
We don't remove the destroyed object immediately because it might still
|
||||
|
|
@ -119,7 +122,7 @@ class ObjectRegistry(collections.UserDict):
|
|||
log.destroy.debug("schedule removal: {}".format(name))
|
||||
QTimer.singleShot(0, functools.partial(self._on_destroyed, name))
|
||||
|
||||
def _on_destroyed(self, name):
|
||||
def _on_destroyed(self, name: str) -> None:
|
||||
"""Remove a destroyed QObject."""
|
||||
log.destroy.debug("removed: {}".format(name))
|
||||
if not hasattr(self, 'data'):
|
||||
|
|
@ -133,7 +136,7 @@ class ObjectRegistry(collections.UserDict):
|
|||
except KeyError:
|
||||
pass
|
||||
|
||||
def dump_objects(self):
|
||||
def dump_objects(self) -> typing.Sequence[str]:
|
||||
"""Dump all objects as a list of strings."""
|
||||
lines = []
|
||||
for name, obj in self.data.items():
|
||||
|
|
@ -142,7 +145,9 @@ class ObjectRegistry(collections.UserDict):
|
|||
except RuntimeError:
|
||||
# Underlying object deleted probably
|
||||
obj_repr = '<deleted>'
|
||||
lines.append("{}: {}".format(name, obj_repr))
|
||||
suffix = (" (for commands only)" if name in self.command_only
|
||||
else "")
|
||||
lines.append("{}: {}{}".format(name, obj_repr, suffix))
|
||||
return lines
|
||||
|
||||
|
||||
|
|
@ -152,13 +157,13 @@ global_registry = ObjectRegistry()
|
|||
window_registry = ObjectRegistry()
|
||||
|
||||
|
||||
def _get_tab_registry(win_id, tab_id):
|
||||
def _get_tab_registry(win_id: _WindowTab,
|
||||
tab_id: _WindowTab) -> ObjectRegistry:
|
||||
"""Get the registry of a tab."""
|
||||
if tab_id is None:
|
||||
raise ValueError("Got tab_id None (win_id {})".format(win_id))
|
||||
if tab_id == 'current' and win_id is None:
|
||||
app = get('app')
|
||||
window = app.activeWindow()
|
||||
window = QApplication.activeWindow() # type: typing.Optional[QWidget]
|
||||
if window is None or not hasattr(window, 'win_id'):
|
||||
raise RegistryUnavailableError('tab')
|
||||
win_id = window.win_id
|
||||
|
|
@ -180,27 +185,32 @@ def _get_tab_registry(win_id, tab_id):
|
|||
raise RegistryUnavailableError('tab')
|
||||
|
||||
|
||||
def _get_window_registry(window):
|
||||
def _get_window_registry(window: _WindowTab) -> ObjectRegistry:
|
||||
"""Get the registry of a window."""
|
||||
if window is None:
|
||||
raise TypeError("window is None with scope window!")
|
||||
try:
|
||||
if window == 'current':
|
||||
app = get('app')
|
||||
win = app.activeWindow()
|
||||
win = QApplication.activeWindow() # type: typing.Optional[QWidget]
|
||||
elif window == 'last-focused':
|
||||
win = last_focused_window()
|
||||
else:
|
||||
win = window_registry[window]
|
||||
except (KeyError, NoWindow):
|
||||
win = None
|
||||
|
||||
if win is None:
|
||||
raise RegistryUnavailableError('window')
|
||||
|
||||
try:
|
||||
return win.registry
|
||||
except AttributeError:
|
||||
raise RegistryUnavailableError('window')
|
||||
|
||||
|
||||
def _get_registry(scope, window=None, tab=None):
|
||||
def _get_registry(scope: str,
|
||||
window: _WindowTab = None,
|
||||
tab: _WindowTab = None) -> ObjectRegistry:
|
||||
"""Get the correct registry for a given scope."""
|
||||
if window is not None and scope not in ['window', 'tab']:
|
||||
raise TypeError("window is set with scope {}".format(scope))
|
||||
|
|
@ -216,24 +226,39 @@ def _get_registry(scope, window=None, tab=None):
|
|||
raise ValueError("Invalid scope '{}'!".format(scope))
|
||||
|
||||
|
||||
def get(name, default=_UNSET, scope='global', window=None, tab=None):
|
||||
def get(name: str,
|
||||
default: typing.Any = usertypes.UNSET,
|
||||
scope: str = 'global',
|
||||
window: _WindowTab = None,
|
||||
tab: _WindowTab = None,
|
||||
from_command: bool = False) -> typing.Any:
|
||||
"""Helper function to get an object.
|
||||
|
||||
Args:
|
||||
default: A default to return if the object does not exist.
|
||||
"""
|
||||
reg = _get_registry(scope, window, tab)
|
||||
if name in reg.command_only and not from_command:
|
||||
raise CommandOnlyError("{} is only registered for commands"
|
||||
.format(name))
|
||||
|
||||
try:
|
||||
return reg[name]
|
||||
except KeyError:
|
||||
if default is not _UNSET:
|
||||
if default is not usertypes.UNSET:
|
||||
return default
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
def register(name, obj, update=False, scope=None, registry=None, window=None,
|
||||
tab=None):
|
||||
def register(name: str,
|
||||
obj: typing.Any,
|
||||
update: bool = False,
|
||||
scope: str = None,
|
||||
registry: ObjectRegistry = None,
|
||||
window: _WindowTab = None,
|
||||
tab: _WindowTab = None,
|
||||
command_only: bool = False) -> None:
|
||||
"""Helper function to register an object.
|
||||
|
||||
Args:
|
||||
|
|
@ -244,25 +269,33 @@ def register(name, obj, update=False, scope=None, registry=None, window=None,
|
|||
if scope is not None and registry is not None:
|
||||
raise ValueError("scope ({}) and registry ({}) can't be given at the "
|
||||
"same time!".format(scope, registry))
|
||||
|
||||
if registry is not None:
|
||||
reg = registry
|
||||
else:
|
||||
if scope is None:
|
||||
scope = 'global'
|
||||
reg = _get_registry(scope, window, tab)
|
||||
|
||||
if not update and name in reg:
|
||||
raise KeyError("Object '{}' is already registered ({})!".format(
|
||||
name, repr(reg[name])))
|
||||
reg[name] = obj
|
||||
|
||||
if command_only:
|
||||
reg.command_only.append(name)
|
||||
|
||||
def delete(name, scope='global', window=None, tab=None):
|
||||
|
||||
def delete(name: str,
|
||||
scope: str = 'global',
|
||||
window: _WindowTab = None,
|
||||
tab: _WindowTab = None) -> None:
|
||||
"""Helper function to unregister an object."""
|
||||
reg = _get_registry(scope, window, tab)
|
||||
del reg[name]
|
||||
|
||||
|
||||
def dump_objects():
|
||||
def dump_objects() -> typing.Sequence[str]:
|
||||
"""Get all registered objects in all registries as a string."""
|
||||
blocks = []
|
||||
lines = []
|
||||
|
|
@ -275,16 +308,16 @@ def dump_objects():
|
|||
dump = tab.registry.dump_objects()
|
||||
data = [' ' + line for line in dump]
|
||||
blocks.append((' tab-{}'.format(tab_id), data))
|
||||
for name, data in blocks:
|
||||
for name, block_data in blocks:
|
||||
lines.append("")
|
||||
lines.append("{} object registry - {} objects:".format(
|
||||
name, len(data)))
|
||||
for line in data:
|
||||
name, len(block_data)))
|
||||
for line in block_data:
|
||||
lines.append(" {}".format(line))
|
||||
return lines
|
||||
|
||||
|
||||
def last_visible_window():
|
||||
def last_visible_window() -> 'mainwindow.MainWindow':
|
||||
"""Get the last visible window, or the last focused window if none."""
|
||||
try:
|
||||
return get('last-visible-main-window')
|
||||
|
|
@ -292,7 +325,7 @@ def last_visible_window():
|
|||
return last_focused_window()
|
||||
|
||||
|
||||
def last_focused_window():
|
||||
def last_focused_window() -> 'mainwindow.MainWindow':
|
||||
"""Get the last focused window, or the last window if none."""
|
||||
try:
|
||||
return get('last-focused-main-window')
|
||||
|
|
@ -300,7 +333,7 @@ def last_focused_window():
|
|||
return window_by_index(-1)
|
||||
|
||||
|
||||
def window_by_index(idx):
|
||||
def window_by_index(idx: int) -> 'mainwindow.MainWindow':
|
||||
"""Get the Nth opened window object."""
|
||||
if not window_registry:
|
||||
raise NoWindow()
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# FIXME:typing Can we have less "# type: ignore" in here?
|
||||
|
||||
"""Misc. utilities related to Qt.
|
||||
|
||||
Module attributes:
|
||||
|
|
@ -116,7 +118,7 @@ def is_new_qtwebkit() -> bool:
|
|||
pkg_resources.parse_version('538.1'))
|
||||
|
||||
|
||||
def is_single_process():
|
||||
def is_single_process() -> bool:
|
||||
"""Check whether QtWebEngine is running in single-process mode."""
|
||||
if objects.backend == usertypes.Backend.QtWebKit:
|
||||
return False
|
||||
|
|
@ -199,7 +201,11 @@ def deserialize_stream(stream: QDataStream, obj: QObject) -> None:
|
|||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def savefile_open(filename, binary=False, encoding='utf-8'):
|
||||
def savefile_open(
|
||||
filename: str,
|
||||
binary: bool = False,
|
||||
encoding: str = 'utf-8'
|
||||
) -> typing.Iterator[typing.Union['PyQIODevice', io.TextIOWrapper]]:
|
||||
"""Context manager to easily use a QSaveFile."""
|
||||
f = QSaveFile(filename)
|
||||
cancelled = False
|
||||
|
|
@ -209,9 +215,11 @@ def savefile_open(filename, binary=False, encoding='utf-8'):
|
|||
raise QtOSError(f)
|
||||
|
||||
if binary:
|
||||
new_f = PyQIODevice(f)
|
||||
new_f = PyQIODevice(
|
||||
f) # type: typing.Union[PyQIODevice, io.TextIOWrapper]
|
||||
else:
|
||||
new_f = io.TextIOWrapper(PyQIODevice(f), encoding=encoding)
|
||||
new_f = io.TextIOWrapper(PyQIODevice(f), # type: ignore
|
||||
encoding=encoding)
|
||||
|
||||
yield new_f
|
||||
|
||||
|
|
@ -268,7 +276,7 @@ class PyQIODevice(io.BufferedIOBase):
|
|||
if not self.writable():
|
||||
raise OSError("Trying to write to unwritable file!")
|
||||
|
||||
def open(self, mode):
|
||||
def open(self, mode: QIODevice.OpenMode) -> contextlib.closing:
|
||||
"""Open the underlying device and ensure opening succeeded.
|
||||
|
||||
Raises OSError if opening failed.
|
||||
|
|
@ -289,10 +297,10 @@ class PyQIODevice(io.BufferedIOBase):
|
|||
"""Close the underlying device."""
|
||||
self.dev.close()
|
||||
|
||||
def fileno(self):
|
||||
def fileno(self) -> int:
|
||||
raise io.UnsupportedOperation
|
||||
|
||||
def seek(self, offset, whence=io.SEEK_SET):
|
||||
def seek(self, offset: int, whence: int = io.SEEK_SET) -> int:
|
||||
self._check_open()
|
||||
self._check_random()
|
||||
if whence == io.SEEK_SET:
|
||||
|
|
@ -307,7 +315,9 @@ class PyQIODevice(io.BufferedIOBase):
|
|||
if not ok:
|
||||
raise QtOSError(self.dev, msg="seek failed!")
|
||||
|
||||
def truncate(self, size=None):
|
||||
return self.dev.pos()
|
||||
|
||||
def truncate(self, size: int = None) -> int:
|
||||
raise io.UnsupportedOperation
|
||||
|
||||
@property
|
||||
|
|
@ -325,7 +335,7 @@ class PyQIODevice(io.BufferedIOBase):
|
|||
def readable(self) -> bool:
|
||||
return self.dev.isReadable()
|
||||
|
||||
def readline(self, size=-1):
|
||||
def readline(self, size: int = -1) -> QByteArray:
|
||||
self._check_open()
|
||||
self._check_readable()
|
||||
|
||||
|
|
@ -346,7 +356,7 @@ class PyQIODevice(io.BufferedIOBase):
|
|||
|
||||
if buf is None:
|
||||
raise QtOSError(self.dev)
|
||||
return buf
|
||||
return buf # type: ignore
|
||||
|
||||
def seekable(self) -> bool:
|
||||
return not self.dev.isSequential()
|
||||
|
|
@ -359,23 +369,26 @@ class PyQIODevice(io.BufferedIOBase):
|
|||
def writable(self) -> bool:
|
||||
return self.dev.isWritable()
|
||||
|
||||
def write(self, b: bytes) -> int:
|
||||
def write(self, data: str) -> int: # type: ignore
|
||||
self._check_open()
|
||||
self._check_writable()
|
||||
num = self.dev.write(b)
|
||||
if num == -1 or num < len(b):
|
||||
num = self.dev.write(data) # type: ignore
|
||||
if num == -1 or num < len(data):
|
||||
raise QtOSError(self.dev)
|
||||
return num
|
||||
|
||||
def read(self, size=-1):
|
||||
def read(self, size: typing.Optional[int] = None) -> QByteArray:
|
||||
self._check_open()
|
||||
self._check_readable()
|
||||
if size < 0:
|
||||
|
||||
if size in [None, -1]:
|
||||
buf = self.dev.readAll()
|
||||
else:
|
||||
buf = self.dev.read(size)
|
||||
buf = self.dev.read(size) # type: ignore
|
||||
|
||||
if buf is None:
|
||||
raise QtOSError(self.dev)
|
||||
|
||||
return buf
|
||||
|
||||
|
||||
|
|
@ -405,11 +418,14 @@ class EventLoop(QEventLoop):
|
|||
super().__init__(parent)
|
||||
self._executing = False
|
||||
|
||||
def exec_(self, flags=QEventLoop.AllEvents):
|
||||
def exec_(
|
||||
self,
|
||||
flags: QEventLoop.ProcessEventsFlag = QEventLoop.AllEvents
|
||||
) -> int:
|
||||
"""Override exec_ to raise an exception when re-running."""
|
||||
if self._executing:
|
||||
raise AssertionError("Eventloop is already running!")
|
||||
self._executing = True
|
||||
status = super().exec_(flags)
|
||||
status = super().exec_(flags) # type: ignore
|
||||
self._executing = False
|
||||
return status
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ import sys
|
|||
import shutil
|
||||
import contextlib
|
||||
import enum
|
||||
import argparse
|
||||
import typing
|
||||
|
||||
from PyQt5.QtCore import QStandardPaths
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
|
@ -58,7 +60,7 @@ class EmptyValueError(Exception):
|
|||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _unset_organization():
|
||||
def _unset_organization() -> typing.Iterator[None]:
|
||||
"""Temporarily unset QApplication.organizationName().
|
||||
|
||||
This is primarily needed in config.py.
|
||||
|
|
@ -66,7 +68,7 @@ def _unset_organization():
|
|||
qapp = QApplication.instance()
|
||||
if qapp is not None:
|
||||
orgname = qapp.organizationName()
|
||||
qapp.setOrganizationName(None)
|
||||
qapp.setOrganizationName(None) # type: ignore
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
|
|
@ -74,36 +76,38 @@ def _unset_organization():
|
|||
qapp.setOrganizationName(orgname)
|
||||
|
||||
|
||||
def _init_config(args):
|
||||
def _init_config(args: typing.Optional[argparse.Namespace]) -> None:
|
||||
"""Initialize the location for configs."""
|
||||
typ = QStandardPaths.ConfigLocation
|
||||
overridden, path = _from_args(typ, args)
|
||||
if not overridden:
|
||||
path = _from_args(typ, args)
|
||||
if path is None:
|
||||
if utils.is_windows:
|
||||
app_data_path = _writable_location(
|
||||
QStandardPaths.AppDataLocation)
|
||||
path = os.path.join(app_data_path, 'config')
|
||||
else:
|
||||
path = _writable_location(typ)
|
||||
|
||||
_create(path)
|
||||
_locations[_Location.config] = path
|
||||
_locations[_Location.auto_config] = path
|
||||
|
||||
# Override the normal (non-auto) config on macOS
|
||||
if utils.is_mac:
|
||||
overridden, path = _from_args(typ, args)
|
||||
if not overridden: # pragma: no branch
|
||||
path = _from_args(typ, args)
|
||||
if path is None: # pragma: no branch
|
||||
path = os.path.expanduser('~/.' + APPNAME)
|
||||
_create(path)
|
||||
_locations[_Location.config] = path
|
||||
|
||||
config_py_file = os.path.join(_locations[_Location.config], 'config.py')
|
||||
if getattr(args, 'config_py', None) is not None:
|
||||
assert args is not None
|
||||
config_py_file = os.path.abspath(args.config_py)
|
||||
_locations[_Location.config_py] = config_py_file
|
||||
|
||||
|
||||
def config(auto=False):
|
||||
def config(auto: bool = False) -> str:
|
||||
"""Get the location for the config directory.
|
||||
|
||||
If auto=True is given, get the location for the autoconfig.yml directory,
|
||||
|
|
@ -123,11 +127,11 @@ def config_py() -> str:
|
|||
return _locations[_Location.config_py]
|
||||
|
||||
|
||||
def _init_data(args):
|
||||
def _init_data(args: typing.Optional[argparse.Namespace]) -> None:
|
||||
"""Initialize the location for data."""
|
||||
typ = QStandardPaths.DataLocation
|
||||
overridden, path = _from_args(typ, args)
|
||||
if not overridden:
|
||||
path = _from_args(typ, args)
|
||||
if path is None:
|
||||
if utils.is_windows:
|
||||
app_data_path = _writable_location(QStandardPaths.AppDataLocation)
|
||||
path = os.path.join(app_data_path, 'data')
|
||||
|
|
@ -137,6 +141,7 @@ def _init_data(args):
|
|||
path = os.path.join(config_path, 'data')
|
||||
else:
|
||||
path = _writable_location(typ)
|
||||
|
||||
_create(path)
|
||||
_locations[_Location.data] = path
|
||||
|
||||
|
|
@ -148,7 +153,7 @@ def _init_data(args):
|
|||
_locations[_Location.system_data] = path
|
||||
|
||||
|
||||
def data(system=False):
|
||||
def data(system: bool = False) -> str:
|
||||
"""Get the data directory.
|
||||
|
||||
If system=True is given, gets the system-wide (probably non-writable) data
|
||||
|
|
@ -162,43 +167,44 @@ def data(system=False):
|
|||
return _locations[_Location.data]
|
||||
|
||||
|
||||
def _init_cache(args):
|
||||
def _init_cache(args: typing.Optional[argparse.Namespace]) -> None:
|
||||
"""Initialize the location for the cache."""
|
||||
typ = QStandardPaths.CacheLocation
|
||||
overridden, path = _from_args(typ, args)
|
||||
if not overridden:
|
||||
path = _from_args(typ, args)
|
||||
if path is None:
|
||||
if utils.is_windows:
|
||||
# Local, not Roaming!
|
||||
data_path = _writable_location(QStandardPaths.DataLocation)
|
||||
path = os.path.join(data_path, 'cache')
|
||||
else:
|
||||
path = _writable_location(typ)
|
||||
|
||||
_create(path)
|
||||
_locations[_Location.cache] = path
|
||||
|
||||
|
||||
def cache():
|
||||
def cache() -> str:
|
||||
return _locations[_Location.cache]
|
||||
|
||||
|
||||
def _init_download(args):
|
||||
def _init_download(args: typing.Optional[argparse.Namespace]) -> None:
|
||||
"""Initialize the location for downloads.
|
||||
|
||||
Note this is only the default directory as found by Qt.
|
||||
Therefore, we also don't create it.
|
||||
"""
|
||||
typ = QStandardPaths.DownloadLocation
|
||||
overridden, path = _from_args(typ, args)
|
||||
if not overridden:
|
||||
path = _from_args(typ, args)
|
||||
if path is None:
|
||||
path = _writable_location(typ)
|
||||
_locations[_Location.download] = path
|
||||
|
||||
|
||||
def download():
|
||||
def download() -> str:
|
||||
return _locations[_Location.download]
|
||||
|
||||
|
||||
def _init_runtime(args):
|
||||
def _init_runtime(args: typing.Optional[argparse.Namespace]) -> None:
|
||||
"""Initialize location for runtime data."""
|
||||
if utils.is_mac or utils.is_windows:
|
||||
# RuntimeLocation is a weird path on macOS and Windows.
|
||||
|
|
@ -206,9 +212,8 @@ def _init_runtime(args):
|
|||
else:
|
||||
typ = QStandardPaths.RuntimeLocation
|
||||
|
||||
overridden, path = _from_args(typ, args)
|
||||
|
||||
if not overridden:
|
||||
path = _from_args(typ, args)
|
||||
if path is None:
|
||||
try:
|
||||
path = _writable_location(typ)
|
||||
except EmptyValueError:
|
||||
|
|
@ -231,11 +236,11 @@ def _init_runtime(args):
|
|||
_locations[_Location.runtime] = path
|
||||
|
||||
|
||||
def runtime():
|
||||
def runtime() -> str:
|
||||
return _locations[_Location.runtime]
|
||||
|
||||
|
||||
def _writable_location(typ):
|
||||
def _writable_location(typ: QStandardPaths.StandardLocation) -> str:
|
||||
"""Wrapper around QStandardPaths.writableLocation.
|
||||
|
||||
Arguments:
|
||||
|
|
@ -271,17 +276,14 @@ def _writable_location(typ):
|
|||
return path
|
||||
|
||||
|
||||
def _from_args(typ, args):
|
||||
def _from_args(
|
||||
typ: QStandardPaths.StandardLocation,
|
||||
args: typing.Optional[argparse.Namespace]
|
||||
) -> typing.Optional[str]:
|
||||
"""Get the standard directory from an argparse namespace.
|
||||
|
||||
Args:
|
||||
typ: A member of the QStandardPaths::StandardLocation enum
|
||||
args: An argparse namespace or None.
|
||||
|
||||
Return:
|
||||
A (override, path) tuple.
|
||||
override: boolean, if the user did override the path
|
||||
path: The overridden path, or None to turn off storage.
|
||||
The overridden path, or None if there is no override.
|
||||
"""
|
||||
basedir_suffix = {
|
||||
QStandardPaths.ConfigLocation: 'config',
|
||||
|
|
@ -291,19 +293,18 @@ def _from_args(typ, args):
|
|||
QStandardPaths.RuntimeLocation: 'runtime',
|
||||
}
|
||||
|
||||
if getattr(args, 'basedir', None) is not None:
|
||||
basedir = args.basedir
|
||||
if getattr(args, 'basedir', None) is None:
|
||||
return None
|
||||
assert args is not None
|
||||
|
||||
try:
|
||||
suffix = basedir_suffix[typ]
|
||||
except KeyError: # pragma: no cover
|
||||
return (False, None)
|
||||
return (True, os.path.abspath(os.path.join(basedir, suffix)))
|
||||
else:
|
||||
return (False, None)
|
||||
try:
|
||||
suffix = basedir_suffix[typ]
|
||||
except KeyError: # pragma: no cover
|
||||
return None
|
||||
return os.path.abspath(os.path.join(args.basedir, suffix))
|
||||
|
||||
|
||||
def _create(path):
|
||||
def _create(path: str) -> None:
|
||||
"""Create the `path` directory.
|
||||
|
||||
From the XDG basedir spec:
|
||||
|
|
@ -315,7 +316,7 @@ def _create(path):
|
|||
os.makedirs(path, 0o700, exist_ok=True)
|
||||
|
||||
|
||||
def _init_dirs(args=None):
|
||||
def _init_dirs(args: argparse.Namespace = None) -> None:
|
||||
"""Create and cache standard directory locations.
|
||||
|
||||
Mainly in a separate function because we need to call it in tests.
|
||||
|
|
@ -327,7 +328,7 @@ def _init_dirs(args=None):
|
|||
_init_runtime(args)
|
||||
|
||||
|
||||
def init(args):
|
||||
def init(args: typing.Optional[argparse.Namespace]) -> None:
|
||||
"""Initialize all standard dirs."""
|
||||
if args is not None:
|
||||
# args can be None during tests
|
||||
|
|
@ -342,7 +343,7 @@ def init(args):
|
|||
_move_windows()
|
||||
|
||||
|
||||
def _move_macos():
|
||||
def _move_macos() -> None:
|
||||
"""Move most config files to new location on macOS."""
|
||||
old_config = config(auto=True) # ~/Library/Preferences/qutebrowser
|
||||
new_config = config() # ~/.qutebrowser
|
||||
|
|
@ -352,7 +353,7 @@ def _move_macos():
|
|||
os.path.join(new_config, f))
|
||||
|
||||
|
||||
def _move_windows():
|
||||
def _move_windows() -> None:
|
||||
"""Move the whole qutebrowser directory from Local to Roaming AppData."""
|
||||
# %APPDATA%\Local\qutebrowser
|
||||
old_appdata_dir = _writable_location(QStandardPaths.DataLocation)
|
||||
|
|
@ -375,7 +376,7 @@ def _move_windows():
|
|||
os.path.join(new_config_dir, f))
|
||||
|
||||
|
||||
def _init_cachedir_tag():
|
||||
def _init_cachedir_tag() -> None:
|
||||
"""Create CACHEDIR.TAG if it doesn't exist.
|
||||
|
||||
See http://www.brynosaurus.com/cachedir/spec.html
|
||||
|
|
@ -394,7 +395,7 @@ def _init_cachedir_tag():
|
|||
log.init.exception("Failed to create CACHEDIR.TAG")
|
||||
|
||||
|
||||
def _move_data(old, new):
|
||||
def _move_data(old: str, new: str) -> bool:
|
||||
"""Migrate data from an old to a new directory.
|
||||
|
||||
If the old directory does not exist, the migration is skipped.
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ https://cs.chromium.org/chromium/src/extensions/common/url_pattern.h
|
|||
|
||||
import ipaddress
|
||||
import fnmatch
|
||||
import typing
|
||||
import urllib.parse
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
|
|
@ -64,15 +65,15 @@ class UrlPattern:
|
|||
_DEFAULT_PORTS = {'https': 443, 'http': 80, 'ftp': 21}
|
||||
_SCHEMES_WITHOUT_HOST = ['about', 'file', 'data', 'javascript']
|
||||
|
||||
def __init__(self, pattern):
|
||||
def __init__(self, pattern: str) -> None:
|
||||
# Make sure all attributes are initialized if we exit early.
|
||||
self._pattern = pattern
|
||||
self._match_all = False
|
||||
self._match_subdomains = False
|
||||
self._scheme = None
|
||||
self._host = None
|
||||
self._path = None
|
||||
self._port = None
|
||||
self._scheme = None # type: typing.Optional[str]
|
||||
self._host = None # type: typing.Optional[str]
|
||||
self._path = None # type: typing.Optional[str]
|
||||
self._port = None # type: typing.Optional[int]
|
||||
|
||||
# > The special pattern <all_urls> matches any URL that starts with a
|
||||
# > permitted scheme.
|
||||
|
|
@ -99,26 +100,26 @@ class UrlPattern:
|
|||
self._init_path(parsed)
|
||||
self._init_port(parsed)
|
||||
|
||||
def _to_tuple(self):
|
||||
def _to_tuple(self) -> typing.Tuple:
|
||||
"""Get a pattern with information used for __eq__/__hash__."""
|
||||
return (self._match_all, self._match_subdomains, self._scheme,
|
||||
self._host, self._path, self._port)
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(self._to_tuple())
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: typing.Any) -> bool:
|
||||
if not isinstance(other, UrlPattern):
|
||||
return NotImplemented
|
||||
return self._to_tuple() == other._to_tuple()
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return utils.get_repr(self, pattern=self._pattern, constructor=True)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self._pattern
|
||||
|
||||
def _fixup_pattern(self, pattern):
|
||||
def _fixup_pattern(self, pattern: str) -> str:
|
||||
"""Make sure the given pattern is parseable by urllib.parse."""
|
||||
if pattern.startswith('*:'): # Any scheme, but *:// is unparseable
|
||||
pattern = 'any:' + pattern[2:]
|
||||
|
|
@ -135,7 +136,7 @@ class UrlPattern:
|
|||
|
||||
return pattern
|
||||
|
||||
def _init_scheme(self, parsed):
|
||||
def _init_scheme(self, parsed: urllib.parse.ParseResult) -> None:
|
||||
"""Parse the scheme from the given URL.
|
||||
|
||||
Deviation from Chromium:
|
||||
|
|
@ -150,7 +151,7 @@ class UrlPattern:
|
|||
|
||||
self._scheme = parsed.scheme
|
||||
|
||||
def _init_path(self, parsed):
|
||||
def _init_path(self, parsed: urllib.parse.ParseResult) -> None:
|
||||
"""Parse the path from the given URL.
|
||||
|
||||
Deviation from Chromium:
|
||||
|
|
@ -168,13 +169,15 @@ class UrlPattern:
|
|||
else:
|
||||
self._path = parsed.path
|
||||
|
||||
def _init_host(self, parsed):
|
||||
def _init_host(self, parsed: urllib.parse.ParseResult) -> None:
|
||||
"""Parse the host from the given URL.
|
||||
|
||||
Deviation from Chromium:
|
||||
- http://:1234/ is not a valid URL because it has no host.
|
||||
"""
|
||||
if parsed.hostname is None or not parsed.hostname.strip():
|
||||
# https://github.com/python/typeshed/commit/f0ccb325aa787ca0a539ef9914276b2c3148327a
|
||||
if (parsed.hostname is None or # type: ignore
|
||||
not parsed.hostname.strip()):
|
||||
if self._scheme not in self._SCHEMES_WITHOUT_HOST:
|
||||
raise ParseError("Pattern without host")
|
||||
assert self._host is None
|
||||
|
|
@ -208,7 +211,7 @@ class UrlPattern:
|
|||
# Only * or *.foo is allowed as host.
|
||||
raise ParseError("Invalid host wildcard")
|
||||
|
||||
def _init_port(self, parsed):
|
||||
def _init_port(self, parsed: urllib.parse.ParseResult) -> None:
|
||||
"""Parse the port from the given URL.
|
||||
|
||||
Deviation from Chromium:
|
||||
|
|
@ -225,15 +228,16 @@ class UrlPattern:
|
|||
except ValueError as e:
|
||||
raise ParseError("Invalid port: {}".format(e))
|
||||
|
||||
if (self._scheme not in list(self._DEFAULT_PORTS) + [None] and
|
||||
self._port is not None):
|
||||
scheme_has_port = (self._scheme in list(self._DEFAULT_PORTS) or
|
||||
self._scheme is None)
|
||||
if self._port is not None and not scheme_has_port:
|
||||
raise ParseError("Ports are unsupported with {} scheme".format(
|
||||
self._scheme))
|
||||
|
||||
def _matches_scheme(self, scheme):
|
||||
def _matches_scheme(self, scheme: str) -> bool:
|
||||
return self._scheme is None or self._scheme == scheme
|
||||
|
||||
def _matches_host(self, host):
|
||||
def _matches_host(self, host: str) -> bool:
|
||||
# FIXME what about multiple dots?
|
||||
host = host.rstrip('.')
|
||||
|
||||
|
|
@ -268,12 +272,12 @@ class UrlPattern:
|
|||
|
||||
return host[len(host) - len(self._host) - 1] == '.'
|
||||
|
||||
def _matches_port(self, scheme, port):
|
||||
def _matches_port(self, scheme: str, port: int) -> bool:
|
||||
if port == -1 and scheme in self._DEFAULT_PORTS:
|
||||
port = self._DEFAULT_PORTS[scheme]
|
||||
return self._port is None or self._port == port
|
||||
|
||||
def _matches_path(self, path):
|
||||
def _matches_path(self, path: str) -> bool:
|
||||
if self._path is None:
|
||||
return True
|
||||
|
||||
|
|
@ -285,7 +289,7 @@ class UrlPattern:
|
|||
# doesn't rely on regexes. Do we need that too?
|
||||
return fnmatch.fnmatchcase(path, self._path)
|
||||
|
||||
def matches(self, qurl):
|
||||
def matches(self, qurl: QUrl) -> bool:
|
||||
"""Check if the pattern matches the given QUrl."""
|
||||
qtutils.ensure_valid(qurl)
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import os.path
|
|||
import ipaddress
|
||||
import posixpath
|
||||
import urllib.parse
|
||||
import typing
|
||||
|
||||
from PyQt5.QtCore import QUrl, QUrlQuery
|
||||
from PyQt5.QtNetwork import QHostInfo, QHostAddress, QNetworkProxy
|
||||
|
|
@ -58,7 +59,7 @@ class InvalidUrlError(Exception):
|
|||
|
||||
"""Error raised if a function got an invalid URL."""
|
||||
|
||||
def __init__(self, url):
|
||||
def __init__(self, url: QUrl) -> None:
|
||||
if url.isValid():
|
||||
raise ValueError("Got valid URL {}!".format(url.toDisplayString()))
|
||||
self.url = url
|
||||
|
|
@ -66,7 +67,7 @@ class InvalidUrlError(Exception):
|
|||
super().__init__(self.msg)
|
||||
|
||||
|
||||
def _parse_search_term(s):
|
||||
def _parse_search_term(s: str) -> typing.Tuple[typing.Optional[str], str]:
|
||||
"""Get a search engine name and search term from a string.
|
||||
|
||||
Args:
|
||||
|
|
@ -79,7 +80,7 @@ def _parse_search_term(s):
|
|||
split = s.split(maxsplit=1)
|
||||
|
||||
if len(split) == 2:
|
||||
engine = split[0]
|
||||
engine = split[0] # type: typing.Optional[str]
|
||||
try:
|
||||
config.val.url.searchengines[engine]
|
||||
except KeyError:
|
||||
|
|
@ -97,7 +98,7 @@ def _parse_search_term(s):
|
|||
return (engine, term)
|
||||
|
||||
|
||||
def _get_search_url(txt):
|
||||
def _get_search_url(txt: str) -> QUrl:
|
||||
"""Get a search engine URL for a text.
|
||||
|
||||
Args:
|
||||
|
|
@ -117,14 +118,14 @@ def _get_search_url(txt):
|
|||
|
||||
if config.val.url.open_base_url and term in config.val.url.searchengines:
|
||||
url = qurl_from_user_input(config.val.url.searchengines[term])
|
||||
url.setPath(None)
|
||||
url.setFragment(None)
|
||||
url.setQuery(None)
|
||||
url.setPath(None) # type: ignore
|
||||
url.setFragment(None) # type: ignore
|
||||
url.setQuery(None) # type: ignore
|
||||
qtutils.ensure_valid(url)
|
||||
return url
|
||||
|
||||
|
||||
def _is_url_naive(urlstr):
|
||||
def _is_url_naive(urlstr: str) -> bool:
|
||||
"""Naive check if given URL is really a URL.
|
||||
|
||||
Args:
|
||||
|
|
@ -150,7 +151,7 @@ def _is_url_naive(urlstr):
|
|||
return '.' in host and not host.endswith('.')
|
||||
|
||||
|
||||
def _is_url_dns(urlstr):
|
||||
def _is_url_dns(urlstr: str) -> bool:
|
||||
"""Check if a URL is really a URL via DNS.
|
||||
|
||||
Args:
|
||||
|
|
@ -178,8 +179,11 @@ def _is_url_dns(urlstr):
|
|||
return not info.error()
|
||||
|
||||
|
||||
def fuzzy_url(urlstr, cwd=None, relative=False, do_search=True,
|
||||
force_search=False):
|
||||
def fuzzy_url(urlstr: str,
|
||||
cwd: str = None,
|
||||
relative: bool = False,
|
||||
do_search: bool = True,
|
||||
force_search: bool = False) -> QUrl:
|
||||
"""Get a QUrl based on a user input which is URL or search term.
|
||||
|
||||
Args:
|
||||
|
|
@ -218,7 +222,7 @@ def fuzzy_url(urlstr, cwd=None, relative=False, do_search=True,
|
|||
return url
|
||||
|
||||
|
||||
def _has_explicit_scheme(url):
|
||||
def _has_explicit_scheme(url: QUrl) -> bool:
|
||||
"""Check if a url has an explicit scheme given.
|
||||
|
||||
Args:
|
||||
|
|
@ -228,13 +232,13 @@ def _has_explicit_scheme(url):
|
|||
# after the scheme delimiter. Since we don't know of any URIs
|
||||
# using this and want to support e.g. searching for scoped C++
|
||||
# symbols, we treat this as not a URI anyways.
|
||||
return (url.isValid() and url.scheme() and
|
||||
(url.host() or url.path()) and
|
||||
' ' not in url.path() and
|
||||
not url.path().startswith(':'))
|
||||
return bool(url.isValid() and url.scheme() and
|
||||
(url.host() or url.path()) and
|
||||
' ' not in url.path() and
|
||||
not url.path().startswith(':'))
|
||||
|
||||
|
||||
def is_special_url(url):
|
||||
def is_special_url(url: QUrl) -> bool:
|
||||
"""Return True if url is an about:... or other special URL.
|
||||
|
||||
Args:
|
||||
|
|
@ -246,7 +250,7 @@ def is_special_url(url):
|
|||
return url.scheme() in special_schemes
|
||||
|
||||
|
||||
def is_url(urlstr):
|
||||
def is_url(urlstr: str) -> bool:
|
||||
"""Check if url seems to be a valid URL.
|
||||
|
||||
Args:
|
||||
|
|
@ -303,7 +307,7 @@ def is_url(urlstr):
|
|||
return url
|
||||
|
||||
|
||||
def qurl_from_user_input(urlstr):
|
||||
def qurl_from_user_input(urlstr: str) -> QUrl:
|
||||
"""Get a QUrl based on a user input. Additionally handles IPv6 addresses.
|
||||
|
||||
QUrl.fromUserInput handles something like '::1' as a file URL instead of an
|
||||
|
|
@ -339,12 +343,12 @@ def qurl_from_user_input(urlstr):
|
|||
return QUrl('http://[{}]{}'.format(ipstr, rest))
|
||||
|
||||
|
||||
def ensure_valid(url):
|
||||
def ensure_valid(url: QUrl) -> None:
|
||||
if not url.isValid():
|
||||
raise InvalidUrlError(url)
|
||||
|
||||
|
||||
def invalid_url_error(url, action):
|
||||
def invalid_url_error(url: QUrl, action: str) -> None:
|
||||
"""Display an error message for a URL.
|
||||
|
||||
Args:
|
||||
|
|
@ -358,7 +362,7 @@ def invalid_url_error(url, action):
|
|||
message.error(errstring)
|
||||
|
||||
|
||||
def raise_cmdexc_if_invalid(url):
|
||||
def raise_cmdexc_if_invalid(url: QUrl) -> None:
|
||||
"""Check if the given QUrl is invalid, and if so, raise a CommandError."""
|
||||
try:
|
||||
ensure_valid(url)
|
||||
|
|
@ -366,7 +370,10 @@ def raise_cmdexc_if_invalid(url):
|
|||
raise cmdutils.CommandError(str(e))
|
||||
|
||||
|
||||
def get_path_if_valid(pathstr, cwd=None, relative=False, check_exists=False):
|
||||
def get_path_if_valid(pathstr: str,
|
||||
cwd: str = None,
|
||||
relative: bool = False,
|
||||
check_exists: bool = False) -> typing.Optional[str]:
|
||||
"""Check if path is a valid path.
|
||||
|
||||
Args:
|
||||
|
|
@ -384,7 +391,7 @@ def get_path_if_valid(pathstr, cwd=None, relative=False, check_exists=False):
|
|||
expanded = os.path.expanduser(pathstr)
|
||||
|
||||
if os.path.isabs(expanded):
|
||||
path = expanded
|
||||
path = expanded # type: typing.Optional[str]
|
||||
elif relative and cwd:
|
||||
path = os.path.join(cwd, expanded)
|
||||
elif relative:
|
||||
|
|
@ -411,7 +418,7 @@ def get_path_if_valid(pathstr, cwd=None, relative=False, check_exists=False):
|
|||
return path
|
||||
|
||||
|
||||
def filename_from_url(url):
|
||||
def filename_from_url(url: QUrl) -> typing.Optional[str]:
|
||||
"""Get a suitable filename from a URL.
|
||||
|
||||
Args:
|
||||
|
|
@ -431,7 +438,7 @@ def filename_from_url(url):
|
|||
return None
|
||||
|
||||
|
||||
def host_tuple(url):
|
||||
def host_tuple(url: QUrl) -> typing.Tuple[str, str, int]:
|
||||
"""Get a (scheme, host, port) tuple from a QUrl.
|
||||
|
||||
This is suitable to identify a connection, e.g. for SSL errors.
|
||||
|
|
@ -456,7 +463,7 @@ def host_tuple(url):
|
|||
return scheme, host, port
|
||||
|
||||
|
||||
def get_errstring(url, base="Invalid URL"):
|
||||
def get_errstring(url: QUrl, base: str = "Invalid URL") -> str:
|
||||
"""Get an error string for a URL.
|
||||
|
||||
Args:
|
||||
|
|
@ -473,7 +480,7 @@ def get_errstring(url, base="Invalid URL"):
|
|||
return base
|
||||
|
||||
|
||||
def same_domain(url1, url2):
|
||||
def same_domain(url1: QUrl, url2: QUrl) -> bool:
|
||||
"""Check if url1 and url2 belong to the same website.
|
||||
|
||||
This will use a "public suffix list" to determine what a "top level domain"
|
||||
|
|
@ -501,7 +508,7 @@ def same_domain(url1, url2):
|
|||
return domain1 == domain2
|
||||
|
||||
|
||||
def encoded_url(url):
|
||||
def encoded_url(url: QUrl) -> str:
|
||||
"""Return the fully encoded url as string.
|
||||
|
||||
Args:
|
||||
|
|
@ -510,16 +517,17 @@ def encoded_url(url):
|
|||
return bytes(url.toEncoded()).decode('ascii')
|
||||
|
||||
|
||||
def file_url(path):
|
||||
def file_url(path: str) -> QUrl:
|
||||
"""Return a file:// url (as string) to the given local path.
|
||||
|
||||
Arguments:
|
||||
path: The absolute path to the local file
|
||||
"""
|
||||
return QUrl.fromLocalFile(path).toString(QUrl.FullyEncoded)
|
||||
url = QUrl.fromLocalFile(path)
|
||||
return url.toString(QUrl.FullyEncoded) # type: ignore
|
||||
|
||||
|
||||
def data_url(mimetype, data):
|
||||
def data_url(mimetype: str, data: bytes) -> QUrl:
|
||||
"""Get a data: QUrl for the given data."""
|
||||
b64 = base64.b64encode(data).decode('ascii')
|
||||
url = QUrl('data:{};base64,{}'.format(mimetype, b64))
|
||||
|
|
@ -527,7 +535,7 @@ def data_url(mimetype, data):
|
|||
return url
|
||||
|
||||
|
||||
def safe_display_string(qurl):
|
||||
def safe_display_string(qurl: QUrl) -> str:
|
||||
"""Get a IDN-homograph phishing safe form of the given QUrl.
|
||||
|
||||
If we're dealing with a Punycode-encoded URL, this prepends the hostname in
|
||||
|
|
@ -538,19 +546,20 @@ def safe_display_string(qurl):
|
|||
"""
|
||||
ensure_valid(qurl)
|
||||
|
||||
host = qurl.host(QUrl.FullyEncoded)
|
||||
host = qurl.host(QUrl.FullyEncoded) # type: ignore
|
||||
if '..' in host: # pragma: no cover
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-60364
|
||||
return '(unparseable URL!) {}'.format(qurl.toDisplayString())
|
||||
|
||||
for part in host.split('.'):
|
||||
if part.startswith('xn--') and host != qurl.host(QUrl.FullyDecoded):
|
||||
url_host = qurl.host(QUrl.FullyDecoded) # type: ignore
|
||||
if part.startswith('xn--') and host != url_host:
|
||||
return '({}) {}'.format(host, qurl.toDisplayString())
|
||||
|
||||
return qurl.toDisplayString()
|
||||
|
||||
|
||||
def query_string(qurl):
|
||||
def query_string(qurl: QUrl) -> str:
|
||||
"""Get a query string for the given URL.
|
||||
|
||||
This is a WORKAROUND for:
|
||||
|
|
@ -566,11 +575,11 @@ class InvalidProxyTypeError(Exception):
|
|||
|
||||
"""Error raised when proxy_from_url gets an unknown proxy type."""
|
||||
|
||||
def __init__(self, typ):
|
||||
def __init__(self, typ: str) -> None:
|
||||
super().__init__("Invalid proxy type {}!".format(typ))
|
||||
|
||||
|
||||
def proxy_from_url(url):
|
||||
def proxy_from_url(url: QUrl) -> QNetworkProxy:
|
||||
"""Create a QNetworkProxy from QUrl and a proxy type.
|
||||
|
||||
Args:
|
||||
|
|
|
|||
|
|
@ -17,23 +17,31 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Custom useful data types.
|
||||
|
||||
Module attributes:
|
||||
_UNSET: Used as default argument in the constructor so default can be None.
|
||||
"""
|
||||
"""Custom useful data types."""
|
||||
|
||||
import operator
|
||||
import collections.abc
|
||||
import enum
|
||||
import typing
|
||||
|
||||
import attr
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer
|
||||
from PyQt5.QtCore import QUrl # pylint: disable=unused-import
|
||||
|
||||
from qutebrowser.utils import log, qtutils, utils
|
||||
|
||||
|
||||
_UNSET = object()
|
||||
_T = typing.TypeVar('_T')
|
||||
|
||||
|
||||
class UnsetObject:
|
||||
|
||||
"""Class for an unset object."""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
UNSET = UnsetObject()
|
||||
|
||||
|
||||
class NeighborList(collections.abc.Sequence):
|
||||
|
|
@ -52,7 +60,9 @@ class NeighborList(collections.abc.Sequence):
|
|||
|
||||
Modes = enum.Enum('Modes', ['edge', 'exception'])
|
||||
|
||||
def __init__(self, items=None, default=_UNSET, mode=Modes.exception):
|
||||
def __init__(self, items: typing.Sequence[_T] = None,
|
||||
default: typing.Union[_T, UnsetObject] = UNSET,
|
||||
mode: Modes = Modes.exception) -> None:
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
|
|
@ -65,28 +75,31 @@ class NeighborList(collections.abc.Sequence):
|
|||
if not isinstance(mode, self.Modes):
|
||||
raise TypeError("Mode {} is not a Modes member!".format(mode))
|
||||
if items is None:
|
||||
self._items = []
|
||||
self._items = [] # type: typing.Sequence[_T]
|
||||
else:
|
||||
self._items = list(items)
|
||||
self._default = default
|
||||
if default is not _UNSET:
|
||||
self._idx = self._items.index(default)
|
||||
|
||||
if not isinstance(default, UnsetObject):
|
||||
idx = self._items.index(default)
|
||||
self._idx = idx # type: typing.Optional[int]
|
||||
else:
|
||||
self._idx = None
|
||||
self._mode = mode
|
||||
self.fuzzyval = None
|
||||
|
||||
def __getitem__(self, key):
|
||||
self._mode = mode
|
||||
self.fuzzyval = None # type: typing.Optional[int]
|
||||
|
||||
def __getitem__(self, key: int) -> _T: # type: ignore
|
||||
return self._items[key]
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
return len(self._items)
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return utils.get_repr(self, items=self._items, mode=self._mode,
|
||||
idx=self._idx, fuzzyval=self.fuzzyval)
|
||||
|
||||
def _snap_in(self, offset):
|
||||
def _snap_in(self, offset: int) -> bool:
|
||||
"""Set the current item to the closest item to self.fuzzyval.
|
||||
|
||||
Args:
|
||||
|
|
@ -97,11 +110,15 @@ class NeighborList(collections.abc.Sequence):
|
|||
True if the value snapped in (changed),
|
||||
False when the value already was in the list.
|
||||
"""
|
||||
assert isinstance(self.fuzzyval, (int, float)), self.fuzzyval
|
||||
|
||||
op = operator.le if offset < 0 else operator.ge
|
||||
items = [(idx, e) for (idx, e) in enumerate(self._items)
|
||||
if op(e, self.fuzzyval)]
|
||||
if items:
|
||||
item = min(items, key=lambda tpl: abs(self.fuzzyval - tpl[1]))
|
||||
item = min(
|
||||
items,
|
||||
key=lambda tpl: abs(self.fuzzyval - tpl[1])) # type: ignore
|
||||
else:
|
||||
sorted_items = sorted(((idx, e) for (idx, e) in
|
||||
enumerate(self.items)), key=lambda e: e[1])
|
||||
|
|
@ -110,7 +127,7 @@ class NeighborList(collections.abc.Sequence):
|
|||
self._idx = item[0]
|
||||
return self.fuzzyval not in self._items
|
||||
|
||||
def _get_new_item(self, offset):
|
||||
def _get_new_item(self, offset: int) -> _T:
|
||||
"""Logic for getitem to get the item at offset.
|
||||
|
||||
Args:
|
||||
|
|
@ -119,6 +136,7 @@ class NeighborList(collections.abc.Sequence):
|
|||
Return:
|
||||
The new item.
|
||||
"""
|
||||
assert self._idx is not None
|
||||
try:
|
||||
if self._idx + offset >= 0:
|
||||
new = self._items[self._idx + offset]
|
||||
|
|
@ -138,11 +156,11 @@ class NeighborList(collections.abc.Sequence):
|
|||
return new
|
||||
|
||||
@property
|
||||
def items(self):
|
||||
def items(self) -> typing.Sequence[_T]:
|
||||
"""Getter for items, which should not be set."""
|
||||
return self._items
|
||||
|
||||
def getitem(self, offset):
|
||||
def getitem(self, offset: int) -> _T:
|
||||
"""Get the item with a relative position.
|
||||
|
||||
Args:
|
||||
|
|
@ -167,38 +185,38 @@ class NeighborList(collections.abc.Sequence):
|
|||
self.fuzzyval = None
|
||||
return self._get_new_item(offset)
|
||||
|
||||
def curitem(self):
|
||||
def curitem(self) -> _T:
|
||||
"""Get the current item in the list."""
|
||||
if self._idx is not None:
|
||||
return self._items[self._idx]
|
||||
else:
|
||||
raise IndexError("No current item!")
|
||||
|
||||
def nextitem(self):
|
||||
def nextitem(self) -> _T:
|
||||
"""Get the next item in the list."""
|
||||
return self.getitem(1)
|
||||
|
||||
def previtem(self):
|
||||
def previtem(self) -> _T:
|
||||
"""Get the previous item in the list."""
|
||||
return self.getitem(-1)
|
||||
|
||||
def firstitem(self):
|
||||
def firstitem(self) -> _T:
|
||||
"""Get the first item in the list."""
|
||||
if not self._items:
|
||||
raise IndexError("No items found!")
|
||||
self._idx = 0
|
||||
return self.curitem()
|
||||
|
||||
def lastitem(self):
|
||||
def lastitem(self) -> _T:
|
||||
"""Get the last item in the list."""
|
||||
if not self._items:
|
||||
raise IndexError("No items found!")
|
||||
self._idx = len(self._items) - 1
|
||||
return self.curitem()
|
||||
|
||||
def reset(self):
|
||||
def reset(self) -> _T:
|
||||
"""Reset the position to the default."""
|
||||
if self._default is _UNSET:
|
||||
if self._default is UNSET:
|
||||
raise ValueError("No default set!")
|
||||
self._idx = self._items.index(self._default)
|
||||
return self.curitem()
|
||||
|
|
@ -334,37 +352,25 @@ class Question(QObject):
|
|||
answered_no = pyqtSignal()
|
||||
completed = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, parent: QObject = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._mode = None
|
||||
self.default = None
|
||||
self.title = None
|
||||
self.text = None
|
||||
self.url = None
|
||||
self.option = None
|
||||
self.answer = None
|
||||
self.mode = None # type: typing.Optional[PromptMode]
|
||||
self.default = None # type: typing.Union[bool, str, None]
|
||||
self.title = None # type: typing.Optional[str]
|
||||
self.text = None # type: typing.Optional[str]
|
||||
self.url = None # type: typing.Optional[QUrl]
|
||||
self.option = None # type: typing.Optional[bool]
|
||||
self.answer = None # type: typing.Union[str, bool, None]
|
||||
self.is_aborted = False
|
||||
self.interrupted = False
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return utils.get_repr(self, title=self.title, text=self.text,
|
||||
mode=self._mode, default=self.default,
|
||||
mode=self.mode, default=self.default,
|
||||
option=self.option)
|
||||
|
||||
@property
|
||||
def mode(self):
|
||||
"""Getter for mode so we can define a setter."""
|
||||
return self._mode
|
||||
|
||||
@mode.setter
|
||||
def mode(self, val):
|
||||
"""Setter for mode to do basic type checking."""
|
||||
if not isinstance(val, PromptMode):
|
||||
raise TypeError("Mode {} is no PromptMode member!".format(val))
|
||||
self._mode = val
|
||||
|
||||
@pyqtSlot()
|
||||
def done(self):
|
||||
def done(self) -> None:
|
||||
"""Must be called when the question was answered completely."""
|
||||
self.answered.emit(self.answer)
|
||||
if self.mode == PromptMode.yesno:
|
||||
|
|
@ -375,13 +381,13 @@ class Question(QObject):
|
|||
self.completed.emit()
|
||||
|
||||
@pyqtSlot()
|
||||
def cancel(self):
|
||||
def cancel(self) -> None:
|
||||
"""Cancel the question (resulting from user-input)."""
|
||||
self.cancelled.emit()
|
||||
self.completed.emit()
|
||||
|
||||
@pyqtSlot()
|
||||
def abort(self):
|
||||
def abort(self) -> None:
|
||||
"""Abort the question."""
|
||||
if self.is_aborted:
|
||||
log.misc.debug("Question was already aborted")
|
||||
|
|
@ -399,7 +405,7 @@ class Timer(QTimer):
|
|||
_name: The name of the timer.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None, name=None):
|
||||
def __init__(self, parent: QObject = None, name: str = None) -> None:
|
||||
super().__init__(parent)
|
||||
if name is None:
|
||||
self._name = "unnamed"
|
||||
|
|
@ -407,15 +413,15 @@ class Timer(QTimer):
|
|||
self.setObjectName(name)
|
||||
self._name = name
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return utils.get_repr(self, name=self._name)
|
||||
|
||||
def setInterval(self, msec):
|
||||
def setInterval(self, msec: int) -> None:
|
||||
"""Extend setInterval to check for overflows."""
|
||||
qtutils.check_overflow(msec, 'int')
|
||||
super().setInterval(msec)
|
||||
|
||||
def start(self, msec=None):
|
||||
def start(self, msec: int = None) -> None:
|
||||
"""Extend start to check for overflows."""
|
||||
if msec is not None:
|
||||
qtutils.check_overflow(msec, 'int')
|
||||
|
|
@ -428,16 +434,16 @@ class AbstractCertificateErrorWrapper:
|
|||
|
||||
"""A wrapper over an SSL/certificate error."""
|
||||
|
||||
def __init__(self, error):
|
||||
def __init__(self, error: typing.Any) -> None:
|
||||
self._error = error
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def is_overridable(self):
|
||||
def is_overridable(self) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
|
|
@ -457,7 +463,7 @@ class NavigationRequest:
|
|||
'other'
|
||||
])
|
||||
|
||||
url = attr.ib()
|
||||
navigation_type = attr.ib()
|
||||
is_main_frame = attr.ib()
|
||||
accepted = attr.ib(default=True)
|
||||
url = attr.ib() # type: QUrl
|
||||
navigation_type = attr.ib() # type: Type
|
||||
is_main_frame = attr.ib() # type: bool
|
||||
accepted = attr.ib(default=True) # type: bool
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import socket
|
|||
import shlex
|
||||
import glob
|
||||
import mimetypes
|
||||
import typing
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtGui import QColor, QClipboard, QDesktopServices
|
||||
|
|
@ -78,7 +79,7 @@ class SelectionUnsupportedError(ClipboardError):
|
|||
|
||||
"""Raised if [gs]et_clipboard is used and selection=True is unsupported."""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super().__init__("Primary selection is not supported on this "
|
||||
"platform!")
|
||||
|
||||
|
|
@ -88,7 +89,7 @@ class ClipboardEmptyError(ClipboardError):
|
|||
"""Raised if get_clipboard is used and the clipboard is empty."""
|
||||
|
||||
|
||||
def elide(text, length):
|
||||
def elide(text: str, length: int) -> str:
|
||||
"""Elide text so it uses a maximum of length chars."""
|
||||
if length < 1:
|
||||
raise ValueError("length must be >= 1!")
|
||||
|
|
@ -98,7 +99,7 @@ def elide(text, length):
|
|||
return text[:length - 1] + '\u2026'
|
||||
|
||||
|
||||
def elide_filename(filename, length):
|
||||
def elide_filename(filename: str, length: int) -> str:
|
||||
"""Elide a filename to the given length.
|
||||
|
||||
The difference to the elide() is that the text is removed from
|
||||
|
|
@ -130,7 +131,7 @@ def elide_filename(filename, length):
|
|||
return filename[:left] + elidestr + filename[-right:]
|
||||
|
||||
|
||||
def compact_text(text, elidelength=None):
|
||||
def compact_text(text: str, elidelength: int = None) -> str:
|
||||
"""Remove leading whitespace and newlines from a text and maybe elide it.
|
||||
|
||||
Args:
|
||||
|
|
@ -146,7 +147,7 @@ def compact_text(text, elidelength=None):
|
|||
return out
|
||||
|
||||
|
||||
def preload_resources():
|
||||
def preload_resources() -> None:
|
||||
"""Load resource files into the cache."""
|
||||
for subdir, pattern in [('html', '*.html'), ('javascript', '*.js')]:
|
||||
path = resource_filename(subdir)
|
||||
|
|
@ -155,7 +156,8 @@ def preload_resources():
|
|||
_resource_cache[sub_path] = read_file(sub_path)
|
||||
|
||||
|
||||
def read_file(filename, binary=False):
|
||||
# FIXME:typing Return value should be bytes/str
|
||||
def read_file(filename: str, binary: bool = False) -> typing.Any:
|
||||
"""Get the contents of a file contained with qutebrowser.
|
||||
|
||||
Args:
|
||||
|
|
@ -177,19 +179,22 @@ def read_file(filename, binary=False):
|
|||
# https://github.com/pyinstaller/pyinstaller/wiki/FAQ#misc
|
||||
fn = os.path.join(os.path.dirname(sys.executable), filename)
|
||||
if binary:
|
||||
with open(fn, 'rb') as f:
|
||||
with open(fn, 'rb') as f: # type: typing.IO
|
||||
return f.read()
|
||||
else:
|
||||
with open(fn, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
else:
|
||||
data = pkg_resources.resource_string(qutebrowser.__name__, filename)
|
||||
if not binary:
|
||||
data = data.decode('UTF-8')
|
||||
return data
|
||||
data = pkg_resources.resource_string(
|
||||
qutebrowser.__name__, filename)
|
||||
|
||||
if binary:
|
||||
return data
|
||||
|
||||
return data.decode('UTF-8')
|
||||
|
||||
|
||||
def resource_filename(filename):
|
||||
def resource_filename(filename: str) -> str:
|
||||
"""Get the absolute filename of a file contained with qutebrowser.
|
||||
|
||||
Args:
|
||||
|
|
@ -203,7 +208,9 @@ def resource_filename(filename):
|
|||
return pkg_resources.resource_filename(qutebrowser.__name__, filename)
|
||||
|
||||
|
||||
def _get_color_percentage(a_c1, a_c2, a_c3, b_c1, b_c2, b_c3, percent):
|
||||
def _get_color_percentage(a_c1: int, a_c2: int, a_c3:
|
||||
int, b_c1: int, b_c2: int, b_c3: int,
|
||||
percent: int) -> typing.Tuple[int, int, int]:
|
||||
"""Get a color which is percent% interpolated between start and end.
|
||||
|
||||
Args:
|
||||
|
|
@ -224,7 +231,12 @@ def _get_color_percentage(a_c1, a_c2, a_c3, b_c1, b_c2, b_c3, percent):
|
|||
return (out_c1, out_c2, out_c3)
|
||||
|
||||
|
||||
def interpolate_color(start, end, percent, colorspace=QColor.Rgb):
|
||||
def interpolate_color(
|
||||
start: QColor,
|
||||
end: QColor,
|
||||
percent: int,
|
||||
colorspace: typing.Optional[QColor.Spec] = QColor.Rgb
|
||||
) -> QColor:
|
||||
"""Get an interpolated color value.
|
||||
|
||||
Args:
|
||||
|
|
@ -273,7 +285,7 @@ def interpolate_color(start, end, percent, colorspace=QColor.Rgb):
|
|||
return out
|
||||
|
||||
|
||||
def format_seconds(total_seconds):
|
||||
def format_seconds(total_seconds: int) -> str:
|
||||
"""Format a count of seconds to get a [H:]M:SS string."""
|
||||
prefix = '-' if total_seconds < 0 else ''
|
||||
hours, rem = divmod(abs(round(total_seconds)), 3600)
|
||||
|
|
@ -289,7 +301,9 @@ def format_seconds(total_seconds):
|
|||
return prefix + ':'.join(chunks)
|
||||
|
||||
|
||||
def format_size(size, base=1024, suffix=''):
|
||||
def format_size(size: typing.Optional[float],
|
||||
base: int = 1024,
|
||||
suffix: str = '') -> str:
|
||||
"""Format a byte size so it's human readable.
|
||||
|
||||
Inspired by http://stackoverflow.com/q/1094841
|
||||
|
|
@ -308,13 +322,13 @@ class FakeIOStream(io.TextIOBase):
|
|||
|
||||
"""A fake file-like stream which calls a function for write-calls."""
|
||||
|
||||
def __init__(self, write_func):
|
||||
def __init__(self, write_func: typing.Callable[[str], int]) -> None:
|
||||
super().__init__()
|
||||
self.write = write_func
|
||||
self.write = write_func # type: ignore
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def fake_io(write_func):
|
||||
def fake_io(write_func: typing.Callable[[str], int]) -> typing.Iterator[None]:
|
||||
"""Run code with stdout and stderr replaced by FakeIOStreams.
|
||||
|
||||
Args:
|
||||
|
|
@ -324,21 +338,21 @@ def fake_io(write_func):
|
|||
old_stderr = sys.stderr
|
||||
fake_stderr = FakeIOStream(write_func)
|
||||
fake_stdout = FakeIOStream(write_func)
|
||||
sys.stderr = fake_stderr
|
||||
sys.stdout = fake_stdout
|
||||
sys.stderr = fake_stderr # type: ignore
|
||||
sys.stdout = fake_stdout # type: ignore
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
# If the code we did run did change sys.stdout/sys.stderr, we leave it
|
||||
# unchanged. Otherwise, we reset it.
|
||||
if sys.stdout is fake_stdout:
|
||||
if sys.stdout is fake_stdout: # type: ignore
|
||||
sys.stdout = old_stdout
|
||||
if sys.stderr is fake_stderr:
|
||||
if sys.stderr is fake_stderr: # type: ignore
|
||||
sys.stderr = old_stderr
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def disabled_excepthook():
|
||||
def disabled_excepthook() -> typing.Iterator[None]:
|
||||
"""Run code with the exception hook temporarily disabled."""
|
||||
old_excepthook = sys.excepthook
|
||||
sys.excepthook = sys.__excepthook__
|
||||
|
|
@ -371,7 +385,7 @@ class prevent_exceptions: # noqa: N801,N806 pylint: disable=invalid-name
|
|||
_predicate: The condition which needs to be True to prevent exceptions
|
||||
"""
|
||||
|
||||
def __init__(self, retval, predicate=True):
|
||||
def __init__(self, retval: typing.Any, predicate: bool = True) -> None:
|
||||
"""Save decorator arguments.
|
||||
|
||||
Gets called on parse-time with the decorator arguments.
|
||||
|
|
@ -382,7 +396,7 @@ class prevent_exceptions: # noqa: N801,N806 pylint: disable=invalid-name
|
|||
self._retval = retval
|
||||
self._predicate = predicate
|
||||
|
||||
def __call__(self, func):
|
||||
def __call__(self, func: typing.Callable) -> typing.Callable:
|
||||
"""Called when a function should be decorated.
|
||||
|
||||
Args:
|
||||
|
|
@ -397,7 +411,7 @@ class prevent_exceptions: # noqa: N801,N806 pylint: disable=invalid-name
|
|||
retval = self._retval
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
def wrapper(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
|
||||
"""Call the original function."""
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
|
|
@ -408,7 +422,7 @@ class prevent_exceptions: # noqa: N801,N806 pylint: disable=invalid-name
|
|||
return wrapper
|
||||
|
||||
|
||||
def is_enum(obj):
|
||||
def is_enum(obj: typing.Any) -> bool:
|
||||
"""Check if a given object is an enum."""
|
||||
try:
|
||||
return issubclass(obj, enum.Enum)
|
||||
|
|
@ -416,7 +430,9 @@ def is_enum(obj):
|
|||
return False
|
||||
|
||||
|
||||
def get_repr(obj, constructor=False, **attrs):
|
||||
def get_repr(obj: typing.Any,
|
||||
constructor: bool = False,
|
||||
**attrs: typing.Any) -> str:
|
||||
"""Get a suitable __repr__ string for an object.
|
||||
|
||||
Args:
|
||||
|
|
@ -439,7 +455,7 @@ def get_repr(obj, constructor=False, **attrs):
|
|||
return '<{}>'.format(cls)
|
||||
|
||||
|
||||
def qualname(obj):
|
||||
def qualname(obj: typing.Any) -> str:
|
||||
"""Get the fully qualified name of an object.
|
||||
|
||||
Based on twisted.python.reflect.fullyQualifiedName.
|
||||
|
|
@ -467,7 +483,10 @@ def qualname(obj):
|
|||
return repr(obj)
|
||||
|
||||
|
||||
def raises(exc, func, *args):
|
||||
def raises(exc: typing.Union[typing.Type[BaseException],
|
||||
typing.Tuple[typing.Type[BaseException]]],
|
||||
func: typing.Callable,
|
||||
*args: typing.Any) -> bool:
|
||||
"""Check if a function raises a given exception.
|
||||
|
||||
Args:
|
||||
|
|
@ -486,7 +505,7 @@ def raises(exc, func, *args):
|
|||
return False
|
||||
|
||||
|
||||
def force_encoding(text, encoding):
|
||||
def force_encoding(text: str, encoding: str) -> str:
|
||||
"""Make sure a given text is encodable with the given encoding.
|
||||
|
||||
This replaces all chars not encodable with question marks.
|
||||
|
|
@ -494,7 +513,8 @@ def force_encoding(text, encoding):
|
|||
return text.encode(encoding, errors='replace').decode(encoding)
|
||||
|
||||
|
||||
def sanitize_filename(name, replacement='_'):
|
||||
def sanitize_filename(name: str,
|
||||
replacement: typing.Optional[str] = '_') -> str:
|
||||
"""Replace invalid filename characters.
|
||||
|
||||
Note: This should be used for the basename, as it also removes the path
|
||||
|
|
@ -527,7 +547,7 @@ def sanitize_filename(name, replacement='_'):
|
|||
return name
|
||||
|
||||
|
||||
def set_clipboard(data, selection=False):
|
||||
def set_clipboard(data: str, selection: bool = False) -> None:
|
||||
"""Set the clipboard to some given data."""
|
||||
global fake_clipboard
|
||||
if selection and not supports_selection():
|
||||
|
|
@ -541,7 +561,7 @@ def set_clipboard(data, selection=False):
|
|||
QApplication.clipboard().setText(data, mode=mode)
|
||||
|
||||
|
||||
def get_clipboard(selection=False, fallback=False):
|
||||
def get_clipboard(selection: bool = False, fallback: bool = False) -> str:
|
||||
"""Get data from the clipboard.
|
||||
|
||||
Args:
|
||||
|
|
@ -574,12 +594,12 @@ def get_clipboard(selection=False, fallback=False):
|
|||
return data
|
||||
|
||||
|
||||
def supports_selection():
|
||||
def supports_selection() -> bool:
|
||||
"""Check if the OS supports primary selection."""
|
||||
return QApplication.clipboard().supportsSelection()
|
||||
|
||||
|
||||
def random_port():
|
||||
def random_port() -> int:
|
||||
"""Get a random free port."""
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.bind(('localhost', 0))
|
||||
|
|
@ -588,7 +608,7 @@ def random_port():
|
|||
return port
|
||||
|
||||
|
||||
def open_file(filename, cmdline=None):
|
||||
def open_file(filename: str, cmdline: str = None) -> None:
|
||||
"""Open the given file.
|
||||
|
||||
If cmdline is not given, downloads.open_dispatcher is used.
|
||||
|
|
@ -624,6 +644,8 @@ def open_file(filename, cmdline=None):
|
|||
if cmdline is None and override:
|
||||
cmdline = override
|
||||
|
||||
assert cmdline is not None
|
||||
|
||||
cmd, *args = shlex.split(cmdline)
|
||||
args = [arg.replace('{}', filename) for arg in args]
|
||||
if '{}' not in cmdline:
|
||||
|
|
@ -634,11 +656,11 @@ def open_file(filename, cmdline=None):
|
|||
proc.start_detached(cmd, args)
|
||||
|
||||
|
||||
def unused(_arg):
|
||||
def unused(_arg: typing.Any) -> None:
|
||||
"""Function which does nothing to avoid pylint complaining."""
|
||||
|
||||
|
||||
def expand_windows_drive(path):
|
||||
def expand_windows_drive(path: str) -> str:
|
||||
r"""Expand a drive-path like E: into E:\.
|
||||
|
||||
Does nothing for other paths.
|
||||
|
|
@ -656,7 +678,7 @@ def expand_windows_drive(path):
|
|||
return path
|
||||
|
||||
|
||||
def yaml_load(f):
|
||||
def yaml_load(f: typing.Union[str, typing.IO[str]]) -> typing.Any:
|
||||
"""Wrapper over yaml.load using the C loader if possible."""
|
||||
start = datetime.datetime.now()
|
||||
|
||||
|
|
@ -686,7 +708,8 @@ def yaml_load(f):
|
|||
return data
|
||||
|
||||
|
||||
def yaml_dump(data, f=None):
|
||||
def yaml_dump(data: typing.Any,
|
||||
f: typing.IO[str] = None) -> typing.Optional[str]:
|
||||
"""Wrapper over yaml.dump using the C dumper if possible.
|
||||
|
||||
Also returns a str instead of bytes.
|
||||
|
|
@ -699,7 +722,7 @@ def yaml_dump(data, f=None):
|
|||
return yaml_data.decode('utf-8')
|
||||
|
||||
|
||||
def chunk(elems, n):
|
||||
def chunk(elems: typing.Sequence, n: int) -> typing.Iterator[typing.Sequence]:
|
||||
"""Yield successive n-sized chunks from elems.
|
||||
|
||||
If elems % n != 0, the last chunk will be smaller.
|
||||
|
|
@ -710,7 +733,7 @@ def chunk(elems, n):
|
|||
yield elems[i:i + n]
|
||||
|
||||
|
||||
def guess_mimetype(filename, fallback=False):
|
||||
def guess_mimetype(filename: str, fallback: bool = False) -> str:
|
||||
"""Guess a mimetype based on a filename.
|
||||
|
||||
Args:
|
||||
|
|
@ -726,7 +749,7 @@ def guess_mimetype(filename, fallback=False):
|
|||
return mimetype
|
||||
|
||||
|
||||
def ceil_log(number, base):
|
||||
def ceil_log(number: int, base: int) -> int:
|
||||
"""Compute max(1, ceil(log(number, base))).
|
||||
|
||||
Use only integer arithmetic in order to avoid numerical error.
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import collections
|
|||
import enum
|
||||
import datetime
|
||||
import getpass
|
||||
import typing
|
||||
|
||||
import attr
|
||||
import pkg_resources
|
||||
|
|
@ -66,10 +67,10 @@ class DistributionInfo:
|
|||
|
||||
"""Information about the running distribution."""
|
||||
|
||||
id = attr.ib()
|
||||
parsed = attr.ib()
|
||||
version = attr.ib()
|
||||
pretty = attr.ib()
|
||||
id = attr.ib() # type: typing.Optional[str]
|
||||
parsed = attr.ib() # type: Distribution
|
||||
version = attr.ib() # type: typing.Optional[typing.Tuple[str, ...]]
|
||||
pretty = attr.ib() # type: str
|
||||
|
||||
|
||||
pastebin_url = None
|
||||
|
|
@ -79,7 +80,7 @@ Distribution = enum.Enum(
|
|||
'kde'])
|
||||
|
||||
|
||||
def distribution():
|
||||
def distribution() -> typing.Optional[DistributionInfo]:
|
||||
"""Get some information about the running Linux distribution.
|
||||
|
||||
Returns:
|
||||
|
|
@ -104,9 +105,12 @@ def distribution():
|
|||
pretty = info.get('PRETTY_NAME', None)
|
||||
if pretty in ['Linux', None]: # Funtoo has PRETTY_NAME=Linux
|
||||
pretty = info.get('NAME', 'Unknown')
|
||||
assert pretty is not None
|
||||
|
||||
if 'VERSION_ID' in info:
|
||||
dist_version = pkg_resources.parse_version(info['VERSION_ID'])
|
||||
dist_version = pkg_resources.parse_version(
|
||||
info['VERSION_ID']
|
||||
) # type: typing.Optional[typing.Tuple[str, ...]]
|
||||
else:
|
||||
dist_version = None
|
||||
|
||||
|
|
@ -115,16 +119,19 @@ def distribution():
|
|||
'funtoo': 'gentoo', # does not have ID_LIKE=gentoo
|
||||
'org.kde.Platform': 'kde',
|
||||
}
|
||||
try:
|
||||
parsed = Distribution[id_mappings.get(dist_id, dist_id)]
|
||||
except KeyError:
|
||||
parsed = Distribution.unknown
|
||||
|
||||
parsed = Distribution.unknown
|
||||
if dist_id is not None:
|
||||
try:
|
||||
parsed = Distribution[id_mappings.get(dist_id, dist_id)]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return DistributionInfo(parsed=parsed, version=dist_version, pretty=pretty,
|
||||
id=dist_id)
|
||||
|
||||
|
||||
def _git_str():
|
||||
def _git_str() -> typing.Optional[str]:
|
||||
"""Try to find out git version.
|
||||
|
||||
Return:
|
||||
|
|
@ -150,7 +157,7 @@ def _git_str():
|
|||
return None
|
||||
|
||||
|
||||
def _git_str_subprocess(gitpath):
|
||||
def _git_str_subprocess(gitpath: str) -> typing.Optional[str]:
|
||||
"""Try to get the git commit ID and timestamp by calling git.
|
||||
|
||||
Args:
|
||||
|
|
@ -176,7 +183,7 @@ def _git_str_subprocess(gitpath):
|
|||
return None
|
||||
|
||||
|
||||
def _release_info():
|
||||
def _release_info() -> typing.Sequence[typing.Tuple[str, str]]:
|
||||
"""Try to gather distribution release information.
|
||||
|
||||
Return:
|
||||
|
|
@ -200,7 +207,7 @@ def _release_info():
|
|||
return data
|
||||
|
||||
|
||||
def _module_versions():
|
||||
def _module_versions() -> typing.Sequence[str]:
|
||||
"""Get versions of optional modules.
|
||||
|
||||
Return:
|
||||
|
|
@ -219,7 +226,7 @@ def _module_versions():
|
|||
('PyQt5.QtWebEngineWidgets', []),
|
||||
('PyQt5.QtWebEngine', ['PYQT_WEBENGINE_VERSION_STR']),
|
||||
('PyQt5.QtWebKitWidgets', []),
|
||||
])
|
||||
]) # type: typing.Mapping[str, typing.Sequence[str]]
|
||||
for modname, attributes in modules.items():
|
||||
try:
|
||||
module = importlib.import_module(modname)
|
||||
|
|
@ -239,7 +246,7 @@ def _module_versions():
|
|||
return lines
|
||||
|
||||
|
||||
def _path_info():
|
||||
def _path_info() -> typing.Mapping[str, str]:
|
||||
"""Get info about important path names.
|
||||
|
||||
Return:
|
||||
|
|
@ -258,7 +265,7 @@ def _path_info():
|
|||
return info
|
||||
|
||||
|
||||
def _os_info():
|
||||
def _os_info() -> typing.Sequence[str]:
|
||||
"""Get operating system info.
|
||||
|
||||
Return:
|
||||
|
|
@ -272,11 +279,11 @@ def _os_info():
|
|||
elif utils.is_windows:
|
||||
osver = ', '.join(platform.win32_ver())
|
||||
elif utils.is_mac:
|
||||
release, versioninfo, machine = platform.mac_ver()
|
||||
if all(not e for e in versioninfo):
|
||||
release, info_tpl, machine = platform.mac_ver()
|
||||
if all(not e for e in info_tpl):
|
||||
versioninfo = ''
|
||||
else:
|
||||
versioninfo = '.'.join(versioninfo)
|
||||
versioninfo = '.'.join(info_tpl)
|
||||
osver = ', '.join([e for e in [release, versioninfo, machine] if e])
|
||||
elif utils.is_posix:
|
||||
osver = ' '.join(platform.uname())
|
||||
|
|
@ -289,7 +296,7 @@ def _os_info():
|
|||
return lines
|
||||
|
||||
|
||||
def _pdfjs_version():
|
||||
def _pdfjs_version() -> str:
|
||||
"""Get the pdf.js version.
|
||||
|
||||
Return:
|
||||
|
|
@ -315,7 +322,7 @@ def _pdfjs_version():
|
|||
return '{} ({})'.format(pdfjs_version, file_path)
|
||||
|
||||
|
||||
def _chromium_version():
|
||||
def _chromium_version() -> str:
|
||||
"""Get the Chromium version for QtWebEngine.
|
||||
|
||||
This can also be checked by looking at this file with the right Qt tag:
|
||||
|
|
@ -368,7 +375,7 @@ def _chromium_version():
|
|||
return match.group(1)
|
||||
|
||||
|
||||
def _backend():
|
||||
def _backend() -> str:
|
||||
"""Get the backend line with relevant information."""
|
||||
if objects.backend == usertypes.Backend.QtWebKit:
|
||||
return 'new QtWebKit (WebKit {})'.format(qWebKitVersion())
|
||||
|
|
@ -397,7 +404,7 @@ def _config_py_loaded() -> str:
|
|||
return "no config.py was loaded"
|
||||
|
||||
|
||||
def version():
|
||||
def version() -> str:
|
||||
"""Return a string with various version information."""
|
||||
lines = ["qutebrowser v{}".format(qutebrowser.__version__)]
|
||||
gitver = _git_str()
|
||||
|
|
@ -471,7 +478,7 @@ def version():
|
|||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def opengl_vendor(): # pragma: no cover
|
||||
def opengl_vendor() -> typing.Optional[str]: # pragma: no cover
|
||||
"""Get the OpenGL vendor used.
|
||||
|
||||
This returns a string such as 'nouveau' or
|
||||
|
|
@ -485,7 +492,8 @@ def opengl_vendor(): # pragma: no cover
|
|||
log.init.debug("Using override {}".format(override))
|
||||
return override
|
||||
|
||||
old_context = QOpenGLContext.currentContext()
|
||||
old_context = typing.cast(typing.Optional[QOpenGLContext],
|
||||
QOpenGLContext.currentContext())
|
||||
old_surface = None if old_context is None else old_context.surface()
|
||||
|
||||
surface = QOffscreenSurface()
|
||||
|
|
@ -527,20 +535,22 @@ def opengl_vendor(): # pragma: no cover
|
|||
old_context.makeCurrent(old_surface)
|
||||
|
||||
|
||||
def pastebin_version(pbclient=None):
|
||||
def pastebin_version(pbclient: pastebin.PastebinClient = None) -> None:
|
||||
"""Pastebin the version and log the url to messages."""
|
||||
def _yank_url(url):
|
||||
def _yank_url(url: str) -> None:
|
||||
utils.set_clipboard(url)
|
||||
message.info("Version url {} yanked to clipboard.".format(url))
|
||||
|
||||
def _on_paste_version_success(url):
|
||||
def _on_paste_version_success(url: str) -> None:
|
||||
assert pbclient is not None
|
||||
global pastebin_url
|
||||
url = url.strip()
|
||||
_yank_url(url)
|
||||
pbclient.deleteLater()
|
||||
pastebin_url = url
|
||||
|
||||
def _on_paste_version_err(text):
|
||||
def _on_paste_version_err(text: str) -> None:
|
||||
assert pbclient is not None
|
||||
message.error("Failed to pastebin version"
|
||||
" info: {}".format(text))
|
||||
pbclient.deleteLater()
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
attrs==19.1.0
|
||||
attrs==19.2.0
|
||||
colorama==0.4.1
|
||||
cssutils==1.0.2
|
||||
Jinja2==2.10.1
|
||||
Jinja2==2.10.3
|
||||
MarkupSafe==1.1.1
|
||||
Pygments==2.4.2
|
||||
pyPEG2==2.15.2
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ def parse_args():
|
|||
default='snakeviz',
|
||||
help="The tool to use to view the profiling data")
|
||||
parser.add_argument('--profile-file', metavar='FILE', action='store',
|
||||
default="profile_data",
|
||||
help="The filename to use with --profile-tool=none")
|
||||
parser.add_argument('--profile-test', action='store_true',
|
||||
help="Run pytest instead of qutebrowser")
|
||||
|
|
@ -80,7 +81,7 @@ def main():
|
|||
profiler.dump_stats(profilefile)
|
||||
|
||||
if args.profile_tool == 'none':
|
||||
pass
|
||||
print("Profile data written to {}".format(profilefile))
|
||||
elif args.profile_tool == 'gprof2dot':
|
||||
# yep, shell=True. I know what I'm doing.
|
||||
subprocess.run(
|
||||
|
|
|
|||
|
|
@ -52,14 +52,6 @@ fg_colors = {
|
|||
bg_colors = {name: col + 10 for name, col in fg_colors.items()}
|
||||
|
||||
|
||||
term_attributes = {
|
||||
'bright': 1,
|
||||
'dim': 2,
|
||||
'normal': 22,
|
||||
'reset': 0,
|
||||
}
|
||||
|
||||
|
||||
def _esc(code):
|
||||
"""Get an ANSI color code based on a color number."""
|
||||
return '\033[{}m'.format(code)
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ class Request(testprocess.Line):
|
|||
return NotImplemented
|
||||
|
||||
|
||||
@attr.s(frozen=True, cmp=False, hash=True)
|
||||
@attr.s(frozen=True, eq=False, hash=True)
|
||||
class ExpectedRequest:
|
||||
|
||||
"""Class to compare expected requests easily."""
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ import pytest
|
|||
import bs4
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
|
||||
from qutebrowser.utils import urlutils
|
||||
from helpers import utils as testutils
|
||||
|
||||
|
||||
pytestmark = pytest.mark.qtwebengine_skip("Title is empty when parsing for "
|
||||
|
|
@ -127,7 +129,10 @@ def parse(quteproc):
|
|||
"""
|
||||
html = quteproc.get_content(plain=False)
|
||||
soup = bs4.BeautifulSoup(html, 'html.parser')
|
||||
print(soup.prettify())
|
||||
|
||||
with testutils.ignore_bs4_warning():
|
||||
print(soup.prettify())
|
||||
|
||||
title_prefix = 'Browse directory: '
|
||||
# Strip off the title prefix to obtain the path of the folder that
|
||||
# we're browsing
|
||||
|
|
|
|||
|
|
@ -47,8 +47,8 @@ from qutebrowser.config import (config, configdata, configtypes, configexc,
|
|||
from qutebrowser.api import config as configapi
|
||||
from qutebrowser.utils import objreg, standarddir, utils, usertypes
|
||||
from qutebrowser.browser import greasemonkey, history, qutescheme
|
||||
from qutebrowser.browser.webkit import cookies
|
||||
from qutebrowser.misc import savemanager, sql, objects
|
||||
from qutebrowser.browser.webkit import cookies, cache
|
||||
from qutebrowser.misc import savemanager, sql, objects, sessions
|
||||
from qutebrowser.keyinput import modeman
|
||||
|
||||
|
||||
|
|
@ -350,12 +350,11 @@ def bookmark_manager_stub(stubs):
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def session_manager_stub(stubs):
|
||||
def session_manager_stub(stubs, monkeypatch):
|
||||
"""Fixture which provides a fake session-manager object."""
|
||||
stub = stubs.SessionManagerStub()
|
||||
objreg.register('session-manager', stub)
|
||||
yield stub
|
||||
objreg.delete('session-manager')
|
||||
monkeypatch.setattr(sessions, 'session_manager', stub)
|
||||
return stub
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -370,15 +369,6 @@ def tabbed_browser_stubs(qapp, stubs, win_registry):
|
|||
objreg.delete('tabbed-browser', scope='window', window=1)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app_stub(stubs):
|
||||
"""Fixture which provides a fake app object."""
|
||||
stub = stubs.ApplicationStub()
|
||||
objreg.register('app', stub)
|
||||
yield stub
|
||||
objreg.delete('app')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def status_command_stub(stubs, qtbot, win_registry):
|
||||
"""Fixture which provides a fake status-command object."""
|
||||
|
|
@ -465,18 +455,11 @@ def webframe(webpage):
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def cookiejar_and_cache(stubs):
|
||||
def cookiejar_and_cache(stubs, monkeypatch):
|
||||
"""Fixture providing a fake cookie jar and cache."""
|
||||
jar = QNetworkCookieJar()
|
||||
ram_jar = cookies.RAMCookieJar()
|
||||
cache = stubs.FakeNetworkCache()
|
||||
objreg.register('cookie-jar', jar)
|
||||
objreg.register('ram-cookie-jar', ram_jar)
|
||||
objreg.register('cache', cache)
|
||||
yield
|
||||
objreg.delete('cookie-jar')
|
||||
objreg.delete('ram-cookie-jar')
|
||||
objreg.delete('cache')
|
||||
monkeypatch.setattr(cookies, 'cookie_jar', QNetworkCookieJar())
|
||||
monkeypatch.setattr(cookies, 'ram_cookie_jar', cookies.RAMCookieJar())
|
||||
monkeypatch.setattr(cache, 'diskcache', stubs.FakeNetworkCache())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -501,18 +484,18 @@ def fake_save_manager():
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_args(request):
|
||||
def fake_args(request, monkeypatch):
|
||||
ns = types.SimpleNamespace()
|
||||
ns.backend = 'webengine' if request.config.webengine else 'webkit'
|
||||
ns.debug_flags = []
|
||||
objreg.register('args', ns)
|
||||
yield ns
|
||||
objreg.delete('args')
|
||||
|
||||
monkeypatch.setattr(objects, 'args', ns)
|
||||
return ns
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mode_manager(win_registry, config_stub, key_config_stub, qapp):
|
||||
mm = modeman.init(0, parent=qapp)
|
||||
mm = modeman.init(win_id=0, parent=qapp)
|
||||
yield mm
|
||||
objreg.delete('mode-manager', scope='window', window=0)
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ from PyQt5.QtWidgets import QCommonStyle, QLineEdit, QWidget, QTabBar
|
|||
|
||||
from qutebrowser.browser import browsertab, downloads
|
||||
from qutebrowser.utils import usertypes
|
||||
from qutebrowser.mainwindow import mainwindow
|
||||
from qutebrowser.commands import runners
|
||||
|
||||
|
||||
class FakeNetworkCache(QAbstractNetworkCache):
|
||||
|
|
@ -547,13 +547,6 @@ class TabWidgetStub(QObject):
|
|||
return self.tabs[idx - 1]
|
||||
|
||||
|
||||
class ApplicationStub(QObject):
|
||||
|
||||
"""Stub to insert as the app object in objreg."""
|
||||
|
||||
new_window = pyqtSignal(mainwindow.MainWindow)
|
||||
|
||||
|
||||
class HTTPPostStub(QObject):
|
||||
|
||||
"""A stub class for HTTPClient.
|
||||
|
|
@ -639,3 +632,22 @@ class FakeHistoryProgress:
|
|||
|
||||
def finish(self):
|
||||
self._finished = True
|
||||
|
||||
|
||||
class FakeCommandRunner(runners.AbstractCommandRunner):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.commands = []
|
||||
|
||||
def run(self, text, count=None, *, safely=False):
|
||||
self.commands.append((text, count))
|
||||
|
||||
|
||||
class FakeHintManager:
|
||||
|
||||
def __init__(self):
|
||||
self.keystr = None
|
||||
|
||||
def handle_partial_key(self, keystr):
|
||||
self.keystr = keystr
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import contextlib
|
|||
|
||||
import pytest
|
||||
|
||||
from qutebrowser.utils import qtutils
|
||||
from qutebrowser.utils import qtutils, log
|
||||
|
||||
|
||||
qt58 = pytest.mark.skipif(
|
||||
|
|
@ -180,3 +180,13 @@ def abs_datapath():
|
|||
@contextlib.contextmanager
|
||||
def nop_contextmanager():
|
||||
yield
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def ignore_bs4_warning():
|
||||
"""WORKAROUND for https://bugs.launchpad.net/beautifulsoup/+bug/1847592."""
|
||||
with log.ignore_py_warnings(
|
||||
category=DeprecationWarning,
|
||||
message="Using or importing the ABCs from 'collections' instead "
|
||||
"of from 'collections.abc' is deprecated", module='bs4.element'):
|
||||
yield
|
||||
|
|
|
|||
|
|
@ -54,8 +54,7 @@ def test_show_benchmark(benchmark, tabbed_browser, qtbot, message_bridge,
|
|||
with qtbot.wait_signal(tab.load_finished):
|
||||
tab.load_url(QUrl('qute://testdata/data/hints/benchmark.html'))
|
||||
|
||||
manager = qutebrowser.browser.hints.HintManager(
|
||||
win_id=0, tab_id=tab.tab_id)
|
||||
manager = qutebrowser.browser.hints.HintManager(win_id=0)
|
||||
|
||||
def bench():
|
||||
with qtbot.wait_signal(mode_manager.entered):
|
||||
|
|
@ -76,8 +75,7 @@ def test_match_benchmark(benchmark, tabbed_browser, qtbot, message_bridge,
|
|||
tab.load_url(QUrl('qute://testdata/data/hints/benchmark.html'))
|
||||
|
||||
config_stub.val.hints.scatter = False
|
||||
manager = qutebrowser.browser.hints.HintManager(
|
||||
win_id=0, tab_id=tab.tab_id)
|
||||
manager = qutebrowser.browser.hints.HintManager(win_id=0)
|
||||
|
||||
with qtbot.wait_signal(mode_manager.entered):
|
||||
manager.start()
|
||||
|
|
@ -106,7 +104,7 @@ def test_scattered_hints_count(min_len, num_chars, num_elements):
|
|||
2. There can only be two hint lengths, only 1 apart
|
||||
3. There are no unique prefixes for long hints, such as 'la' with no 'l<x>'
|
||||
"""
|
||||
manager = qutebrowser.browser.hints.HintManager(0, 0)
|
||||
manager = qutebrowser.browser.hints.HintManager(win_id=0)
|
||||
chars = string.ascii_lowercase[:num_chars]
|
||||
|
||||
hints = manager._hint_scattered(min_len, chars,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ from PyQt5.QtNetwork import QNetworkRequest
|
|||
|
||||
from qutebrowser.browser.webkit.network import filescheme
|
||||
from qutebrowser.utils import urlutils, utils
|
||||
from helpers import utils as testutils
|
||||
|
||||
|
||||
@pytest.mark.parametrize('create_file, create_dir, filterfunc, expected', [
|
||||
|
|
@ -129,7 +130,10 @@ class TestDirbrowserHtml:
|
|||
def parse(path):
|
||||
html = filescheme.dirbrowser_html(path).decode('utf-8')
|
||||
soup = bs4.BeautifulSoup(html, 'html.parser')
|
||||
print(soup.prettify())
|
||||
|
||||
with testutils.ignore_bs4_warning():
|
||||
print(soup.prettify())
|
||||
|
||||
container = soup('div', id='dirbrowserContainer')[0]
|
||||
|
||||
parent_elem = container('ul', class_='parent')
|
||||
|
|
@ -156,7 +160,10 @@ class TestDirbrowserHtml:
|
|||
def test_basic(self):
|
||||
html = filescheme.dirbrowser_html(os.getcwd()).decode('utf-8')
|
||||
soup = bs4.BeautifulSoup(html, 'html.parser')
|
||||
print(soup.prettify())
|
||||
|
||||
with testutils.ignore_bs4_warning():
|
||||
print(soup.prettify())
|
||||
|
||||
container = soup.div
|
||||
assert container['id'] == 'dirbrowserContainer'
|
||||
title_elem = container('div', id='dirbrowserTitle')[0]
|
||||
|
|
@ -170,7 +177,9 @@ class TestDirbrowserHtml:
|
|||
|
||||
html = filescheme.dirbrowser_html(os.getcwd()).decode('utf-8')
|
||||
soup = bs4.BeautifulSoup(html, 'html.parser')
|
||||
print(soup.prettify())
|
||||
|
||||
with testutils.ignore_bs4_warning():
|
||||
print(soup.prettify())
|
||||
|
||||
css = soup.html.head.style.string
|
||||
assert "background-image: url('file:///test%20path/foo.svg');" in css
|
||||
|
|
@ -239,7 +248,10 @@ class TestDirbrowserHtml:
|
|||
m.side_effect = OSError('Error message')
|
||||
html = filescheme.dirbrowser_html('').decode('utf-8')
|
||||
soup = bs4.BeautifulSoup(html, 'html.parser')
|
||||
print(soup.prettify())
|
||||
|
||||
with testutils.ignore_bs4_warning():
|
||||
print(soup.prettify())
|
||||
|
||||
error_msg = soup('p', id='error-message-text')[0].string
|
||||
assert error_msg == 'Error message'
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ def completionview(qtbot, status_command_stub, config_stub, win_registry,
|
|||
mocker.patch(
|
||||
'qutebrowser.completion.completiondelegate.CompletionItemDelegate',
|
||||
new=lambda *_: None)
|
||||
view = completionwidget.CompletionView(win_id=0)
|
||||
view = completionwidget.CompletionView(cmd=status_command_stub, win_id=0)
|
||||
qtbot.addWidget(view)
|
||||
return view
|
||||
|
||||
|
|
|
|||
|
|
@ -677,7 +677,7 @@ def test_session_completion(qtmodeltester, session_manager_stub):
|
|||
})
|
||||
|
||||
|
||||
def test_tab_completion(qtmodeltester, fake_web_tab, app_stub, win_registry,
|
||||
def test_tab_completion(qtmodeltester, fake_web_tab, win_registry,
|
||||
tabbed_browser_stubs):
|
||||
tabbed_browser_stubs[0].widget.tabs = [
|
||||
fake_web_tab(QUrl('https://github.com'), 'GitHub', 0),
|
||||
|
|
@ -703,8 +703,8 @@ def test_tab_completion(qtmodeltester, fake_web_tab, app_stub, win_registry,
|
|||
})
|
||||
|
||||
|
||||
def test_tab_completion_delete(qtmodeltester, fake_web_tab, app_stub,
|
||||
win_registry, tabbed_browser_stubs):
|
||||
def test_tab_completion_delete(qtmodeltester, fake_web_tab, win_registry,
|
||||
tabbed_browser_stubs):
|
||||
"""Verify closing a tab by deleting it from the completion widget."""
|
||||
tabbed_browser_stubs[0].widget.tabs = [
|
||||
fake_web_tab(QUrl('https://github.com'), 'GitHub', 0),
|
||||
|
|
@ -731,8 +731,8 @@ def test_tab_completion_delete(qtmodeltester, fake_web_tab, app_stub,
|
|||
QUrl('https://duckduckgo.com')]
|
||||
|
||||
|
||||
def test_tab_completion_not_sorted(qtmodeltester, fake_web_tab, app_stub,
|
||||
win_registry, tabbed_browser_stubs):
|
||||
def test_tab_completion_not_sorted(qtmodeltester, fake_web_tab, win_registry,
|
||||
tabbed_browser_stubs):
|
||||
"""Ensure that the completion row order is the same as tab index order.
|
||||
|
||||
Would be violated for more than 9 tabs if the completion was being
|
||||
|
|
@ -758,8 +758,8 @@ def test_tab_completion_not_sorted(qtmodeltester, fake_web_tab, app_stub,
|
|||
})
|
||||
|
||||
|
||||
def test_other_buffer_completion(qtmodeltester, fake_web_tab, app_stub,
|
||||
win_registry, tabbed_browser_stubs, info):
|
||||
def test_other_buffer_completion(qtmodeltester, fake_web_tab, win_registry,
|
||||
tabbed_browser_stubs, info):
|
||||
tabbed_browser_stubs[0].widget.tabs = [
|
||||
fake_web_tab(QUrl('https://github.com'), 'GitHub', 0),
|
||||
fake_web_tab(QUrl('https://wikipedia.org'), 'Wikipedia', 1),
|
||||
|
|
@ -782,7 +782,7 @@ def test_other_buffer_completion(qtmodeltester, fake_web_tab, app_stub,
|
|||
})
|
||||
|
||||
|
||||
def test_other_buffer_completion_id0(qtmodeltester, fake_web_tab, app_stub,
|
||||
def test_other_buffer_completion_id0(qtmodeltester, fake_web_tab,
|
||||
win_registry, tabbed_browser_stubs, info):
|
||||
tabbed_browser_stubs[0].widget.tabs = [
|
||||
fake_web_tab(QUrl('https://github.com'), 'GitHub', 0),
|
||||
|
|
|
|||
|
|
@ -126,3 +126,9 @@ Fake traceback
|
|||
""")
|
||||
# Make sure the traceback is not indented
|
||||
assert '<pre>\nFake traceback\n' in html
|
||||
|
||||
|
||||
def test_config_file_errors_fatal():
|
||||
err = configexc.ConfigErrorDesc("Text", Exception("Text"))
|
||||
errors = configexc.ConfigFileErrors("state", [err], fatal=True)
|
||||
assert errors.fatal
|
||||
|
|
|
|||
|
|
@ -737,13 +737,27 @@ class TestConfigPy:
|
|||
expected = {'normal': {'H': 'message-info back'}}
|
||||
assert config.instance.get_obj('bindings.commands') == expected
|
||||
|
||||
def test_bind_none(self, confpy):
|
||||
def test_bind_nop(self, confpy):
|
||||
confpy.write("c.bindings.commands = None",
|
||||
"config.bind(',x', 'nop')")
|
||||
confpy.read()
|
||||
expected = {'normal': {',x': 'nop'}}
|
||||
assert config.instance.get_obj('bindings.commands') == expected
|
||||
|
||||
def test_bind_none(self, confpy):
|
||||
confpy.write("config.bind('<Ctrl+q>', None)")
|
||||
with pytest.raises(configexc.ConfigFileErrors) as excinfo:
|
||||
confpy.read()
|
||||
|
||||
expected = {'normal': {'<Ctrl+q>': None}}
|
||||
assert config.instance.get_obj('bindings.commands') == expected
|
||||
|
||||
msg = ("While unbinding '<Ctrl+q>': Unbinding commands with "
|
||||
"config.bind('<Ctrl+q>', None) is deprecated. Use "
|
||||
"config.unbind('<Ctrl+q>') instead.")
|
||||
assert len(excinfo.value.errors) == 1
|
||||
assert str(excinfo.value.errors[0]) == msg
|
||||
|
||||
@pytest.mark.parametrize('line, key, mode', [
|
||||
('config.unbind("o")', 'o', 'normal'),
|
||||
('config.unbind("y", mode="yesno")', 'y', 'yesno'),
|
||||
|
|
|
|||
|
|
@ -101,7 +101,6 @@ class TestEarlyInit:
|
|||
assert actual_errors == expected_errors
|
||||
|
||||
# Make sure things have been init'ed
|
||||
objreg.get('config-commands')
|
||||
assert isinstance(config.instance, config.Config)
|
||||
assert isinstance(config.key_instance, config.KeyConfig)
|
||||
|
||||
|
|
@ -205,6 +204,12 @@ class TestEarlyInit:
|
|||
|
||||
assert dump == '\n'.join(expected)
|
||||
|
||||
def test_state_init_errors(self, init_patch, args, data_tmpdir):
|
||||
state_file = data_tmpdir / 'state'
|
||||
state_file.write_binary(b'\x00')
|
||||
configinit.early_init(args)
|
||||
assert configinit._init_errors.errors
|
||||
|
||||
def test_invalid_change_filter(self, init_patch, args):
|
||||
config.change_filter('foobar')
|
||||
with pytest.raises(configexc.NoOptionError):
|
||||
|
|
@ -325,16 +330,23 @@ class TestEarlyInit:
|
|||
configinit._init_envvars()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('errors', [True, False])
|
||||
@pytest.mark.parametrize('errors', [True, 'fatal', False])
|
||||
def test_late_init(init_patch, monkeypatch, fake_save_manager, args,
|
||||
mocker, errors):
|
||||
configinit.early_init(args)
|
||||
|
||||
if errors:
|
||||
err = configexc.ConfigErrorDesc("Error text", Exception("Exception"))
|
||||
errs = configexc.ConfigFileErrors("config.py", [err])
|
||||
if errors == 'fatal':
|
||||
errs.fatal = True
|
||||
|
||||
monkeypatch.setattr(configinit, '_init_errors', errs)
|
||||
|
||||
msgbox_mock = mocker.patch('qutebrowser.config.configinit.msgbox.msgbox',
|
||||
autospec=True)
|
||||
exit_mock = mocker.patch('qutebrowser.config.configinit.sys.exit',
|
||||
autospec=True)
|
||||
|
||||
configinit.late_init(fake_save_manager)
|
||||
|
||||
|
|
@ -342,12 +354,15 @@ def test_late_init(init_patch, monkeypatch, fake_save_manager, args,
|
|||
'state-config', unittest.mock.ANY)
|
||||
fake_save_manager.add_saveable.assert_any_call(
|
||||
'yaml-config', unittest.mock.ANY, unittest.mock.ANY)
|
||||
|
||||
if errors:
|
||||
assert len(msgbox_mock.call_args_list) == 1
|
||||
_call_posargs, call_kwargs = msgbox_mock.call_args_list[0]
|
||||
text = call_kwargs['text'].strip()
|
||||
assert text.startswith('Errors occurred while reading config.py:')
|
||||
assert '<b>Error text</b>: Exception' in text
|
||||
|
||||
assert exit_mock.called == (errors == 'fatal')
|
||||
else:
|
||||
assert not msgbox_mock.called
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue