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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = '<b>{}</b> says:<br/>{}'.format(
html.escape(proxy.hostName()),

View File

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

View File

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

View File

@ -17,18 +17,15 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""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):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -25,4 +25,4 @@
try:
from PyQt5 import sip
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
# 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)

View File

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

View File

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

View File

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

View File

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

View File

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

11
tox.ini
View File

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