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}