diff --git a/.travis.yml b/.travis.yml
index 5b918a393..5a8965fd4 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -38,9 +38,11 @@ matrix:
# env: TESTENV=py35 OSX=yosemite
# osx_image: xcode6.4
- os: linux
- env: TESTENV=pylint PYTHON=python3.6
+ env: TESTENV=pylint
- os: linux
env: TESTENV=flake8
+ - os: linux
+ env: TESTENV=mypy
- os: linux
env: TESTENV=docs
addons:
diff --git a/MANIFEST.in b/MANIFEST.in
index ff96264aa..3a29ba690 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -32,6 +32,7 @@ include doc/changelog.asciidoc
prune tests
prune qutebrowser/3rdparty
exclude pytest.ini
+exclude mypy.ini
exclude qutebrowser/javascript/.eslintrc.yaml
exclude qutebrowser/javascript/.eslintignore
exclude doc/help
diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc
index 41515d287..51647bafd 100644
--- a/doc/changelog.asciidoc
+++ b/doc/changelog.asciidoc
@@ -66,6 +66,7 @@ Fixed
like GMail. However, the default for `content.cookies.accept` is still `all`
to be in line with what other browsers do.
- `:navigate` not incrementing in anchors or queries or anchors.
+- Crash when trying to use a proxy requiring authentication with QtWebKit.
v1.5.2
------
diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt
new file mode 100644
index 000000000..f2951fdf5
--- /dev/null
+++ b/misc/requirements/requirements-mypy.txt
@@ -0,0 +1,8 @@
+# This file is automatically generated by scripts/dev/recompile_requirements.py
+
+mypy==0.641
+mypy-extensions==0.4.1
+PyQt5==5.11.3
+PyQt5-sip==4.19.13
+-e git+https://github.com/qutebrowser/PyQt5-stubs.git@wip#egg=PyQt5_stubs
+typed-ast==1.1.0
diff --git a/misc/requirements/requirements-mypy.txt-raw b/misc/requirements/requirements-mypy.txt-raw
new file mode 100644
index 000000000..636ad43a4
--- /dev/null
+++ b/misc/requirements/requirements-mypy.txt-raw
@@ -0,0 +1,5 @@
+mypy
+-e git+https://github.com/qutebrowser/PyQt5-stubs.git@wip#egg=PyQt5-stubs
+
+# remove @commit-id for scm installs
+#@ replace: @.*# @wip#
diff --git a/misc/requirements/requirements-optional.txt b/misc/requirements/requirements-optional.txt
new file mode 100644
index 000000000..6e1e8b8ff
--- /dev/null
+++ b/misc/requirements/requirements-optional.txt
@@ -0,0 +1,7 @@
+# This file is automatically generated by scripts/dev/recompile_requirements.py
+
+colorama==0.4.0
+cssutils==1.0.2
+hunter==2.1.0
+Pympler==0.6
+six==1.11.0
diff --git a/misc/requirements/requirements-optional.txt-raw b/misc/requirements/requirements-optional.txt-raw
new file mode 100644
index 000000000..a0be23733
--- /dev/null
+++ b/misc/requirements/requirements-optional.txt-raw
@@ -0,0 +1,3 @@
+hunter
+cssutils
+pympler
diff --git a/mypy.ini b/mypy.ini
new file mode 100644
index 000000000..578a8c1fc
--- /dev/null
+++ b/mypy.ini
@@ -0,0 +1,51 @@
+[mypy]
+# We also need to support 3.5, but if we'd chose that here, we'd need to deal
+# with conditional imports (like secrets.py).
+python_version = 3.6
+
+# --strict
+warn_redundant_casts = True
+warn_unused_ignores = True
+disallow_subclassing_any = True
+## https://github.com/python/mypy/issues/5957
+# warn_unused_configs = True
+# disallow_untyped_calls = True
+# disallow_untyped_defs = True
+## https://github.com/python/mypy/issues/5954
+# disallow_incomplete_defs = True
+# check_untyped_defs = True
+# disallow_untyped_decorators = True
+# no_implicit_optional = True
+# warn_return_any = True
+
+[mypy-faulthandler]
+# https://github.com/python/typeshed/pull/2627
+ignore_missing_imports = True
+
+[mypy-colorama]
+# https://github.com/tartley/colorama/issues/206
+ignore_missing_imports = True
+
+[mypy-hunter]
+# https://github.com/ionelmc/python-hunter/issues/43
+ignore_missing_imports = True
+
+[mypy-pygments.*]
+# https://bitbucket.org/birkenfeld/pygments-main/issues/1485/type-hints
+ignore_missing_imports = True
+
+[mypy-cssutils]
+# Pretty much inactive currently
+ignore_missing_imports = True
+
+[mypy-pypeg2]
+# Pretty much inactive currently
+ignore_missing_imports = True
+
+[mypy-bdb]
+# stdlib, missing in typeshed
+ignore_missing_imports = True
+
+[mypy-qutebrowser.browser.webkit.rfc6266]
+# subclasses dynamic PyPEG2 classes
+disallow_subclassing_any = False
diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py
index b2be458ce..02d9b70dd 100644
--- a/qutebrowser/browser/browsertab.py
+++ b/qutebrowser/browser/browsertab.py
@@ -136,8 +136,8 @@ class AbstractAction:
action_base: The type of the actions (QWeb{Engine,}Page.WebAction)
"""
- action_class = None
- action_base = None
+ action_class = None # type: type
+ action_base = None # type: type
def __init__(self, tab):
self._widget = None
@@ -685,7 +685,7 @@ class AbstractAudio(QObject):
self._widget = None
self._tab = tab
- def set_muted(self, muted: bool, override: bool = False):
+ def set_muted(self, muted: bool, override: bool = False) -> None:
"""Set this tab as muted or not.
Arguments:
@@ -699,7 +699,7 @@ class AbstractAudio(QObject):
"""Whether this tab is muted."""
raise NotImplementedError
- def toggle_muted(self, *, override: bool = False):
+ def toggle_muted(self, *, override: bool = False) -> None:
self.set_muted(not self.is_muted(), override=override)
def is_recently_audible(self):
diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py
index ceafbc011..a53cfc75e 100644
--- a/qutebrowser/browser/commands.py
+++ b/qutebrowser/browser/commands.py
@@ -75,7 +75,7 @@ class CommandDispatcher:
new_window.show()
return new_window.tabbed_browser
- def _count(self):
+ def _count(self) -> int:
"""Convenience method to get the widget count."""
return self._tabbed_browser.widget.count()
@@ -513,7 +513,8 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('win_id', completion=miscmodels.window)
@cmdutils.argument('count', count=True)
- def tab_give(self, win_id: int = None, keep=False, count=None):
+ def tab_give(self, win_id: int = None, keep: bool = False,
+ count: int = None) -> None:
"""Give the current tab to a new or existing window if win_id given.
If no win_id is given, the tab will get detached into a new window.
@@ -601,7 +602,8 @@ class CommandDispatcher:
@cmdutils.argument('where', choices=['prev', 'next', 'up', 'increment',
'decrement'])
@cmdutils.argument('count', count=True)
- def navigate(self, where: str, tab=False, bg=False, window=False, count=1):
+ def navigate(self, where: str, tab: bool = False, bg: bool = False,
+ window: bool = False, count: int = 1) -> None:
"""Open typical prev/next links or navigate using the URL path.
This tries to automatically click on typical _Previous Page_ or
@@ -645,7 +647,7 @@ class CommandDispatcher:
inc_or_dec='decrement'),
'increment': functools.partial(navigate.incdec,
inc_or_dec='increment'),
- }
+ } # type: typing.Dict[str, typing.Callable]
try:
if where in ['prev', 'next']:
@@ -665,7 +667,7 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
- def scroll_px(self, dx: int, dy: int, count=1):
+ def scroll_px(self, dx: int, dy: int, count: int = 1) -> None:
"""Scroll the current tab by 'count * dx/dy' pixels.
Args:
@@ -681,7 +683,7 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
- def scroll(self, direction: typing.Union[str, int], count=1):
+ def scroll(self, direction: str, count: int = 1) -> None:
"""Scroll the current tab in the given direction.
Note you can use `:run-with-count` to have a keybinding with a bigger
@@ -719,7 +721,8 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
@cmdutils.argument('horizontal', flag='x')
- def scroll_to_perc(self, perc: float = None, horizontal=False, count=None):
+ def scroll_to_perc(self, perc: float = None, horizontal: bool = False,
+ count: int = None) -> None:
"""Scroll to a specific percentage of the page.
The percentage can be given either as argument or as count.
@@ -764,7 +767,7 @@ class CommandDispatcher:
choices=('next', 'increment'))
def scroll_page(self, x: float, y: float, *,
top_navigate: str = None, bottom_navigate: str = None,
- count=1):
+ count: int = 1) -> None:
"""Scroll the frame page-wise.
Args:
@@ -1120,7 +1123,7 @@ class CommandDispatcher:
@cmdutils.argument('index', choices=['last'])
@cmdutils.argument('count', count=True)
def tab_focus(self, index: typing.Union[str, int] = None,
- count=None, no_last=False):
+ count: int = None, no_last: bool = False) -> None:
"""Select the tab given as argument/[count].
If neither count nor index are given, it behaves like tab-next.
@@ -1143,6 +1146,8 @@ class CommandDispatcher:
self.tab_next()
return
+ assert isinstance(index, int)
+
if index < 0:
index = self._count() + index + 1
@@ -1159,7 +1164,8 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('index', choices=['+', '-'])
@cmdutils.argument('count', count=True)
- def tab_move(self, index: typing.Union[str, int] = None, count=None):
+ def tab_move(self, index: typing.Union[str, int] = None,
+ count: int = None) -> None:
"""Move the current tab according to the argument and [count].
If neither is given, move it to the first position.
@@ -1188,6 +1194,7 @@ class CommandDispatcher:
if count is not None:
new_idx = count - 1
elif index is not None:
+ assert isinstance(index, int)
new_idx = index - 1 if index >= 0 else index + self._count()
else:
new_idx = 0
@@ -1715,10 +1722,10 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('filter_', choices=['id'])
- def click_element(self, filter_: str, value, *,
+ def click_element(self, filter_: str, value: str, *,
target: usertypes.ClickTarget =
usertypes.ClickTarget.normal,
- force_event=False):
+ force_event: bool = False) -> None:
"""Click the element matching the given filter.
The given filter needs to result in exactly one element, otherwise, an
@@ -2067,8 +2074,8 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0, no_cmd_split=True)
- def jseval(self, js_code, file=False, quiet=False, *,
- world: typing.Union[usertypes.JsWorld, int] = None):
+ def jseval(self, js_code: str, file: bool = False, quiet: bool = False, *,
+ world: typing.Union[usertypes.JsWorld, int] = None) -> None:
"""Evaluate a JavaScript string.
Args:
diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py
index 92d846bd8..1709c7425 100644
--- a/qutebrowser/browser/downloads.py
+++ b/qutebrowser/browser/downloads.py
@@ -40,7 +40,11 @@ from qutebrowser.utils import (usertypes, standarddir, utils, message, log,
from qutebrowser.qt import sip
-ModelRole = enum.IntEnum('ModelRole', ['item'], start=Qt.UserRole)
+class ModelRole(enum.IntEnum):
+
+ """Custom download model roles."""
+
+ item = Qt.UserRole
# Remember the last used directory
@@ -1058,7 +1062,7 @@ class DownloadModel(QAbstractListModel):
@cmdutils.register(instance='download-model', scope='window', maxsplit=0)
@cmdutils.argument('count', count=True)
- def download_open(self, cmdline: str = None, count=0):
+ def download_open(self, cmdline: str = None, count: int = 0) -> None:
"""Open the last/[count]th download.
If no specific command is given, this will use the system's default
diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py
index d8d5a0624..aa5b5f34c 100644
--- a/qutebrowser/browser/hints.py
+++ b/qutebrowser/browser/hints.py
@@ -737,7 +737,7 @@ class HintManager(QObject):
self._context.baseurl = tabbed_browser.current_url()
except qtutils.QtValueError:
raise cmdexc.CommandError("No URL set for this page yet!")
- self._context.args = args
+ self._context.args = list(args)
self._context.group = group
try:
diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py
index 0fa9366a6..b78404de9 100644
--- a/qutebrowser/browser/qutescheme.py
+++ b/qutebrowser/browser/qutescheme.py
@@ -37,7 +37,7 @@ try:
import secrets
except ImportError:
# New in Python 3.6
- secrets = None
+ secrets = None # type: ignore
from PyQt5.QtCore import QUrlQuery, QUrl, qVersion
diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py
index 0f1d368e9..9945886fa 100644
--- a/qutebrowser/browser/webengine/webenginetab.py
+++ b/qutebrowser/browser/webengine/webenginetab.py
@@ -670,7 +670,7 @@ class WebEngineAudio(browsertab.AbstractAudio):
self._tab.url_changed.connect(self._on_url_changed)
config.instance.changed.connect(self._on_config_changed)
- def set_muted(self, muted: bool, override: bool = False):
+ def set_muted(self, muted: bool, override: bool = False) -> None:
self._overridden = override
page = self._widget.page()
page.setAudioMuted(muted)
diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py
index b10cc5f9a..e70226f30 100644
--- a/qutebrowser/browser/webengine/webview.py
+++ b/qutebrowser/browser/webengine/webview.py
@@ -240,7 +240,7 @@ class WebEnginePage(QWebEnginePage):
def acceptNavigationRequest(self,
url: QUrl,
typ: QWebEnginePage.NavigationType,
- is_main_frame: bool):
+ is_main_frame: bool) -> bool:
"""Override acceptNavigationRequest to forward it to the tab API."""
type_map = {
QWebEnginePage.NavigationTypeLinkClicked:
diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py
index 8d2523456..0c3148ee4 100644
--- a/qutebrowser/browser/webkit/network/networkmanager.py
+++ b/qutebrowser/browser/webkit/network/networkmanager.py
@@ -21,6 +21,7 @@
import collections
import html
+import typing # pylint: disable=unused-import
import attr
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QCoreApplication, QUrl,
@@ -28,6 +29,7 @@ from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QCoreApplication, QUrl,
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QSslSocket
from qutebrowser.config import config
+from qutebrowser.mainwindow import prompt # pylint: disable=unused-import
from qutebrowser.utils import (message, log, usertypes, utils, objreg,
urlutils, debug)
from qutebrowser.browser import shared
@@ -37,7 +39,7 @@ from qutebrowser.browser.webkit.network import (webkitqutescheme, networkreply,
HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%'
-_proxy_auth_cache = {}
+_proxy_auth_cache = {} # type: typing.Dict[ProxyId, prompt.AuthInfo]
@attr.s(frozen=True)
@@ -295,9 +297,9 @@ class NetworkManager(QNetworkAccessManager):
"""Called when a proxy needs authentication."""
proxy_id = ProxyId(proxy.type(), proxy.hostName(), proxy.port())
if proxy_id in _proxy_auth_cache:
- user, password = _proxy_auth_cache[proxy_id]
- authenticator.setUser(user)
- authenticator.setPassword(password)
+ authinfo = _proxy_auth_cache[proxy_id]
+ authenticator.setUser(authinfo.user)
+ authenticator.setPassword(authinfo.password)
else:
msg = '{} says:
{}'.format(
html.escape(proxy.hostName()),
diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py
index 2edea1777..c791326ce 100644
--- a/qutebrowser/browser/webkit/webkittab.py
+++ b/qutebrowser/browser/webkit/webkittab.py
@@ -641,7 +641,7 @@ class WebKitAudio(browsertab.AbstractAudio):
"""Dummy handling of audio status for QtWebKit."""
- def set_muted(self, muted: bool, override: bool = False):
+ def set_muted(self, muted: bool, override: bool = False) -> None:
raise browsertab.WebTabError('Muting is not supported on QtWebKit!')
def is_muted(self):
diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py
index ce985b466..0195ec17f 100644
--- a/qutebrowser/browser/webkit/webpage.py
+++ b/qutebrowser/browser/webkit/webpage.py
@@ -469,7 +469,7 @@ class BrowserPage(QWebPage):
def acceptNavigationRequest(self,
frame: QWebFrame,
request: QNetworkRequest,
- typ: QWebPage.NavigationType):
+ typ: QWebPage.NavigationType) -> bool:
"""Override acceptNavigationRequest to handle clicked links.
Setting linkDelegationPolicy to DelegateAllLinks and using a slot bound
diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py
index f9ce91b8f..41e875202 100644
--- a/qutebrowser/commands/cmdutils.py
+++ b/qutebrowser/commands/cmdutils.py
@@ -17,18 +17,15 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see .
-"""Contains various command utils and a global command dict.
-
-Module attributes:
- cmd_dict: A mapping from command-strings to command objects.
-"""
+"""Contains various command utils and a global command dict."""
import inspect
+import typing # pylint: disable=unused-import
from qutebrowser.utils import qtutils, log
from qutebrowser.commands import command, cmdexc
-cmd_dict = {}
+cmd_dict = {} # type: typing.Dict[str, command.Command]
def check_overflow(arg, ctype):
diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py
index c3f5d87a1..f1c7641e7 100644
--- a/qutebrowser/commands/runners.py
+++ b/qutebrowser/commands/runners.py
@@ -58,6 +58,9 @@ def _current_url(tabbed_browser):
def replace_variables(win_id, arglist):
"""Utility function to replace variables like {url} in a list of args."""
+ tabbed_browser = objreg.get('tabbed-browser', scope='window',
+ window=win_id)
+
variables = {
'url': lambda: _current_url(tabbed_browser).toString(
QUrl.FullyEncoded | QUrl.RemovePassword),
@@ -67,13 +70,13 @@ def replace_variables(win_id, arglist):
'clipboard': utils.get_clipboard,
'primary': lambda: utils.get_clipboard(selection=True),
}
+
for key in list(variables):
modified_key = '{' + key + '}'
variables[modified_key] = lambda x=modified_key: x
+
values = {}
args = []
- tabbed_browser = objreg.get('tabbed-browser', scope='window',
- window=win_id)
def repl_cb(matchobj):
"""Return replacement for given match."""
diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py
index 1f2e45741..1d7e34345 100644
--- a/qutebrowser/config/config.py
+++ b/qutebrowser/config/config.py
@@ -22,6 +22,7 @@
import copy
import contextlib
import functools
+import typing
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
@@ -30,11 +31,16 @@ from qutebrowser.utils import utils, log, jinja
from qutebrowser.misc import objects
from qutebrowser.keyinput import keyutils
+MYPY = False
+if MYPY:
+ # pylint: disable=unused-import
+ from qutebrowser.config import configcache # pragma: no cover
+
# An easy way to access the config from other code via config.val.foo
-val = None
-instance = None
-key_instance = None
-cache = None
+val = typing.cast('ConfigContainer', None)
+instance = typing.cast('Config', None)
+key_instance = typing.cast('KeyConfig', None)
+cache = typing.cast('configcache.ConfigCache', None)
# Keeping track of all change filters to validate them later.
change_filters = []
diff --git a/qutebrowser/config/configcache.py b/qutebrowser/config/configcache.py
index dfead6664..a421ba85c 100644
--- a/qutebrowser/config/configcache.py
+++ b/qutebrowser/config/configcache.py
@@ -20,6 +20,8 @@
"""Implementation of a basic config cache."""
+import typing
+
from qutebrowser.config import config
@@ -36,14 +38,14 @@ class ConfigCache:
"""
def __init__(self) -> None:
- self._cache = {}
+ self._cache = {} # type: typing.Dict[str, typing.Any]
config.instance.changed.connect(self._on_config_changed)
def _on_config_changed(self, attr: str) -> None:
if attr in self._cache:
self._cache[attr] = config.instance.get(attr)
- def __getitem__(self, attr: str):
+ def __getitem__(self, attr: str) -> typing.Any:
if attr not in self._cache:
assert not config.instance.get_opt(attr).supports_pattern
self._cache[attr] = config.instance.get(attr)
diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py
index 5503ea4f3..31eca988e 100644
--- a/qutebrowser/config/configtypes.py
+++ b/qutebrowser/config/configtypes.py
@@ -52,6 +52,7 @@ import datetime
import functools
import operator
import json
+import typing # pylint: disable=unused-import
import attr
import yaml
@@ -304,7 +305,7 @@ class MappingType(BaseType):
MAPPING: The mapping to use.
"""
- MAPPING = {}
+ MAPPING = {} # type: typing.Dict[str, typing.Any]
def __init__(self, none_ok=False, valid_values=None):
super().__init__(none_ok)
@@ -576,7 +577,7 @@ class FlagList(List):
the valid values of the setting.
"""
- combinable_values = None
+ combinable_values = None # type: typing.Optional[typing.Iterable]
_show_valtype = False
@@ -1118,7 +1119,7 @@ class QtFont(Font):
font.setWeight(weight_map[namedweight])
if weight:
# based on qcssparser.cpp:setFontWeightFromValue
- font.setWeight(min(int(weight) / 8, 99))
+ font.setWeight(min(int(weight) // 8, 99))
if size:
if size.lower().endswith('pt'):
font.setPointSizeF(float(size[:-2]))
diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py
index fb80c543b..e6d19db7e 100644
--- a/qutebrowser/config/websettings.py
+++ b/qutebrowser/config/websettings.py
@@ -19,6 +19,8 @@
"""Bridge from QWeb(Engine)Settings to our own settings."""
+import typing # pylint: disable=unused-import
+
from PyQt5.QtGui import QFont
from qutebrowser.config import config, configutils
@@ -44,10 +46,10 @@ class AbstractSettings:
"""Abstract base class for settings set via QWeb(Engine)Settings."""
- _ATTRIBUTES = None
- _FONT_SIZES = None
- _FONT_FAMILIES = None
- _FONT_TO_QFONT = None
+ _ATTRIBUTES = {} # type: typing.Dict[str, AttributeInfo]
+ _FONT_SIZES = {} # type: typing.Dict[str, typing.Any]
+ _FONT_FAMILIES = {} # type: typing.Dict[str, typing.Any]
+ _FONT_TO_QFONT = {} # type: typing.Dict[typing.Any, QFont.StyleHint]
def __init__(self, settings):
self._settings = settings
diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py
index 5eb76c86e..f6a8b1224 100644
--- a/qutebrowser/mainwindow/prompt.py
+++ b/qutebrowser/mainwindow/prompt.py
@@ -391,7 +391,8 @@ class PromptContainer(QWidget):
@cmdutils.register(instance='prompt-container', scope='window',
modes=[usertypes.KeyMode.prompt], maxsplit=0)
- def prompt_open_download(self, cmdline: str = None, pdfjs=False):
+ def prompt_open_download(self, cmdline: str = None,
+ pdfjs: bool = False) -> None:
"""Immediately open a download.
If no specific command is given, this will use the system's default
diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py
index c3ef53b1b..3edb8128a 100644
--- a/qutebrowser/mainwindow/statusbar/bar.py
+++ b/qutebrowser/mainwindow/statusbar/bar.py
@@ -145,7 +145,7 @@ class StatusBar(QWidget):
resized = pyqtSignal('QRect')
moved = pyqtSignal('QPoint')
_severity = None
- _color_flags = []
+ _color_flags = None
STYLESHEET = _generate_stylesheet()
diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py
index f2605e7d3..a3ba0f1da 100644
--- a/qutebrowser/mainwindow/tabwidget.py
+++ b/qutebrowser/mainwindow/tabwidget.py
@@ -37,8 +37,11 @@ from qutebrowser.misc import objects
from qutebrowser.browser import browsertab
-PixelMetrics = enum.IntEnum('PixelMetrics', ['icon_padding'],
- start=QStyle.PM_CustomBase)
+class PixelMetrics(enum.IntEnum):
+
+ """Custom PixelMetrics attributes."""
+
+ icon_padding = QStyle.PM_CustomBase
class TabWidget(QTabWidget):
@@ -339,7 +342,7 @@ class TabWidget(QTabWidget):
qtutils.ensure_valid(url)
return url
- def update_tab_favicon(self, tab: QWidget):
+ def update_tab_favicon(self, tab: QWidget) -> None:
"""Update favicon of the given tab."""
idx = self.indexOf(tab)
@@ -397,7 +400,7 @@ class TabBar(QTabBar):
return self.parent().currentWidget()
@pyqtSlot(str)
- def _on_config_changed(self, option: str):
+ def _on_config_changed(self, option: str) -> None:
if option == 'fonts.tabs':
self._set_font()
elif option == 'tabs.favicons.scale':
@@ -540,7 +543,7 @@ class TabBar(QTabBar):
return
super().mousePressEvent(e)
- def minimumTabSizeHint(self, index, ellipsis: bool = True) -> QSize:
+ def minimumTabSizeHint(self, index: int, ellipsis: bool = True) -> QSize:
"""Set the minimum tab size to indicator/icon/... text.
Args:
@@ -620,7 +623,7 @@ class TabBar(QTabBar):
return False
return widget.data.pinned
- def tabSizeHint(self, index: int):
+ def tabSizeHint(self, index: int) -> QSize:
"""Override tabSizeHint to customize qb's tab size.
https://wiki.python.org/moin/PyQt/Customising%20tab%20bars
diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py
index d5f7c9680..8e57a0223 100644
--- a/qutebrowser/misc/backendproblem.py
+++ b/qutebrowser/misc/backendproblem.py
@@ -38,10 +38,14 @@ from qutebrowser.utils import usertypes, objreg, version, qtutils, log, utils
from qutebrowser.misc import objects, msgbox
-_Result = enum.IntEnum(
- '_Result',
- ['quit', 'restart', 'restart_webkit', 'restart_webengine'],
- start=QDialog.Accepted + 1)
+class _Result(enum.IntEnum):
+
+ """The result code returned by the backend problem dialog."""
+
+ quit = QDialog.Accepted + 1
+ restart = QDialog.Accepted + 2
+ restart_webkit = QDialog.Accepted + 3
+ restart_webengine = QDialog.Accepted + 4
@attr.s
diff --git a/qutebrowser/misc/checkpyver.py b/qutebrowser/misc/checkpyver.py
index 50330ef88..cf8e13810 100644
--- a/qutebrowser/misc/checkpyver.py
+++ b/qutebrowser/misc/checkpyver.py
@@ -30,12 +30,12 @@ try:
except ImportError: # pragma: no cover
try:
# Python2
- from Tkinter import Tk
- import tkMessageBox as messagebox
+ from Tkinter import Tk # type: ignore
+ import tkMessageBox as messagebox # type: ignore
except ImportError:
# Some Python without Tk
- Tk = None
- messagebox = None
+ Tk = None # type: ignore
+ messagebox = None # type: ignore
# First we check the version of Python. This code should run fine with python2
diff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py
index 27dec3345..a846cc59a 100644
--- a/qutebrowser/misc/crashdialog.py
+++ b/qutebrowser/misc/crashdialog.py
@@ -42,8 +42,12 @@ from qutebrowser.misc import (miscwidgets, autoupdate, msgbox, httpclient,
from qutebrowser.config import config, configfiles
-Result = enum.IntEnum('Result', ['restore', 'no_restore'],
- start=QDialog.Accepted + 1)
+class Result(enum.IntEnum):
+
+ """The result code returned by the crash dialog."""
+
+ restore = QDialog.Accepted + 1
+ no_restore = QDialog.Accepted + 2
def parse_fatal_stacktrace(text):
diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py
index b29e1508f..38dffa691 100644
--- a/qutebrowser/misc/earlyinit.py
+++ b/qutebrowser/misc/earlyinit.py
@@ -38,7 +38,7 @@ import datetime
try:
import tkinter
except ImportError:
- tkinter = None
+ tkinter = None # type: ignore
# NOTE: No qutebrowser or PyQt import should be done here, as some early
# initialization needs to take place before that!
diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py
index 78adeb983..e50d803a2 100644
--- a/qutebrowser/misc/sessions.py
+++ b/qutebrowser/misc/sessions.py
@@ -23,6 +23,7 @@ import os
import os.path
import itertools
import urllib
+import typing
from PyQt5.QtCore import QUrl, QObject, QPoint, QTimer
from PyQt5.QtWidgets import QApplication
@@ -37,7 +38,14 @@ from qutebrowser.mainwindow import mainwindow
from qutebrowser.qt import sip
-default = object() # Sentinel value
+class Sentinel:
+
+ """Sentinel value for default argument."""
+
+ pass
+
+
+default = Sentinel()
def init(parent=None):
@@ -109,7 +117,7 @@ class SessionManager(QObject):
def __init__(self, base_path, parent=None):
super().__init__(parent)
- self._current = None
+ self._current = None # type: typing.Optional[str]
self._base_path = base_path
self._last_window_session = None
self.did_load = False
@@ -504,9 +512,13 @@ class SessionManager(QObject):
@cmdutils.argument('name', completion=miscmodels.session)
@cmdutils.argument('win_id', win_id=True)
@cmdutils.argument('with_private', flag='p')
- def session_save(self, name: str = default, current=False, quiet=False,
- force=False, only_active_window=False, with_private=False,
- win_id=None):
+ def session_save(self, name: typing.Union[str, Sentinel] = default,
+ current: bool = False,
+ quiet: bool = False,
+ force: bool = False,
+ only_active_window: bool = False,
+ with_private: bool = False,
+ win_id: int = None) -> None:
"""Save a session.
Args:
@@ -518,7 +530,9 @@ class SessionManager(QObject):
only_active_window: Saves only tabs of the currently active window.
with_private: Include private windows.
"""
- if name is not default and name.startswith('_') and not force:
+ if (not isinstance(name, Sentinel) and
+ name.startswith('_') and
+ not force):
raise cmdexc.CommandError("{} is an internal session, use --force "
"to save anyways.".format(name))
if current:
diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py
index d108a56ac..b893c8d6e 100644
--- a/qutebrowser/misc/utilcmds.py
+++ b/qutebrowser/misc/utilcmds.py
@@ -44,7 +44,7 @@ from qutebrowser.qt import sip
@cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True)
@cmdutils.argument('win_id', win_id=True)
-def later(ms: int, command, win_id):
+def later(ms: int, command: str, win_id: int) -> None:
"""Execute a command after some time.
Args:
@@ -75,7 +75,7 @@ def later(ms: int, command, win_id):
@cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True)
@cmdutils.argument('win_id', win_id=True)
@cmdutils.argument('count', count=True)
-def repeat(times: int, command, win_id, count=None):
+def repeat(times: int, command: str, win_id: int, count: int = None) -> None:
"""Repeat a given command.
Args:
@@ -96,7 +96,8 @@ def repeat(times: int, command, win_id, count=None):
@cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True)
@cmdutils.argument('win_id', win_id=True)
@cmdutils.argument('count', count=True)
-def run_with_count(count_arg: int, command, win_id, count=1):
+def run_with_count(count_arg: int, command: str, win_id: int,
+ count: int = 1) -> None:
"""Run a command with the given count.
If run_with_count itself is run with a count, it multiplies count_arg.
@@ -303,7 +304,7 @@ def repeat_command(win_id, count=None):
@cmdutils.register(debug=True, name='debug-log-capacity')
-def log_capacity(capacity: int):
+def log_capacity(capacity: int) -> None:
"""Change the number of log lines to be stored in RAM.
Args:
@@ -312,6 +313,7 @@ def log_capacity(capacity: int):
if capacity < 0:
raise cmdexc.CommandError("Can't set a negative log capacity!")
else:
+ assert log.ram_handler is not None
log.ram_handler.change_log_capacity(capacity)
@@ -319,18 +321,19 @@ def log_capacity(capacity: int):
@cmdutils.argument('level', choices=sorted(
(level.lower() for level in log.LOG_LEVELS),
key=lambda e: log.LOG_LEVELS[e.upper()]))
-def debug_log_level(level: str):
+def debug_log_level(level: str) -> None:
"""Change the log level for console logging.
Args:
level: The log level to set.
"""
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()])
@cmdutils.register(debug=True)
-def debug_log_filter(filters: str):
+def debug_log_filter(filters: str) -> None:
"""Change the log filter for console logging.
Args:
diff --git a/qutebrowser/qt.py b/qutebrowser/qt.py
index 2878bbe98..d9f1dc58d 100644
--- a/qutebrowser/qt.py
+++ b/qutebrowser/qt.py
@@ -25,4 +25,4 @@
try:
from PyQt5 import sip
except ImportError:
- import sip
+ import sip # type: ignore
diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py
index 381e9ca5d..bbc025515 100644
--- a/qutebrowser/utils/log.py
+++ b/qutebrowser/utils/log.py
@@ -76,12 +76,13 @@ LOG_COLORS = {
# We first monkey-patch logging to support our VDEBUG level before getting the
# loggers. Based on http://stackoverflow.com/a/13638084
+# mypy doesn't know about this, so we need to ignore it.
VDEBUG_LEVEL = 9
logging.addLevelName(VDEBUG_LEVEL, 'VDEBUG')
-logging.VDEBUG = VDEBUG_LEVEL
+logging.VDEBUG = VDEBUG_LEVEL # type: ignore
LOG_LEVELS = {
- 'VDEBUG': logging.VDEBUG,
+ 'VDEBUG': logging.VDEBUG, # type: ignore
'DEBUG': logging.DEBUG,
'INFO': logging.INFO,
'WARNING': logging.WARNING,
@@ -89,17 +90,6 @@ LOG_LEVELS = {
'CRITICAL': logging.CRITICAL,
}
-LOGGER_NAMES = [
- 'statusbar', 'completion', 'init', 'url',
- 'destroy', 'modes', 'webview', 'misc',
- 'mouse', 'procs', 'hints', 'keyboard',
- 'commands', 'signals', 'downloads',
- 'js', 'qt', 'rfc6266', 'ipc', 'shlexer',
- 'save', 'message', 'config', 'sessions',
- 'webelem', 'prompt', 'network', 'sql',
- 'greasemonkey'
-]
-
def vdebug(self, msg, *args, **kwargs):
"""Log with a VDEBUG level.
@@ -114,7 +104,7 @@ def vdebug(self, msg, *args, **kwargs):
# pylint: enable=protected-access
-logging.Logger.vdebug = vdebug
+logging.Logger.vdebug = vdebug # type: ignore
# The different loggers used.
@@ -148,6 +138,17 @@ network = logging.getLogger('network')
sql = logging.getLogger('sql')
greasemonkey = logging.getLogger('greasemonkey')
+LOGGER_NAMES = [
+ 'statusbar', 'completion', 'init', 'url',
+ 'destroy', 'modes', 'webview', 'misc',
+ 'mouse', 'procs', 'hints', 'keyboard',
+ 'commands', 'signals', 'downloads',
+ 'js', 'qt', 'rfc6266', 'ipc', 'shlexer',
+ 'save', 'message', 'config', 'sessions',
+ 'webelem', 'prompt', 'network', 'sql',
+ 'greasemonkey'
+]
+
ram_handler = None
console_handler = None
@@ -467,7 +468,7 @@ def qt_message_handler(msg_type, context, msg):
stack = ''.join(traceback.format_stack())
else:
stack = None
- record = qt.makeRecord(name, level, context.file, context.line, msg, None,
+ record = qt.makeRecord(name, level, context.file, context.line, msg, (),
None, func, sinfo=stack)
qt.handle(record)
diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py
index c634eb95f..a7c30919f 100644
--- a/qutebrowser/utils/qtutils.py
+++ b/qutebrowser/utils/qtutils.py
@@ -39,7 +39,7 @@ from PyQt5.QtCore import (qVersion, QEventLoop, QDataStream, QByteArray,
try:
from PyQt5.QtWebKit import qWebKitVersion
except ImportError: # pragma: no cover
- qWebKitVersion = None
+ qWebKitVersion = None # type: ignore
MAXVALS = {
diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py
index 039d805f9..cd36db49a 100644
--- a/qutebrowser/utils/usertypes.py
+++ b/qutebrowser/utils/usertypes.py
@@ -221,10 +221,15 @@ KeyMode = enum.Enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt',
'jump_mark', 'record_macro', 'run_macro'])
-# Exit statuses for errors. Needs to be an int for sys.exit.
-Exit = enum.IntEnum('Exit', ['ok', 'reserved', 'exception', 'err_ipc',
- 'err_init', 'err_config', 'err_key_config'],
- start=0)
+class Exit(enum.IntEnum):
+
+ """Exit statuses for errors. Needs to be an int for sys.exit."""
+
+ ok = 0
+ reserved = 1
+ exception = 2
+ err_ipc = 3
+ err_init = 4
# Load status of a tab
diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py
index 6119675ba..18dce2b05 100644
--- a/qutebrowser/utils/utils.py
+++ b/qutebrowser/utils/utils.py
@@ -41,7 +41,8 @@ from PyQt5.QtWidgets import QApplication
import pkg_resources
import yaml
try:
- from yaml import CSafeLoader as YamlLoader, CSafeDumper as YamlDumper
+ from yaml import (CSafeLoader as YamlLoader, # type: ignore
+ CSafeDumper as YamlDumper)
YAML_C_EXT = True
except ImportError: # pragma: no cover
from yaml import SafeLoader as YamlLoader, SafeDumper as YamlDumper
diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py
index 7a99a4b65..a52e31ed8 100644
--- a/qutebrowser/utils/version.py
+++ b/qutebrowser/utils/version.py
@@ -42,12 +42,12 @@ from PyQt5.QtWidgets import QApplication
try:
from PyQt5.QtWebKit import qWebKitVersion
except ImportError: # pragma: no cover
- qWebKitVersion = None
+ qWebKitVersion = None # type: ignore
try:
from PyQt5.QtWebEngineWidgets import QWebEngineProfile
except ImportError: # pragma: no cover
- QWebEngineProfile = None
+ QWebEngineProfile = None # type: ignore
import qutebrowser
from qutebrowser.utils import log, utils, standarddir, usertypes, message
diff --git a/scripts/dev/run_vulture.py b/scripts/dev/run_vulture.py
index 0015539f9..b5c083546 100755
--- a/scripts/dev/run_vulture.py
+++ b/scripts/dev/run_vulture.py
@@ -84,6 +84,7 @@ def whitelist_generator(): # noqa
yield 'qutebrowser.utils.log.QtWarningFilter.filter'
yield 'qutebrowser.browser.pdfjs.is_available'
yield 'qutebrowser.misc.guiprocess.spawn_output'
+ yield 'qutebrowser.utils.usertypes.ExitStatus.reserved'
yield 'QEvent.posted'
yield 'log_stack' # from message.py
yield 'propagate' # logging.getLogger('...).propagate = False
diff --git a/tox.ini b/tox.ini
index 8e9a54f11..8a4232aaa 100644
--- a/tox.ini
+++ b/tox.ini
@@ -188,3 +188,14 @@ deps =
whitelist_externals = eslint
changedir = {toxinidir}/qutebrowser/javascript
commands = eslint --color --report-unused-disable-directives .
+
+[testenv:mypy]
+basepython = {env:PYTHON:python3}
+passenv =
+deps =
+ -r{toxinidir}/requirements.txt
+ -r{toxinidir}/misc/requirements/requirements-optional.txt
+ -r{toxinidir}/misc/requirements/requirements-pyqt.txt
+ -r{toxinidir}/misc/requirements/requirements-mypy.txt
+commands =
+ {envpython} -m mypy qutebrowser {posargs}