Merge branch 'master' into focus-stack

This commit is contained in:
Florian Bruhin 2019-10-15 16:02:13 +02:00
commit 44942e5320
105 changed files with 2096 additions and 1507 deletions

View File

@ -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:

View File

@ -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]

View File

@ -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)
-------------------

View File

@ -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
~~~~~~~~~

View File

@ -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:[&lt;Ctrl-E&gt;]+: +pass:[open-editor]+
* +pass:[&lt;Escape&gt;]+: +pass:[leave-mode]+
* +pass:[&lt;Shift-Ins&gt;]+: +pass:[insert-text {primary}]+
* +pass:[&lt;Shift-Ins&gt;]+: +pass:[insert-text -- {primary}]+
- +pass:[normal]+:
* +pass:[&#x27;]+: +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).

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,2 +1,4 @@
#@ filter: PyQt5 < 5.11
PyQt5 >= 5.10, < 5.11
#@ filter: sip < 5
sip < 5

View File

@ -2,3 +2,4 @@
PyQt5==5.11.3 # rq.filter: < 5.12
PyQt5-sip==4.19.19
sip==4.19.8 # rq.filter: < 5

View File

@ -1,2 +1,4 @@
#@ filter: PyQt5 < 5.12
PyQt5 >= 5.11, < 5.12
#@ filter: sip < 5
sip < 5

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,2 +1,4 @@
#@ filter: PyQt5 < 5.8
#@ filter: sip < 5
PyQt5 >= 5.7, < 5.8
sip < 5

View File

@ -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

View File

@ -1,2 +1,4 @@
#@ filter: PyQt5 < 5.10
PyQt5 >= 5.9, < 5.10
#@ filter: sip < 5
sip < 5

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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)

View File

@ -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;

View File

@ -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

View File

@ -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:

View File

@ -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():

View File

@ -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(

View File

@ -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

View File

@ -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))

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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,

View File

@ -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.

View File

@ -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)

View File

@ -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():

View File

@ -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:

View File

@ -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):

View File

@ -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

View File

@ -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:")

View File

@ -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

View File

@ -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)

View File

@ -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:

View File

@ -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, *,

View File

@ -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))

View File

@ -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])

View File

@ -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)

View File

@ -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()

View File

@ -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))

View File

@ -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:

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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."""

View File

@ -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()

View File

@ -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()

View File

@ -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:

View File

@ -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(

View File

@ -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()

View File

@ -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)

View File

@ -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))

View File

@ -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,

View File

@ -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)

View File

@ -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:

View File

@ -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)))

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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']:

View File

@ -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:

View File

@ -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()

View File

@ -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

View File

@ -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.

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

@ -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.

View File

@ -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()

View File

@ -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

View File

@ -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(

View File

@ -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)

View File

@ -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."""

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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'

View File

@ -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

View File

@ -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),

View File

@ -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

View File

@ -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'),

View File

@ -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