Merge branch 'mypy'

This commit is contained in:
Florian Bruhin 2018-11-27 16:58:18 +01:00
commit 07c632869d
41 changed files with 257 additions and 107 deletions

View File

@ -38,9 +38,11 @@ matrix:
# env: TESTENV=py35 OSX=yosemite # env: TESTENV=py35 OSX=yosemite
# osx_image: xcode6.4 # osx_image: xcode6.4
- os: linux - os: linux
env: TESTENV=pylint PYTHON=python3.6 env: TESTENV=pylint
- os: linux - os: linux
env: TESTENV=flake8 env: TESTENV=flake8
- os: linux
env: TESTENV=mypy
- os: linux - os: linux
env: TESTENV=docs env: TESTENV=docs
addons: addons:

View File

@ -32,6 +32,7 @@ include doc/changelog.asciidoc
prune tests prune tests
prune qutebrowser/3rdparty prune qutebrowser/3rdparty
exclude pytest.ini exclude pytest.ini
exclude mypy.ini
exclude qutebrowser/javascript/.eslintrc.yaml exclude qutebrowser/javascript/.eslintrc.yaml
exclude qutebrowser/javascript/.eslintignore exclude qutebrowser/javascript/.eslintignore
exclude doc/help exclude doc/help

View File

@ -66,6 +66,7 @@ Fixed
like GMail. However, the default for `content.cookies.accept` is still `all` like GMail. However, the default for `content.cookies.accept` is still `all`
to be in line with what other browsers do. to be in line with what other browsers do.
- `:navigate` not incrementing in anchors or queries or anchors. - `:navigate` not incrementing in anchors or queries or anchors.
- Crash when trying to use a proxy requiring authentication with QtWebKit.
v1.5.2 v1.5.2
------ ------

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
hunter
cssutils
pympler

51
mypy.ini Normal file
View File

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

View File

@ -136,8 +136,8 @@ class AbstractAction:
action_base: The type of the actions (QWeb{Engine,}Page.WebAction) action_base: The type of the actions (QWeb{Engine,}Page.WebAction)
""" """
action_class = None action_class = None # type: type
action_base = None action_base = None # type: type
def __init__(self, tab): def __init__(self, tab):
self._widget = None self._widget = None
@ -685,7 +685,7 @@ class AbstractAudio(QObject):
self._widget = None self._widget = None
self._tab = tab 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. """Set this tab as muted or not.
Arguments: Arguments:
@ -699,7 +699,7 @@ class AbstractAudio(QObject):
"""Whether this tab is muted.""" """Whether this tab is muted."""
raise NotImplementedError 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) self.set_muted(not self.is_muted(), override=override)
def is_recently_audible(self): def is_recently_audible(self):

View File

@ -75,7 +75,7 @@ class CommandDispatcher:
new_window.show() new_window.show()
return new_window.tabbed_browser return new_window.tabbed_browser
def _count(self): def _count(self) -> int:
"""Convenience method to get the widget count.""" """Convenience method to get the widget count."""
return self._tabbed_browser.widget.count() return self._tabbed_browser.widget.count()
@ -513,7 +513,8 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('win_id', completion=miscmodels.window) @cmdutils.argument('win_id', completion=miscmodels.window)
@cmdutils.argument('count', count=True) @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. """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. 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', @cmdutils.argument('where', choices=['prev', 'next', 'up', 'increment',
'decrement']) 'decrement'])
@cmdutils.argument('count', count=True) @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. """Open typical prev/next links or navigate using the URL path.
This tries to automatically click on typical _Previous Page_ or This tries to automatically click on typical _Previous Page_ or
@ -645,7 +647,7 @@ class CommandDispatcher:
inc_or_dec='decrement'), inc_or_dec='decrement'),
'increment': functools.partial(navigate.incdec, 'increment': functools.partial(navigate.incdec,
inc_or_dec='increment'), inc_or_dec='increment'),
} } # type: typing.Dict[str, typing.Callable]
try: try:
if where in ['prev', 'next']: if where in ['prev', 'next']:
@ -665,7 +667,7 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True) @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. """Scroll the current tab by 'count * dx/dy' pixels.
Args: Args:
@ -681,7 +683,7 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True) @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. """Scroll the current tab in the given direction.
Note you can use `:run-with-count` to have a keybinding with a bigger 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.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True) @cmdutils.argument('count', count=True)
@cmdutils.argument('horizontal', flag='x') @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. """Scroll to a specific percentage of the page.
The percentage can be given either as argument or as count. The percentage can be given either as argument or as count.
@ -764,7 +767,7 @@ class CommandDispatcher:
choices=('next', 'increment')) choices=('next', 'increment'))
def scroll_page(self, x: float, y: float, *, def scroll_page(self, x: float, y: float, *,
top_navigate: str = None, bottom_navigate: str = None, top_navigate: str = None, bottom_navigate: str = None,
count=1): count: int = 1) -> None:
"""Scroll the frame page-wise. """Scroll the frame page-wise.
Args: Args:
@ -1120,7 +1123,7 @@ class CommandDispatcher:
@cmdutils.argument('index', choices=['last']) @cmdutils.argument('index', choices=['last'])
@cmdutils.argument('count', count=True) @cmdutils.argument('count', count=True)
def tab_focus(self, index: typing.Union[str, int] = None, 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]. """Select the tab given as argument/[count].
If neither count nor index are given, it behaves like tab-next. If neither count nor index are given, it behaves like tab-next.
@ -1143,6 +1146,8 @@ class CommandDispatcher:
self.tab_next() self.tab_next()
return return
assert isinstance(index, int)
if index < 0: if index < 0:
index = self._count() + index + 1 index = self._count() + index + 1
@ -1159,7 +1164,8 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('index', choices=['+', '-']) @cmdutils.argument('index', choices=['+', '-'])
@cmdutils.argument('count', count=True) @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]. """Move the current tab according to the argument and [count].
If neither is given, move it to the first position. If neither is given, move it to the first position.
@ -1188,6 +1194,7 @@ class CommandDispatcher:
if count is not None: if count is not None:
new_idx = count - 1 new_idx = count - 1
elif index is not None: elif index is not None:
assert isinstance(index, int)
new_idx = index - 1 if index >= 0 else index + self._count() new_idx = index - 1 if index >= 0 else index + self._count()
else: else:
new_idx = 0 new_idx = 0
@ -1715,10 +1722,10 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('filter_', choices=['id']) @cmdutils.argument('filter_', choices=['id'])
def click_element(self, filter_: str, value, *, def click_element(self, filter_: str, value: str, *,
target: usertypes.ClickTarget = target: usertypes.ClickTarget =
usertypes.ClickTarget.normal, usertypes.ClickTarget.normal,
force_event=False): force_event: bool = False) -> None:
"""Click the element matching the given filter. """Click the element matching the given filter.
The given filter needs to result in exactly one element, otherwise, an 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', @cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0, no_cmd_split=True) maxsplit=0, no_cmd_split=True)
def jseval(self, js_code, file=False, quiet=False, *, def jseval(self, js_code: str, file: bool = False, quiet: bool = False, *,
world: typing.Union[usertypes.JsWorld, int] = None): world: typing.Union[usertypes.JsWorld, int] = None) -> None:
"""Evaluate a JavaScript string. """Evaluate a JavaScript string.
Args: Args:

View File

@ -40,7 +40,11 @@ from qutebrowser.utils import (usertypes, standarddir, utils, message, log,
from qutebrowser.qt import sip 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 # Remember the last used directory
@ -1058,7 +1062,7 @@ class DownloadModel(QAbstractListModel):
@cmdutils.register(instance='download-model', scope='window', maxsplit=0) @cmdutils.register(instance='download-model', scope='window', maxsplit=0)
@cmdutils.argument('count', count=True) @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. """Open the last/[count]th download.
If no specific command is given, this will use the system's default If no specific command is given, this will use the system's default

View File

@ -737,7 +737,7 @@ class HintManager(QObject):
self._context.baseurl = tabbed_browser.current_url() self._context.baseurl = tabbed_browser.current_url()
except qtutils.QtValueError: except qtutils.QtValueError:
raise cmdexc.CommandError("No URL set for this page yet!") raise cmdexc.CommandError("No URL set for this page yet!")
self._context.args = args self._context.args = list(args)
self._context.group = group self._context.group = group
try: try:

View File

@ -37,7 +37,7 @@ try:
import secrets import secrets
except ImportError: except ImportError:
# New in Python 3.6 # New in Python 3.6
secrets = None secrets = None # type: ignore
from PyQt5.QtCore import QUrlQuery, QUrl, qVersion from PyQt5.QtCore import QUrlQuery, QUrl, qVersion

View File

@ -670,7 +670,7 @@ class WebEngineAudio(browsertab.AbstractAudio):
self._tab.url_changed.connect(self._on_url_changed) self._tab.url_changed.connect(self._on_url_changed)
config.instance.changed.connect(self._on_config_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 self._overridden = override
page = self._widget.page() page = self._widget.page()
page.setAudioMuted(muted) page.setAudioMuted(muted)

View File

@ -240,7 +240,7 @@ class WebEnginePage(QWebEnginePage):
def acceptNavigationRequest(self, def acceptNavigationRequest(self,
url: QUrl, url: QUrl,
typ: QWebEnginePage.NavigationType, typ: QWebEnginePage.NavigationType,
is_main_frame: bool): is_main_frame: bool) -> bool:
"""Override acceptNavigationRequest to forward it to the tab API.""" """Override acceptNavigationRequest to forward it to the tab API."""
type_map = { type_map = {
QWebEnginePage.NavigationTypeLinkClicked: QWebEnginePage.NavigationTypeLinkClicked:

View File

@ -21,6 +21,7 @@
import collections import collections
import html import html
import typing # pylint: disable=unused-import
import attr import attr
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QCoreApplication, QUrl, 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 PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QSslSocket
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.mainwindow import prompt # pylint: disable=unused-import
from qutebrowser.utils import (message, log, usertypes, utils, objreg, from qutebrowser.utils import (message, log, usertypes, utils, objreg,
urlutils, debug) urlutils, debug)
from qutebrowser.browser import shared from qutebrowser.browser import shared
@ -37,7 +39,7 @@ from qutebrowser.browser.webkit.network import (webkitqutescheme, networkreply,
HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%' HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%'
_proxy_auth_cache = {} _proxy_auth_cache = {} # type: typing.Dict[ProxyId, prompt.AuthInfo]
@attr.s(frozen=True) @attr.s(frozen=True)
@ -295,9 +297,9 @@ class NetworkManager(QNetworkAccessManager):
"""Called when a proxy needs authentication.""" """Called when a proxy needs authentication."""
proxy_id = ProxyId(proxy.type(), proxy.hostName(), proxy.port()) proxy_id = ProxyId(proxy.type(), proxy.hostName(), proxy.port())
if proxy_id in _proxy_auth_cache: if proxy_id in _proxy_auth_cache:
user, password = _proxy_auth_cache[proxy_id] authinfo = _proxy_auth_cache[proxy_id]
authenticator.setUser(user) authenticator.setUser(authinfo.user)
authenticator.setPassword(password) authenticator.setPassword(authinfo.password)
else: else:
msg = '<b>{}</b> says:<br/>{}'.format( msg = '<b>{}</b> says:<br/>{}'.format(
html.escape(proxy.hostName()), html.escape(proxy.hostName()),

View File

@ -641,7 +641,7 @@ class WebKitAudio(browsertab.AbstractAudio):
"""Dummy handling of audio status for QtWebKit.""" """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!') raise browsertab.WebTabError('Muting is not supported on QtWebKit!')
def is_muted(self): def is_muted(self):

View File

@ -469,7 +469,7 @@ class BrowserPage(QWebPage):
def acceptNavigationRequest(self, def acceptNavigationRequest(self,
frame: QWebFrame, frame: QWebFrame,
request: QNetworkRequest, request: QNetworkRequest,
typ: QWebPage.NavigationType): typ: QWebPage.NavigationType) -> bool:
"""Override acceptNavigationRequest to handle clicked links. """Override acceptNavigationRequest to handle clicked links.
Setting linkDelegationPolicy to DelegateAllLinks and using a slot bound Setting linkDelegationPolicy to DelegateAllLinks and using a slot bound

View File

@ -17,18 +17,15 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Contains various command utils and a global command dict. """Contains various command utils and a global command dict."""
Module attributes:
cmd_dict: A mapping from command-strings to command objects.
"""
import inspect import inspect
import typing # pylint: disable=unused-import
from qutebrowser.utils import qtutils, log from qutebrowser.utils import qtutils, log
from qutebrowser.commands import command, cmdexc from qutebrowser.commands import command, cmdexc
cmd_dict = {} cmd_dict = {} # type: typing.Dict[str, command.Command]
def check_overflow(arg, ctype): def check_overflow(arg, ctype):

View File

@ -58,6 +58,9 @@ def _current_url(tabbed_browser):
def replace_variables(win_id, arglist): def replace_variables(win_id, arglist):
"""Utility function to replace variables like {url} in a list of args.""" """Utility function to replace variables like {url} in a list of args."""
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
variables = { variables = {
'url': lambda: _current_url(tabbed_browser).toString( 'url': lambda: _current_url(tabbed_browser).toString(
QUrl.FullyEncoded | QUrl.RemovePassword), QUrl.FullyEncoded | QUrl.RemovePassword),
@ -67,13 +70,13 @@ def replace_variables(win_id, arglist):
'clipboard': utils.get_clipboard, 'clipboard': utils.get_clipboard,
'primary': lambda: utils.get_clipboard(selection=True), 'primary': lambda: utils.get_clipboard(selection=True),
} }
for key in list(variables): for key in list(variables):
modified_key = '{' + key + '}' modified_key = '{' + key + '}'
variables[modified_key] = lambda x=modified_key: x variables[modified_key] = lambda x=modified_key: x
values = {} values = {}
args = [] args = []
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
def repl_cb(matchobj): def repl_cb(matchobj):
"""Return replacement for given match.""" """Return replacement for given match."""

View File

@ -22,6 +22,7 @@
import copy import copy
import contextlib import contextlib
import functools import functools
import typing
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject 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.misc import objects
from qutebrowser.keyinput import keyutils 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 # An easy way to access the config from other code via config.val.foo
val = None val = typing.cast('ConfigContainer', None)
instance = None instance = typing.cast('Config', None)
key_instance = None key_instance = typing.cast('KeyConfig', None)
cache = None cache = typing.cast('configcache.ConfigCache', None)
# Keeping track of all change filters to validate them later. # Keeping track of all change filters to validate them later.
change_filters = [] change_filters = []

View File

@ -20,6 +20,8 @@
"""Implementation of a basic config cache.""" """Implementation of a basic config cache."""
import typing
from qutebrowser.config import config from qutebrowser.config import config
@ -36,14 +38,14 @@ class ConfigCache:
""" """
def __init__(self) -> None: def __init__(self) -> None:
self._cache = {} self._cache = {} # type: typing.Dict[str, typing.Any]
config.instance.changed.connect(self._on_config_changed) config.instance.changed.connect(self._on_config_changed)
def _on_config_changed(self, attr: str) -> None: def _on_config_changed(self, attr: str) -> None:
if attr in self._cache: if attr in self._cache:
self._cache[attr] = config.instance.get(attr) 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: if attr not in self._cache:
assert not config.instance.get_opt(attr).supports_pattern assert not config.instance.get_opt(attr).supports_pattern
self._cache[attr] = config.instance.get(attr) self._cache[attr] = config.instance.get(attr)

View File

@ -52,6 +52,7 @@ import datetime
import functools import functools
import operator import operator
import json import json
import typing # pylint: disable=unused-import
import attr import attr
import yaml import yaml
@ -304,7 +305,7 @@ class MappingType(BaseType):
MAPPING: The mapping to use. MAPPING: The mapping to use.
""" """
MAPPING = {} MAPPING = {} # type: typing.Dict[str, typing.Any]
def __init__(self, none_ok=False, valid_values=None): def __init__(self, none_ok=False, valid_values=None):
super().__init__(none_ok) super().__init__(none_ok)
@ -576,7 +577,7 @@ class FlagList(List):
the valid values of the setting. the valid values of the setting.
""" """
combinable_values = None combinable_values = None # type: typing.Optional[typing.Iterable]
_show_valtype = False _show_valtype = False
@ -1118,7 +1119,7 @@ class QtFont(Font):
font.setWeight(weight_map[namedweight]) font.setWeight(weight_map[namedweight])
if weight: if weight:
# based on qcssparser.cpp:setFontWeightFromValue # based on qcssparser.cpp:setFontWeightFromValue
font.setWeight(min(int(weight) / 8, 99)) font.setWeight(min(int(weight) // 8, 99))
if size: if size:
if size.lower().endswith('pt'): if size.lower().endswith('pt'):
font.setPointSizeF(float(size[:-2])) font.setPointSizeF(float(size[:-2]))

View File

@ -19,6 +19,8 @@
"""Bridge from QWeb(Engine)Settings to our own settings.""" """Bridge from QWeb(Engine)Settings to our own settings."""
import typing # pylint: disable=unused-import
from PyQt5.QtGui import QFont from PyQt5.QtGui import QFont
from qutebrowser.config import config, configutils from qutebrowser.config import config, configutils
@ -44,10 +46,10 @@ class AbstractSettings:
"""Abstract base class for settings set via QWeb(Engine)Settings.""" """Abstract base class for settings set via QWeb(Engine)Settings."""
_ATTRIBUTES = None _ATTRIBUTES = {} # type: typing.Dict[str, AttributeInfo]
_FONT_SIZES = None _FONT_SIZES = {} # type: typing.Dict[str, typing.Any]
_FONT_FAMILIES = None _FONT_FAMILIES = {} # type: typing.Dict[str, typing.Any]
_FONT_TO_QFONT = None _FONT_TO_QFONT = {} # type: typing.Dict[typing.Any, QFont.StyleHint]
def __init__(self, settings): def __init__(self, settings):
self._settings = settings self._settings = settings

View File

@ -391,7 +391,8 @@ class PromptContainer(QWidget):
@cmdutils.register(instance='prompt-container', scope='window', @cmdutils.register(instance='prompt-container', scope='window',
modes=[usertypes.KeyMode.prompt], maxsplit=0) 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. """Immediately open a download.
If no specific command is given, this will use the system's default If no specific command is given, this will use the system's default

View File

@ -145,7 +145,7 @@ class StatusBar(QWidget):
resized = pyqtSignal('QRect') resized = pyqtSignal('QRect')
moved = pyqtSignal('QPoint') moved = pyqtSignal('QPoint')
_severity = None _severity = None
_color_flags = [] _color_flags = None
STYLESHEET = _generate_stylesheet() STYLESHEET = _generate_stylesheet()

View File

@ -37,8 +37,11 @@ from qutebrowser.misc import objects
from qutebrowser.browser import browsertab from qutebrowser.browser import browsertab
PixelMetrics = enum.IntEnum('PixelMetrics', ['icon_padding'], class PixelMetrics(enum.IntEnum):
start=QStyle.PM_CustomBase)
"""Custom PixelMetrics attributes."""
icon_padding = QStyle.PM_CustomBase
class TabWidget(QTabWidget): class TabWidget(QTabWidget):
@ -339,7 +342,7 @@ class TabWidget(QTabWidget):
qtutils.ensure_valid(url) qtutils.ensure_valid(url)
return url return url
def update_tab_favicon(self, tab: QWidget): def update_tab_favicon(self, tab: QWidget) -> None:
"""Update favicon of the given tab.""" """Update favicon of the given tab."""
idx = self.indexOf(tab) idx = self.indexOf(tab)
@ -397,7 +400,7 @@ class TabBar(QTabBar):
return self.parent().currentWidget() return self.parent().currentWidget()
@pyqtSlot(str) @pyqtSlot(str)
def _on_config_changed(self, option: str): def _on_config_changed(self, option: str) -> None:
if option == 'fonts.tabs': if option == 'fonts.tabs':
self._set_font() self._set_font()
elif option == 'tabs.favicons.scale': elif option == 'tabs.favicons.scale':
@ -540,7 +543,7 @@ class TabBar(QTabBar):
return return
super().mousePressEvent(e) 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. """Set the minimum tab size to indicator/icon/... text.
Args: Args:
@ -620,7 +623,7 @@ class TabBar(QTabBar):
return False return False
return widget.data.pinned return widget.data.pinned
def tabSizeHint(self, index: int): def tabSizeHint(self, index: int) -> QSize:
"""Override tabSizeHint to customize qb's tab size. """Override tabSizeHint to customize qb's tab size.
https://wiki.python.org/moin/PyQt/Customising%20tab%20bars https://wiki.python.org/moin/PyQt/Customising%20tab%20bars

View File

@ -38,10 +38,14 @@ from qutebrowser.utils import usertypes, objreg, version, qtutils, log, utils
from qutebrowser.misc import objects, msgbox from qutebrowser.misc import objects, msgbox
_Result = enum.IntEnum( class _Result(enum.IntEnum):
'_Result',
['quit', 'restart', 'restart_webkit', 'restart_webengine'], """The result code returned by the backend problem dialog."""
start=QDialog.Accepted + 1)
quit = QDialog.Accepted + 1
restart = QDialog.Accepted + 2
restart_webkit = QDialog.Accepted + 3
restart_webengine = QDialog.Accepted + 4
@attr.s @attr.s

View File

@ -30,12 +30,12 @@ try:
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
try: try:
# Python2 # Python2
from Tkinter import Tk from Tkinter import Tk # type: ignore
import tkMessageBox as messagebox import tkMessageBox as messagebox # type: ignore
except ImportError: except ImportError:
# Some Python without Tk # Some Python without Tk
Tk = None Tk = None # type: ignore
messagebox = None messagebox = None # type: ignore
# First we check the version of Python. This code should run fine with python2 # First we check the version of Python. This code should run fine with python2

View File

@ -42,8 +42,12 @@ from qutebrowser.misc import (miscwidgets, autoupdate, msgbox, httpclient,
from qutebrowser.config import config, configfiles from qutebrowser.config import config, configfiles
Result = enum.IntEnum('Result', ['restore', 'no_restore'], class Result(enum.IntEnum):
start=QDialog.Accepted + 1)
"""The result code returned by the crash dialog."""
restore = QDialog.Accepted + 1
no_restore = QDialog.Accepted + 2
def parse_fatal_stacktrace(text): def parse_fatal_stacktrace(text):

View File

@ -38,7 +38,7 @@ import datetime
try: try:
import tkinter import tkinter
except ImportError: except ImportError:
tkinter = None tkinter = None # type: ignore
# NOTE: No qutebrowser or PyQt import should be done here, as some early # NOTE: No qutebrowser or PyQt import should be done here, as some early
# initialization needs to take place before that! # initialization needs to take place before that!

View File

@ -23,6 +23,7 @@ import os
import os.path import os.path
import itertools import itertools
import urllib import urllib
import typing
from PyQt5.QtCore import QUrl, QObject, QPoint, QTimer from PyQt5.QtCore import QUrl, QObject, QPoint, QTimer
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
@ -37,7 +38,14 @@ from qutebrowser.mainwindow import mainwindow
from qutebrowser.qt import sip from qutebrowser.qt import sip
default = object() # Sentinel value class Sentinel:
"""Sentinel value for default argument."""
pass
default = Sentinel()
def init(parent=None): def init(parent=None):
@ -109,7 +117,7 @@ class SessionManager(QObject):
def __init__(self, base_path, parent=None): def __init__(self, base_path, parent=None):
super().__init__(parent) super().__init__(parent)
self._current = None self._current = None # type: typing.Optional[str]
self._base_path = base_path self._base_path = base_path
self._last_window_session = None self._last_window_session = None
self.did_load = False self.did_load = False
@ -504,9 +512,13 @@ class SessionManager(QObject):
@cmdutils.argument('name', completion=miscmodels.session) @cmdutils.argument('name', completion=miscmodels.session)
@cmdutils.argument('win_id', win_id=True) @cmdutils.argument('win_id', win_id=True)
@cmdutils.argument('with_private', flag='p') @cmdutils.argument('with_private', flag='p')
def session_save(self, name: str = default, current=False, quiet=False, def session_save(self, name: typing.Union[str, Sentinel] = default,
force=False, only_active_window=False, with_private=False, current: bool = False,
win_id=None): quiet: bool = False,
force: bool = False,
only_active_window: bool = False,
with_private: bool = False,
win_id: int = None) -> None:
"""Save a session. """Save a session.
Args: Args:
@ -518,7 +530,9 @@ class SessionManager(QObject):
only_active_window: Saves only tabs of the currently active window. only_active_window: Saves only tabs of the currently active window.
with_private: Include private windows. 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 " raise cmdexc.CommandError("{} is an internal session, use --force "
"to save anyways.".format(name)) "to save anyways.".format(name))
if current: if current:

View File

@ -44,7 +44,7 @@ from qutebrowser.qt import sip
@cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True) @cmdutils.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True)
@cmdutils.argument('win_id', win_id=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. """Execute a command after some time.
Args: 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.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True)
@cmdutils.argument('win_id', win_id=True) @cmdutils.argument('win_id', win_id=True)
@cmdutils.argument('count', count=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. """Repeat a given command.
Args: 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.register(maxsplit=1, no_cmd_split=True, no_replace_variables=True)
@cmdutils.argument('win_id', win_id=True) @cmdutils.argument('win_id', win_id=True)
@cmdutils.argument('count', count=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. """Run a command with the given count.
If run_with_count itself is run with a count, it multiplies count_arg. 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') @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. """Change the number of log lines to be stored in RAM.
Args: Args:
@ -312,6 +313,7 @@ def log_capacity(capacity: int):
if capacity < 0: if capacity < 0:
raise cmdexc.CommandError("Can't set a negative log capacity!") raise cmdexc.CommandError("Can't set a negative log capacity!")
else: else:
assert log.ram_handler is not None
log.ram_handler.change_log_capacity(capacity) log.ram_handler.change_log_capacity(capacity)
@ -319,18 +321,19 @@ def log_capacity(capacity: int):
@cmdutils.argument('level', choices=sorted( @cmdutils.argument('level', choices=sorted(
(level.lower() for level in log.LOG_LEVELS), (level.lower() for level in log.LOG_LEVELS),
key=lambda e: log.LOG_LEVELS[e.upper()])) 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. """Change the log level for console logging.
Args: Args:
level: The log level to set. level: The log level to set.
""" """
log.change_console_formatter(log.LOG_LEVELS[level.upper()]) 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()]) log.console_handler.setLevel(log.LOG_LEVELS[level.upper()])
@cmdutils.register(debug=True) @cmdutils.register(debug=True)
def debug_log_filter(filters: str): def debug_log_filter(filters: str) -> None:
"""Change the log filter for console logging. """Change the log filter for console logging.
Args: Args:

View File

@ -25,4 +25,4 @@
try: try:
from PyQt5 import sip from PyQt5 import sip
except ImportError: except ImportError:
import sip import sip # type: ignore

View File

@ -76,12 +76,13 @@ LOG_COLORS = {
# We first monkey-patch logging to support our VDEBUG level before getting the # We first monkey-patch logging to support our VDEBUG level before getting the
# loggers. Based on http://stackoverflow.com/a/13638084 # loggers. Based on http://stackoverflow.com/a/13638084
# mypy doesn't know about this, so we need to ignore it.
VDEBUG_LEVEL = 9 VDEBUG_LEVEL = 9
logging.addLevelName(VDEBUG_LEVEL, 'VDEBUG') logging.addLevelName(VDEBUG_LEVEL, 'VDEBUG')
logging.VDEBUG = VDEBUG_LEVEL logging.VDEBUG = VDEBUG_LEVEL # type: ignore
LOG_LEVELS = { LOG_LEVELS = {
'VDEBUG': logging.VDEBUG, 'VDEBUG': logging.VDEBUG, # type: ignore
'DEBUG': logging.DEBUG, 'DEBUG': logging.DEBUG,
'INFO': logging.INFO, 'INFO': logging.INFO,
'WARNING': logging.WARNING, 'WARNING': logging.WARNING,
@ -89,17 +90,6 @@ LOG_LEVELS = {
'CRITICAL': logging.CRITICAL, '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): def vdebug(self, msg, *args, **kwargs):
"""Log with a VDEBUG level. """Log with a VDEBUG level.
@ -114,7 +104,7 @@ def vdebug(self, msg, *args, **kwargs):
# pylint: enable=protected-access # pylint: enable=protected-access
logging.Logger.vdebug = vdebug logging.Logger.vdebug = vdebug # type: ignore
# The different loggers used. # The different loggers used.
@ -148,6 +138,17 @@ network = logging.getLogger('network')
sql = logging.getLogger('sql') sql = logging.getLogger('sql')
greasemonkey = logging.getLogger('greasemonkey') 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 ram_handler = None
console_handler = None console_handler = None
@ -467,7 +468,7 @@ def qt_message_handler(msg_type, context, msg):
stack = ''.join(traceback.format_stack()) stack = ''.join(traceback.format_stack())
else: else:
stack = None 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) None, func, sinfo=stack)
qt.handle(record) qt.handle(record)

View File

@ -39,7 +39,7 @@ from PyQt5.QtCore import (qVersion, QEventLoop, QDataStream, QByteArray,
try: try:
from PyQt5.QtWebKit import qWebKitVersion from PyQt5.QtWebKit import qWebKitVersion
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
qWebKitVersion = None qWebKitVersion = None # type: ignore
MAXVALS = { MAXVALS = {

View File

@ -221,10 +221,15 @@ KeyMode = enum.Enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt',
'jump_mark', 'record_macro', 'run_macro']) 'jump_mark', 'record_macro', 'run_macro'])
# Exit statuses for errors. Needs to be an int for sys.exit. class Exit(enum.IntEnum):
Exit = enum.IntEnum('Exit', ['ok', 'reserved', 'exception', 'err_ipc',
'err_init', 'err_config', 'err_key_config'], """Exit statuses for errors. Needs to be an int for sys.exit."""
start=0)
ok = 0
reserved = 1
exception = 2
err_ipc = 3
err_init = 4
# Load status of a tab # Load status of a tab

View File

@ -41,7 +41,8 @@ from PyQt5.QtWidgets import QApplication
import pkg_resources import pkg_resources
import yaml import yaml
try: 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 YAML_C_EXT = True
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
from yaml import SafeLoader as YamlLoader, SafeDumper as YamlDumper from yaml import SafeLoader as YamlLoader, SafeDumper as YamlDumper

View File

@ -42,12 +42,12 @@ from PyQt5.QtWidgets import QApplication
try: try:
from PyQt5.QtWebKit import qWebKitVersion from PyQt5.QtWebKit import qWebKitVersion
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
qWebKitVersion = None qWebKitVersion = None # type: ignore
try: try:
from PyQt5.QtWebEngineWidgets import QWebEngineProfile from PyQt5.QtWebEngineWidgets import QWebEngineProfile
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
QWebEngineProfile = None QWebEngineProfile = None # type: ignore
import qutebrowser import qutebrowser
from qutebrowser.utils import log, utils, standarddir, usertypes, message from qutebrowser.utils import log, utils, standarddir, usertypes, message

View File

@ -84,6 +84,7 @@ def whitelist_generator(): # noqa
yield 'qutebrowser.utils.log.QtWarningFilter.filter' yield 'qutebrowser.utils.log.QtWarningFilter.filter'
yield 'qutebrowser.browser.pdfjs.is_available' yield 'qutebrowser.browser.pdfjs.is_available'
yield 'qutebrowser.misc.guiprocess.spawn_output' yield 'qutebrowser.misc.guiprocess.spawn_output'
yield 'qutebrowser.utils.usertypes.ExitStatus.reserved'
yield 'QEvent.posted' yield 'QEvent.posted'
yield 'log_stack' # from message.py yield 'log_stack' # from message.py
yield 'propagate' # logging.getLogger('...).propagate = False yield 'propagate' # logging.getLogger('...).propagate = False

11
tox.ini
View File

@ -188,3 +188,14 @@ deps =
whitelist_externals = eslint whitelist_externals = eslint
changedir = {toxinidir}/qutebrowser/javascript changedir = {toxinidir}/qutebrowser/javascript
commands = eslint --color --report-unused-disable-directives . 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}