mypy: Upgrade to PyQt5-stubs 5.15.6.0

For some unknown reason, those new stubs cause a *lot* of things now to be
checked by mypy which formerly probably got skipped due to Any being implied
somewhere.

The stubs themselves mainly improved, with a couple of regressions too.

In total, there were some 337 (!) new mypy errors. This commit fixes almost all
of them, and the next commit improves a fix to get things down to 0 errors
again.

Overview of the changes:

==== qutebrowser/app.py

- Drop type ignore due to improved stubs.

==== qutebrowser/browser/browsertab.py

- Specify the type of _widget members more closely than just QWidget.
  This is debatable: I suppose the abstract stuff shouldn't need to know
  anything about the concrete backends at all. But it seems like we cut some
  corners when initially implementing things, and put some code in browsertab.py
  just because the APIs of both backends happened to be compatible. Perhaps
  something to reconsider once we drop QtWebKit and hopefully implement a dummy
  backend.

- Add an additional assertion in AbstractAction.run_string. This is already
  covered by the isinstance(member, self.action_base) above it, but that's too
  dynamic for mypy to understand.

- Fix the return type of AbstractScroller.pos_px, which is a QPoint (with x
  and y components), not a single int.

- Fix the return type of AbstractScroller.pos_perc, which is a Tuple (with x
  and y components), not a single int.

- Fix the argument types of AbstractScroller.to_perc, as it's possible to pass
  fractional percentages too.

- Specify the type for AbstractHistoryPrivate._history. See above (_widget) re
  this being debatable.

- Fix the return type of AbstractTabPrivate.event_target(), which can be None
  (see #3888).

- Fix the return type of AbstractTabPrivate.run_js_sync, which is Any (the JS
  return value), not None.

- Fix the argument type for AbstractTabPrivate.toggle_inspector: position can
  be None to use the last used position.

- Declare the type of sub-objects of AbstractTab.

- Fix the return value of AbstractTab.icon(), which is the QIcon, not None.

==== qutebrowser/browser/commands.py

- Make sure the active window is a MainWindow (with a .win_id attribute).

==== qutebrowser/browser/downloadview.py

- Add _model() which makes sure that self.model() is a DownloadModel, not None
  or any other model. This is needed because other methods access a variety of
  custom attributes on it, e.g. last_index().

==== qutebrowser/browser/greasemonkey.py

- Add an ignore for AbstractDownload.requested_url which we patch onto the
  downloads. Probably would be nicer to add it as a proper attribute which always
  gets set by the DownloadManager.

==== qutebrowser/browser/hints.py

- Remove type ignores for QUrl.toString().
- Add a new type ignore for combining different URL flags (which works, but is
  not exactly type safe... still probably a regression in the stubs).
- Make sure the things we get back from self._get_keyparser are what we actually
  expect. Probably should introduce a TypedDict (and/or overloads for
  _get_keyparser with typing.Literal) to teach mypy about the exact return value.
  See #7098.
  This is needed because we access Hint/NormalKeyParser-specific attributes such
  as .set_inhibited_timout() or .update_bindings().

==== qutebrowser/browser/inspector.py

- Similar changes than in browsertab.py to make some types where we share API
  (e.g. .setPage()) more concrete. Didn't work out unfortunately, see next
  commit.

==== qutebrowser/browser/network/pac.py

- Remove now unneeded type ignore for signal.

==== qutebrowser/browser/qtnetworkdownloads.py

- Make sure that downloads is a qtnetworkdownloads.DownloadItem (rather than an
  AbstractDownload), so that we can call ._uses_nam() on it.

==== qutebrowser/browser/qutescheme.py

- Remove now unneeded type ignore for QUrl flags.

==== qutebrowser/browser/urlmarks.py

- Specify the type of UrlMarkManager._lineparser, as those only get initialized
  in _init_lineparser of subclasses, so mypy doesn't know it's supposed to exist.

==== qutebrowser/browser/webelem.py

- New casts to turn single KeyboardModifier (enum) entries into
  KeyboardModifiers (flags). Might not be needed anymore with Qt 6.
- With that, casting the final value is now unneeded.

==== qutebrowser/browser/webengine/notification.py

- Remove now unneeded type ignore for signal.
- Make sure the self.sender() we get in HerbeNotificationAdapter._on_finished()
  is a QProcess, not just any QObject.

==== qutebrowser/browser/webengine/webenginedownloads.py

- Remove now unneeded type ignores for signals.

==== qutebrowser/browser/webengine/webengineelem.py

- Specify the type of WebEngineElement._tab.
- Remove now unneeded type ignore for mixed flags.

==== qutebrowser/browser/webengine/webengineinspector.py

- See changes to inspector.py and next commit.
- Remove now unneeded type ignore for signal.

==== qutebrowser/browser/webengine/webenginequtescheme.py

- Remove now unneeded type ignore for mixed flags.

==== qutebrowser/browser/webengine/webenginesettings.py

- Ignore access of .setter attribute which we patch onto QWebEngineProfile.
  Would be nice to have a subclass or wrapper-class instead.

==== qutebrowser/browser/webengine/webenginetab.py

- Specified the type of _widget members more closely than just QWidget.
  See browsertab.py changes for details.
- Remove some now-unneeded type ignores for creating FindFlags.
- Specify more concrete types for WebEngineTab members where we actually need to
  access WebEngine-specific attributes.
- Make sure the page we get is our custom WebEnginePage subclass, not just any
  QWebEnginePage. This is needed because we access custom attributes on it.

==== qutebrowser/browser/webengine/webview.py

- Make sure the page we get is our custom WebEnginePage subclass, not just any
  QWebEnginePage. This is needed because we access custom attributes on it.

==== qutebrowser/browser/webkit/network/networkreply.py

- Remove now unneeded type ignores for signals.

==== qutebrowser/browser/webkit/webkitinspector.py

- See changes to inspector.py and next commit.

==== qutebrowser/browser/webkit/webkittab.py

- Specify the type of _widget members more closely than just QWidget.
  See browsertab.py changes for details.
- Add a type ignore for WebKitAction because our workaround needs to
  treat them as ints (which is allowed by PyQt, even if not type-safe).
- Add new ignores for findText calls: The text is a QString and can be None; the
  flags are valid despite mypy thinking they aren't (stubs regression?).
- Specify the type for WebKitHistoryPrivate._history, because we access
  WebKit-specific attributes. See above (_widget) re this being debatable.
- Make mypy aware that .currentFrame() and .frameAt() can return None (stubs
  regression?).
- Make sure the .page() and .page().networkAccessManager() are our subclasses
  rather than the more generic QtWebKit objects, as we use custom attributes.
- Add new type ignores for signals (stubs regression!)

==== qutebrowser/browser/webkit/webpage.py

- Make sure the .networkAccessManager() is our subclass rather than the more
  generic QtWebKit object, as we use custom attributes.
- Replace a cast by a type ignore. The cast didn't work anymore.

==== qutebrowser/browser/webkit/webview.py

- Make sure the .page() is our subclass rather than the more generic QtWebKit
  object, as we use custom attributes.

==== qutebrowser/commands/userscripts.py

- Remove now unneeded type ignore for signal.

==== qutebrowser/completion/completer.py

- Add a new _completion() getter (which ensures it actually gets the completion
  view) rather than accessing the .parent() directly (which could be any QObject).

==== qutebrowser/completion/completiondelegate.py

- Make sure self.parent() is a CompletionView (no helper method as there is only
  one instance).
- Remove a now-unneeded type ignore for adding QSizes.

==== qutebrowser/completion/completionwidget.py

- Add a ._model() getter which ensures that we get a CompletionModel (with
  custom attributes) rather than Qt's .model() which can be any QAbstractItemModel
  (or None).
- Removed a now-unneeded type ignore for OR-ing flags.

==== qutebrowser/completion/models/completionmodel.py

- Remove now unneeded type ignores for signals.
- Ignore a complaint about .set_pattern() not being defined. Completion
  categories don't share any common parent class, so it would be good to introduce
  a typing.Protocol for this. See #7098.

==== qutebrowser/components/misccommands.py

- Removed a now-unneeded type ignore for OR-ing flags.

==== qutebrowser/components/readlinecommands.py

- Make sure QApplication.instance() is a QApplication (and not just a
  QCoreApplication). This includes the former "not None" check.

==== qutebrowser/components/scrollcommands.py

- Add basic annotation for "funcs" dict. Could have a callable protocol to
  specify it needs a count kwarg, see #7098.

==== qutebrowser/config/stylesheet.py

- Correctly specify that stylesheet apply to QWidgets, not any QObject.
- Ignore an attr-defined for obj.STYLESHEET. Perhaps could somehow teach mypy
  about this with overloads and protocols (stylesheet for set_register being None
  => STYLESHEET needs to be defined, otherwise anything goes), but perhaps not
  worth the troble. See #7098.

==== qutebrowser/keyinput/keyutils.py

- Remove some now-unneeded type ignores and add a cast for using a single enum
  value as flags. Might need to look at this again with Qt 6 support.

==== qutebrowser/keyinput/modeman.py

- Add a FIXME for using a TypedDict, see comments for hints.py above.

==== qutebrowser/mainwindow/mainwindow.py

- Remove now-unneeded type ignores for calling with OR-ed flags.
- Improve where we cast from WindowType to WindowFlags, no int needed
- Use new .tab_bar() getter, see below.

==== qutebrowser/mainwindow/prompt.py

- Remove now-unneeded type ignores for calling with OR-ed flags.

==== qutebrowser/mainwindow/statusbar/bar.py

- Adjust type ignores around @pyqtProperty. The fact one is still needed seems
  like a stub regression.

==== qutebrowser/mainwindow/statusbar/command.py

- Fix type for setText() override (from QLineEdit): text can be None
  (QString in C++).

==== qutebrowser/mainwindow/statusbar/url.py

- Adjust type ignores around @pyqtProperty. The fact one is still needed seems
  like a stub regression.

==== qutebrowser/mainwindow/tabbedbrowser.py

- Specify that TabDeque manages browser tabs, not any QWidgets. It accesses
  AbstractTab-specific attributes.
- Make sure that the .tabBar() we get is a tabwidget.TabBar, as we access
  .maybe_hide.
- Fix the annotations for stored marks: Scroll positions are a QPoint, not int.
- Add _current_tab() and _tab_by_idx() wrappers for .currentWidget() and
  .widget(), which ensures that the return values are valid AbstractTabs (or None
  for _tab_by_idx). This is needed because we access AbstractTab-specific
  attributes.
- For some places, where the tab can be None, continue using .currentTab() but
  add asserts.
- Remove some now-unneeded [unreachable] ignores, as mypy knows about the None
  possibility now.

==== qutebrowser/mainwindow/tabwidget.py

- Add new tab_bar() and _tab_by_idx() helpers which check that the .tabBar() and
  .widget() are of type TabBar and AbstractTab, respectively.
- Add additional assertions where we expect ._tab_by_idx() to never be None.
- Remove dead code in get_tab_fields for handling a None y scroll position. I
  was unable to find any place in the code where this could be set to None.
- Remove some now-unneeded type ignores and casts, as mypy now knows that
  _type_by_idx() could be None.
- Work around a strange instance where mypy complains about not being able to
  find the type of TabBar.drag_in_progress from TabWidget._toggle_visibility,
  despite it clearly being shown as a bool *inside* that class without any
  annotation.
- Add a ._tab_widget() getter in TabBar which ensures that the .parent() is in
  fact a TabWidget.

==== qutebrowser/misc/crashsignal.py

- Remove now unneeded type ignores for signals.

==== qutebrowser/misc/editor.py

- Remove now unneeded type ignores for signals.

==== qutebrowser/misc/ipc.py

- Remove now unneeded type ignores for signals.
- Add new type ignores for .error() which is both a signal and a getter
  (stub regression?). Won't be relevant for Qt 6 anymore, as the signal was
  renamed to errorOccurred in 5.15.

==== qutebrowser/misc/objects.py

- Make sure mypy knows that objects.app is our custom Application (with custom
  attributes) rather than any QApplication.

==== qutebrowser/utils/objreg.py

- Ignore attr-defined for .win_id attributes. Maybe could add a typing.Protocol,
  but ideally, the whole objreg stuff should die one day anyways.

==== tests/unit/completion/test_completer.py

- Make CompletionWidgetStub inherit from CompletionView so that it passes the
  new isinstance() asserts in completer.py (see above).
This commit is contained in:
Florian Bruhin 2022-04-24 18:06:07 +02:00
parent d1c7f94826
commit a20bb67a87
50 changed files with 408 additions and 248 deletions

View File

@ -11,7 +11,7 @@ mypy==0.942
mypy-extensions==0.4.3
pluggy==1.0.0
Pygments==2.11.2
PyQt5-stubs==5.15.2.0
PyQt5-stubs==5.15.6.0
tomli==2.0.1
types-PyYAML==6.0.6
typing_extensions==4.2.0

View File

@ -562,8 +562,7 @@ class Application(QApplication):
log.init.debug("Initializing application...")
self.launch_time = datetime.datetime.now()
self.focusObjectChanged.connect( # type: ignore[attr-defined]
self.on_focus_object_changed)
self.focusObjectChanged.connect(self.on_focus_object_changed)
self.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
self.new_window.connect(self._on_new_window)

View File

@ -24,7 +24,7 @@ import itertools
import functools
import dataclasses
from typing import (cast, TYPE_CHECKING, Any, Callable, Iterable, List, Optional,
Sequence, Set, Type, Union)
Sequence, Set, Type, Union, Tuple)
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF, Qt,
QEvent, QPoint, QRect)
@ -35,12 +35,12 @@ from PyQt5.QtNetwork import QNetworkAccessManager
if TYPE_CHECKING:
from PyQt5.QtWebKit import QWebHistory, QWebHistoryItem
from PyQt5.QtWebKitWidgets import QWebPage
from PyQt5.QtWebKitWidgets import QWebPage, QWebView
from PyQt5.QtWebEngineWidgets import (
QWebEngineHistory, QWebEngineHistoryItem, QWebEnginePage)
QWebEngineHistory, QWebEngineHistoryItem, QWebEnginePage, QWebEngineView)
from qutebrowser.keyinput import modeman
from qutebrowser.config import config
from qutebrowser.config import config, websettings
from qutebrowser.utils import (utils, objreg, usertypes, log, qtutils,
urlutils, message, jinja)
from qutebrowser.misc import miscwidgets, objects, sessions
@ -53,6 +53,7 @@ if TYPE_CHECKING:
tab_id_gen = itertools.count(0)
_WidgetType = Union["QWebView", "QWebEngineView"]
def create(win_id: int,
@ -156,7 +157,7 @@ class AbstractAction:
action_base: Type[Union['QWebPage.WebAction', 'QWebEnginePage.WebAction']]
def __init__(self, tab: 'AbstractTab') -> None:
self._widget = cast(QWidget, None)
self._widget = cast(_WidgetType, None)
self._tab = tab
def exit_fullscreen(self) -> None:
@ -172,6 +173,7 @@ class AbstractAction:
member = getattr(self.action_class, name, None)
if not isinstance(member, self.action_base):
raise WebTabError("{} is not a valid web action!".format(name))
assert member is not None # for mypy
self._widget.triggerPageAction(member)
def show_source(self, pygments: bool = False) -> None:
@ -229,7 +231,7 @@ class AbstractPrinting:
"""Attribute ``printing`` of AbstractTab for printing the page."""
def __init__(self, tab: 'AbstractTab') -> None:
self._widget = cast(QWidget, None)
self._widget = cast(_WidgetType, None)
self._tab = tab
def check_pdf_support(self) -> None:
@ -308,7 +310,7 @@ class AbstractSearch(QObject):
def __init__(self, tab: 'AbstractTab', parent: QWidget = None):
super().__init__(parent)
self._tab = tab
self._widget = cast(QWidget, None)
self._widget = cast(_WidgetType, None)
self.text: Optional[str] = None
self.search_displayed = False
@ -372,7 +374,7 @@ class AbstractZoom(QObject):
def __init__(self, tab: 'AbstractTab', parent: QWidget = None) -> None:
super().__init__(parent)
self._tab = tab
self._widget = cast(QWidget, None)
self._widget = cast(_WidgetType, None)
# Whether zoom was changed from the default.
self._default_zoom_changed = False
self._init_neighborlist()
@ -466,7 +468,7 @@ class AbstractCaret(QObject):
mode_manager: modeman.ModeManager,
parent: QWidget = None) -> None:
super().__init__(parent)
self._widget = cast(QWidget, None)
self._widget = cast(_WidgetType, None)
self._mode_manager = mode_manager
mode_manager.entered.connect(self._on_mode_entered)
mode_manager.left.connect(self._on_mode_left)
@ -559,7 +561,7 @@ class AbstractScroller(QObject):
def __init__(self, tab: 'AbstractTab', parent: QWidget = None):
super().__init__(parent)
self._tab = tab
self._widget = cast(QWidget, None)
self._widget = cast(_WidgetType, None)
if 'log-scroll-pos' in objects.debug_flags:
self.perc_changed.connect(self._log_scroll_pos_change)
@ -568,16 +570,16 @@ class AbstractScroller(QObject):
log.webview.vdebug( # type: ignore[attr-defined]
"Scroll position changed to {}".format(self.pos_px()))
def _init_widget(self, widget: QWidget) -> None:
def _init_widget(self, widget: _WidgetType) -> None:
self._widget = widget
def pos_px(self) -> int:
def pos_px(self) -> QPoint:
raise NotImplementedError
def pos_perc(self) -> int:
def pos_perc(self) -> Tuple[int, int]:
raise NotImplementedError
def to_perc(self, x: int = None, y: int = None) -> None:
def to_perc(self, x: float = None, y: float = None) -> None:
raise NotImplementedError
def to_point(self, point: QPoint) -> None:
@ -627,6 +629,8 @@ class AbstractHistoryPrivate:
"""Private API related to the history."""
_history: Union["QWebHistory", "QWebEngineHistory"]
def serialize(self) -> bytes:
"""Serialize into an opaque format understood by self.deserialize."""
raise NotImplementedError
@ -711,7 +715,7 @@ class AbstractElements:
_ErrorCallback = Callable[[Exception], None]
def __init__(self, tab: 'AbstractTab') -> None:
self._widget = cast(QWidget, None)
self._widget = cast(_WidgetType, None)
self._tab = tab
def find_css(self, selector: str,
@ -772,7 +776,7 @@ class AbstractAudio(QObject):
def __init__(self, tab: 'AbstractTab', parent: QWidget = None) -> None:
super().__init__(parent)
self._widget = cast(QWidget, None)
self._widget = cast(_WidgetType, None)
self._tab = tab
def set_muted(self, muted: bool, override: bool = False) -> None:
@ -804,11 +808,11 @@ class AbstractTabPrivate:
def __init__(self, mode_manager: modeman.ModeManager,
tab: 'AbstractTab') -> None:
self._widget = cast(QWidget, None)
self._widget = cast(_WidgetType, None)
self._tab = tab
self._mode_manager = mode_manager
def event_target(self) -> QWidget:
def event_target(self) -> Optional[QWidget]:
"""Return the widget events should be sent to."""
raise NotImplementedError
@ -848,7 +852,7 @@ class AbstractTabPrivate:
def shutdown(self) -> None:
raise NotImplementedError
def run_js_sync(self, code: str) -> None:
def run_js_sync(self, code: str) -> Any:
"""Run javascript sync.
Result will be returned when running JS is complete.
@ -867,7 +871,7 @@ class AbstractTabPrivate:
self._tab.data.inspector = None
self.toggle_inspector(inspector.Position.window)
def toggle_inspector(self, position: inspector.Position) -> None:
def toggle_inspector(self, position: Optional[inspector.Position]) -> None:
"""Show/hide (and if needed, create) the web inspector for this tab."""
tabdata = self._tab.data
if tabdata.inspector is None:
@ -944,6 +948,19 @@ class AbstractTab(QWidget):
# for a given hostname anyways.
_insecure_hosts: Set[str] = set()
# Sub-APIs initialized by subclasses
history: AbstractHistory
scroller: AbstractScroller
caret: AbstractCaret
zoom: AbstractZoom
search: AbstractSearch
printing: AbstractPrinting
action: AbstractAction
elements: AbstractElements
audio: AbstractAudio
private_api: AbstractTabPrivate
settings: websettings.AbstractSettings
def __init__(self, *, win_id: int,
mode_manager: 'modeman.ModeManager',
private: bool,
@ -962,7 +979,7 @@ class AbstractTab(QWidget):
self.data = TabData()
self._layout = miscwidgets.WrapperLayout(self)
self._widget = cast(QWidget, None)
self._widget = cast(_WidgetType, None)
self._progress = 0
self._load_status = usertypes.LoadStatus.none
self._tab_event_filter = eventfilter.TabEventFilter(
@ -976,7 +993,7 @@ class AbstractTab(QWidget):
self.before_load_started.connect(self._on_before_load_started)
def _set_widget(self, widget: QWidget) -> None:
def _set_widget(self, widget: Union["QWebView", "QWebEngineView"]) -> None:
# pylint: disable=protected-access
self._widget = widget
self.data.splitter = miscwidgets.InspectorSplitter(
@ -1195,7 +1212,7 @@ class AbstractTab(QWidget):
def title(self) -> str:
raise NotImplementedError
def icon(self) -> None:
def icon(self) -> QIcon:
raise NotImplementedError
def set_html(self, html: str, base_url: QUrl = QUrl()) -> None:

View File

@ -913,6 +913,7 @@ class CommandDispatcher:
# Not sure how you enter a command without an active window...
raise cmdutils.CommandError(
"No window specified and couldn't find active window!")
assert isinstance(active_win, mainwindow.MainWindow), active_win
win_id = active_win.win_id
if win_id not in objreg.window_registry:

View File

@ -86,6 +86,15 @@ class DownloadView(QListView):
count = model.rowCount()
return utils.get_repr(self, count=count)
def _model(self) -> downloads.DownloadModel:
"""Get the current download model.
Ensures the model is not None.
"""
model = self.model()
assert isinstance(model, downloads.DownloadModel), model
return model
@pyqtSlot()
def _update_geometry(self):
"""Wrapper to call updateGeometry.
@ -112,7 +121,7 @@ class DownloadView(QListView):
"""
if not index.isValid():
return
item = self.model().data(index, downloads.ModelRole.item)
item = self._model().data(index, downloads.ModelRole.item)
if item.done and item.successful:
item.open_file()
item.remove()
@ -126,7 +135,7 @@ class DownloadView(QListView):
Args:
item: The DownloadItem to get the actions for, or None.
"""
model = self.model()
model = self._model()
actions: _ActionListType = []
if item is None:
pass
@ -154,7 +163,7 @@ class DownloadView(QListView):
"""Show the context menu."""
index = self.indexAt(point)
if index.isValid():
item = self.model().data(index, downloads.ModelRole.item)
item = self._model().data(index, downloads.ModelRole.item)
else:
item = None
self._menu = QMenu(self)
@ -176,7 +185,7 @@ class DownloadView(QListView):
def sizeHint(self):
"""Return sizeHint based on the view contents."""
idx = self.model().last_index()
idx = self._model().last_index()
bottom = self.visualRect(idx).bottom()
if bottom != -1:
margins = self.contentsMargins()

View File

@ -380,7 +380,7 @@ class GreasemonkeyManager(QObject):
"""
# See if we are still waiting on any required scripts for this one
for dl in self._in_progress_dls:
if dl.requested_url in script.requires:
if dl.requested_url in script.requires: # type: ignore[attr-defined]
return False
# Need to add the required scripts to the IIFE now
@ -414,6 +414,7 @@ class GreasemonkeyManager(QObject):
force_overwrite=True)
download = download_manager.get(QUrl(url), target=target,
auto_remove=True)
# FIXME:mypy Build this into downloads instead of patching here?
download.requested_url = url
self._in_progress_dls.append(download)
if download.successful:

View File

@ -254,8 +254,8 @@ class HintActions:
flags = QUrl.FullyEncoded | QUrl.RemovePassword
if url.scheme() == 'mailto':
flags |= QUrl.RemoveScheme
urlstr = url.toString(flags) # type: ignore[arg-type]
flags |= QUrl.RemoveScheme # type: ignore[operator]
urlstr = url.toString(flags)
new_content = urlstr
@ -356,8 +356,7 @@ class HintActions:
url: The URL to open as a QUrl.
context: The HintContext to use.
"""
urlstr = url.toString(
QUrl.FullyEncoded | QUrl.RemovePassword) # type: ignore[arg-type]
urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
args = context.get_args(urlstr)
commandrunner = runners.CommandRunner(self._win_id)
commandrunner.run_safely('spawn ' + ' '.join(args))
@ -657,6 +656,7 @@ class HintManager(QObject):
self._context.labels[string] = label
keyparser = self._get_keyparser(usertypes.KeyMode.hint)
assert isinstance(keyparser, modeparsers.HintKeyParser), keyparser
keyparser.update_bindings(strings)
modeman.enter(self._win_id, usertypes.KeyMode.hint,
@ -852,6 +852,7 @@ class HintManager(QObject):
# apply auto_follow_timeout
timeout = config.val.hints.auto_follow_timeout
normal_parser = self._get_keyparser(usertypes.KeyMode.normal)
assert isinstance(normal_parser, modeparsers.NormalKeyParser), normal_parser
normal_parser.set_inhibited_timeout(timeout)
# unpacking gets us the first (and only) key in the dict.
self._fire(*visible)
@ -927,6 +928,7 @@ class HintManager(QObject):
self._context.labels[string] = label
keyparser = self._get_keyparser(usertypes.KeyMode.hint)
assert isinstance(keyparser, modeparsers.HintKeyParser), keyparser
keyparser.update_bindings(strings, preserve_filter=True)
# Note: filter_hints can be called with non-None filterstr only

View File

@ -22,7 +22,7 @@
import base64
import binascii
import enum
from typing import cast, Optional
from typing import cast, Optional, Union, TYPE_CHECKING
from PyQt5.QtWidgets import QWidget
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QEvent
@ -34,6 +34,14 @@ from qutebrowser.utils import log, usertypes
from qutebrowser.keyinput import modeman
from qutebrowser.misc import miscwidgets
if TYPE_CHECKING:
from PyQt5.QtWebKitWidgets import QWebInspector, QWebPage
from PyQt5.QtWebEngineWidgets import QWebEnginePage
from qutebrowser.browser.webengine import webengineinspector
_WidgetType = Union["QWebInspector", "webengineinspector.WebEngineInspectorView"]
class Position(enum.Enum):
@ -93,7 +101,7 @@ class AbstractWebInspector(QWidget):
win_id: int,
parent: QWidget = None) -> None:
super().__init__(parent)
self._widget = cast(QWidget, None)
self._widget = cast(_WidgetType, None)
self._layout = miscwidgets.WrapperLayout(self)
self._splitter = splitter
self._position: Optional[Position] = None
@ -105,7 +113,7 @@ class AbstractWebInspector(QWidget):
eventfilter=self._event_filter,
parent=self)
def _set_widget(self, widget: QWidget) -> None:
def _set_widget(self, widget: _WidgetType) -> None:
self._widget = widget
self._widget.setWindowTitle("Web Inspector")
self._widget.installEventFilter(self._child_event_filter)
@ -198,7 +206,7 @@ class AbstractWebInspector(QWidget):
geom = base64.b64encode(data).decode('ASCII')
configfiles.state['inspector']['window'] = geom
def inspect(self, page: QWidget) -> None:
def inspect(self, page: Union["QWebPage", "QWebEnginePage"]) -> None:
"""Inspect the given QWeb(Engine)Page."""
raise NotImplementedError

View File

@ -270,8 +270,7 @@ class PACFetcher(QObject):
"""Fetch the proxy from the remote URL."""
assert self._manager is not None
self._reply = self._manager.get(QNetworkRequest(self._pac_url))
self._reply.finished.connect( # type: ignore[attr-defined]
self._finish)
self._reply.finished.connect(self._finish)
@pyqtSlot()
def _finish(self):

View File

@ -601,6 +601,7 @@ class DownloadManager(downloads.AbstractDownloadManager):
"""
assert nam.adopted_downloads == 0
for download in self.downloads:
assert isinstance(download, DownloadItem), download
if download._uses_nam(nam): # pylint: disable=protected-access
nam.adopt_download(download)
return nam.adopted_downloads

View File

@ -128,9 +128,7 @@ def data_for_url(url: QUrl) -> Tuple[str, bytes]:
Return:
A (mimetype, data) tuple.
"""
norm_url = url.adjusted(
QUrl.NormalizePathSegments | # type: ignore[arg-type]
QUrl.StripTrailingSlash)
norm_url = url.adjusted(QUrl.NormalizePathSegments | QUrl.StripTrailingSlash)
if norm_url != url:
raise Redirect(norm_url)

View File

@ -74,6 +74,8 @@ class UrlMarkManager(QObject):
changed = pyqtSignal()
_lineparser: lineparser.LineParser
def __init__(self, parent=None):
"""Initialize and read quickmarks."""
super().__init__(parent)

View File

@ -19,7 +19,7 @@
"""Generic web element related code."""
from typing import cast, TYPE_CHECKING, Iterator, Optional, Set, Union
from typing import cast, TYPE_CHECKING, Iterator, Optional, Set, Union, Dict
import collections.abc
from PyQt5.QtCore import QUrl, Qt, QEvent, QTimer, QRect, QPoint
@ -345,18 +345,19 @@ class AbstractWebElement(collections.abc.MutableMapping): # type: ignore[type-a
log.webelem.debug("Sending fake click to {!r} at position {} with "
"target {}".format(self, pos, click_target))
target_modifiers = {
usertypes.ClickTarget.normal: Qt.NoModifier,
target_modifiers: Dict[usertypes.ClickTarget, Qt.KeyboardModifiers] = {
usertypes.ClickTarget.normal: cast(Qt.KeyboardModifiers, Qt.NoModifier),
usertypes.ClickTarget.window: Qt.AltModifier | Qt.ShiftModifier,
usertypes.ClickTarget.tab: Qt.ControlModifier,
usertypes.ClickTarget.tab_bg: Qt.ControlModifier,
usertypes.ClickTarget.tab: cast(Qt.KeyboardModifiers, Qt.ControlModifier),
usertypes.ClickTarget.tab_bg:
cast(Qt.KeyboardModifiers, Qt.ControlModifier),
}
if config.val.tabs.background:
target_modifiers[usertypes.ClickTarget.tab] |= Qt.ShiftModifier
else:
target_modifiers[usertypes.ClickTarget.tab_bg] |= Qt.ShiftModifier
modifiers = cast(Qt.KeyboardModifiers, target_modifiers[click_target])
modifiers = target_modifiers[click_target]
events = [
QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton, Qt.NoModifier),

View File

@ -288,7 +288,7 @@ class NotificationBridgePresenter(QObject):
qt_notification.show()
self._active_notifications[notification_id] = qt_notification
qt_notification.closed.connect( # type: ignore[attr-defined]
qt_notification.closed.connect(
functools.partial(self._adapter.on_web_closed, notification_id))
def _find_replaces_id(
@ -632,6 +632,7 @@ class HerbeNotificationAdapter(AbstractNotificationAdapter):
self.close_id.emit(pid)
else:
proc = self.sender()
assert isinstance(proc, QProcess), proc
stderr = proc.readAllStandardError()
raise Error(f'herbe exited with status {code}: {stderr}')
@ -757,8 +758,7 @@ class DBusNotificationAdapter(AbstractNotificationAdapter):
QDBusServiceWatcher.WatchForUnregistration,
self,
)
self._watcher.serviceUnregistered.connect( # type: ignore[attr-defined]
self._on_service_unregistered)
self._watcher.serviceUnregistered.connect(self._on_service_unregistered)
test_service = 'test-notification-service' in objects.debug_flags
service = self.TEST_SERVICE if test_service else self.SERVICE

View File

@ -44,10 +44,8 @@ class DownloadItem(downloads.AbstractDownloadItem):
parent: QObject = None) -> None:
super().__init__(manager=manager, parent=manager)
self._qt_item = qt_item
qt_item.downloadProgress.connect( # type: ignore[attr-defined]
self.stats.on_download_progress)
qt_item.stateChanged.connect( # type: ignore[attr-defined]
self._on_state_changed)
qt_item.downloadProgress.connect(self.stats.on_download_progress)
qt_item.stateChanged.connect(self._on_state_changed)
# Ensure wrapped qt_item is deleted manually when the wrapper object
# is deleted. See https://github.com/qutebrowser/qutebrowser/issues/3373
@ -92,7 +90,7 @@ class DownloadItem(downloads.AbstractDownloadItem):
def _do_die(self):
progress_signal = self._qt_item.downloadProgress
progress_signal.disconnect() # type: ignore[attr-defined]
progress_signal.disconnect()
if self._qt_item.state() != QWebEngineDownloadItem.DownloadInterrupted:
self._qt_item.cancel()

View File

@ -37,6 +37,8 @@ class WebEngineElement(webelem.AbstractWebElement):
"""A web element for QtWebEngine, using JS under the hood."""
_tab: "webenginetab.WebEngineTab"
def __init__(self, js_dict: Dict[str, Any],
tab: 'webenginetab.WebEngineTab') -> None:
super().__init__(tab)
@ -248,7 +250,7 @@ class WebEngineElement(webelem.AbstractWebElement):
# (it does so with a 0ms QTimer...)
# This is also used in Qt's tests:
# https://github.com/qt/qtwebengine/commit/5e572e88efa7ba7c2b9138ec19e606d3e345ac90
QApplication.processEvents( # type: ignore[call-overload]
QApplication.processEvents(
QEventLoop.ExcludeSocketNotifiers |
QEventLoop.ExcludeUserInputEvents)

View File

@ -49,13 +49,17 @@ class WebEngineInspectorView(QWebEngineView):
See WebEngineView.createWindow for details.
"""
return self.page().inspectedPage().view().createWindow(wintype)
view = self.page().inspectedPage().view()
assert isinstance(view, QWebEngineView), view
return view.createWindow(wintype)
class WebEngineInspector(inspector.AbstractWebInspector):
"""A web inspector for QtWebEngine with Qt API support."""
_widget: WebEngineInspectorView
def __init__(self, splitter: miscwidgets.InspectorSplitter,
win_id: int,
parent: QWidget = None) -> None:
@ -66,8 +70,7 @@ class WebEngineInspector(inspector.AbstractWebInspector):
self._settings = webenginesettings.WebEngineSettings(view.settings())
self._set_widget(view)
page = view.page()
page.windowCloseRequested.connect( # type: ignore[attr-defined]
self._on_window_close_requested)
page.windowCloseRequested.connect(self._on_window_close_requested)
def _on_window_close_requested(self) -> None:
"""Called when the 'x' was clicked in the devtools."""
@ -96,7 +99,7 @@ class WebEngineInspector(inspector.AbstractWebInspector):
"please install the qt5-qtwebengine-devtools "
"Fedora package.")
def inspect(self, page: QWebEnginePage) -> None: # type: ignore[override]
def inspect(self, page: QWebEnginePage) -> None:
inspector_page = self._widget.page()
inspector_page.setInspectedPage(page)
self._settings.update_for_url(inspector_page.requestedUrl())

View File

@ -138,6 +138,6 @@ def init():
assert not QWebEngineUrlScheme.schemeByName(b'qute').name()
scheme = QWebEngineUrlScheme(b'qute')
scheme.setFlags(
QWebEngineUrlScheme.LocalScheme | # type: ignore[arg-type]
QWebEngineUrlScheme.LocalScheme |
QWebEngineUrlScheme.LocalAccessAllowed)
QWebEngineUrlScheme.registerScheme(scheme)

View File

@ -332,9 +332,9 @@ class ProfileSetter:
def _update_settings(option):
"""Update global settings when qwebsettings changed."""
_global_settings.update_setting(option)
default_profile.setter.update_setting(option)
default_profile.setter.update_setting(option) # type: ignore[attr-defined]
if private_profile:
private_profile.setter.update_setting(option)
private_profile.setter.update_setting(option) # type: ignore[attr-defined]
def _init_user_agent_str(ua):
@ -352,8 +352,9 @@ def _init_profile(profile: QWebEngineProfile) -> None:
This currently only contains the steps which are shared between a private and a
non-private profile (at the moment, only the default profile).
"""
# FIXME:mypy subclass QWebEngineProfile instead?
profile.setter = ProfileSetter(profile) # type: ignore[attr-defined]
profile.setter.init_profile()
profile.setter.init_profile() # type: ignore[attr-defined]
_qute_scheme_handler.install(profile)
_req_interceptor.install(profile)

View File

@ -29,8 +29,8 @@ from typing import cast, Union, Optional
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QTimer, QUrl,
QObject)
from PyQt5.QtNetwork import QAuthenticator
from PyQt5.QtWidgets import QWidget
from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript, QWebEngineHistory
from PyQt5.QtWebEngineWidgets import (QWebEnginePage, QWebEngineView, QWebEngineScript,
QWebEngineHistory)
from qutebrowser.config import config
from qutebrowser.browser import browsertab, eventfilter, shared, webelem, greasemonkey
@ -57,6 +57,8 @@ class WebEngineAction(browsertab.AbstractAction):
"""QtWebEngine implementations related to web actions."""
_widget: webview.WebEngineView
action_class = QWebEnginePage
action_base = QWebEnginePage.WebAction
@ -79,6 +81,8 @@ class WebEnginePrinting(browsertab.AbstractPrinting):
"""QtWebEngine implementations related to printing."""
_widget: webview.WebEngineView
def check_pdf_support(self):
pass
@ -191,6 +195,8 @@ class WebEngineSearch(browsertab.AbstractSearch):
back yet.
"""
_widget: webview.WebEngineView
def __init__(self, tab, parent=None):
super().__init__(tab, parent)
self._flags = self._empty_flags()
@ -199,7 +205,7 @@ class WebEngineSearch(browsertab.AbstractSearch):
self._wrap_handler = _WebEngineSearchWrapHandler()
def _empty_flags(self):
return QWebEnginePage.FindFlags(0) # type: ignore[call-overload]
return QWebEnginePage.FindFlags(0)
def _args_to_flags(self, reverse, ignore_case):
flags = self._empty_flags()
@ -275,8 +281,7 @@ class WebEngineSearch(browsertab.AbstractSearch):
def prev_result(self, *, result_cb=None):
# The int() here makes sure we get a copy of the flags.
flags = QWebEnginePage.FindFlags(
int(self._flags)) # type: ignore[call-overload]
flags = QWebEnginePage.FindFlags(int(self._flags))
if flags & QWebEnginePage.FindBackward:
if self._wrap_handler.prevent_wrapping(going_up=False):
return
@ -493,6 +498,8 @@ class WebEngineScroller(browsertab.AbstractScroller):
"""QtWebEngine implementations related to scrolling."""
_widget: webview.WebEngineView
def __init__(self, tab, parent=None):
super().__init__(tab, parent)
self._pos_perc = (0, 0)
@ -713,6 +720,8 @@ class WebEngineZoom(browsertab.AbstractZoom):
"""QtWebEngine implementations related to zooming."""
_widget: webview.WebEngineView
def _set_factor_internal(self, factor):
self._widget.setZoomFactor(factor)
@ -799,6 +808,8 @@ class WebEngineAudio(browsertab.AbstractAudio):
If that's the case, we leave it alone.
"""
_widget: webview.WebEngineView
def __init__(self, tab, parent=None):
super().__init__(tab, parent)
self._overridden = False
@ -870,6 +881,8 @@ class _WebEnginePermissions(QObject):
"""Handling of various permission-related signals."""
_widget: webview.WebEngineView
# Using 0 as WORKAROUND for:
# https://www.riverbankcomputing.com/pipermail/pyqt/2019-July/041903.html
@ -898,7 +911,7 @@ class _WebEnginePermissions(QObject):
def __init__(self, tab, parent=None):
super().__init__(parent)
self._tab = tab
self._widget = cast(QWidget, None)
self._widget = cast(webview.WebEngineView, None)
assert self._options.keys() == self._messages.keys()
def connect_signals(self):
@ -1033,10 +1046,12 @@ class _Quirk:
class _WebEngineScripts(QObject):
_widget: webview.WebEngineView
def __init__(self, tab, parent=None):
super().__init__(parent)
self._tab = tab
self._widget = cast(QWidget, None)
self._widget = cast(webview.WebEngineView, None)
self._greasemonkey = greasemonkey.gm_manager
def connect_signals(self):
@ -1240,6 +1255,8 @@ class WebEngineTabPrivate(browsertab.AbstractTabPrivate):
"""QtWebEngine-related methods which aren't part of the public API."""
_widget: webview.WebEngineView
def networkaccessmanager(self):
return None
@ -1275,6 +1292,10 @@ class WebEngineTab(browsertab.AbstractTab):
abort_questions = pyqtSignal()
_widget: QWebEngineView
search: WebEngineSearch
audio: WebEngineAudio
def __init__(self, *, win_id, mode_manager, private, parent=None):
super().__init__(win_id=win_id,
mode_manager=mode_manager,
@ -1685,6 +1706,7 @@ class WebEngineTab(browsertab.AbstractTab):
def _connect_signals(self):
view = self._widget
page = view.page()
assert isinstance(page, webview.WebEnginePage), page
page.windowCloseRequested.connect(self.window_close_requested)
page.linkHovered.connect(self.link_hovered)

View File

@ -70,7 +70,10 @@ class WebEngineView(QWebEngineView):
return self.focusProxy()
def shutdown(self):
self.page().shutdown()
"""Shut down the underlying page."""
page = self.page()
assert isinstance(page, WebEnginePage), page
page.shutdown()
def createWindow(self, wintype):
"""Called by Qt when a page wants to create a new window.

View File

@ -59,15 +59,9 @@ class FixedDataNetworkReply(QNetworkReply):
# For some reason, a segfault will be triggered if these lambdas aren't
# there.
# pylint: disable=unnecessary-lambda
QTimer.singleShot(
0,
lambda: self.metaDataChanged.emit()) # type: ignore[attr-defined]
QTimer.singleShot(
0,
lambda: self.readyRead.emit()) # type: ignore[attr-defined]
QTimer.singleShot(
0,
lambda: self.finished.emit()) # type: ignore[attr-defined]
QTimer.singleShot(0, lambda: self.metaDataChanged.emit())
QTimer.singleShot(0, lambda: self.readyRead.emit())
QTimer.singleShot(0, lambda: self.finished.emit())
@pyqtSlot()
def abort(self):
@ -122,10 +116,8 @@ class ErrorNetworkReply(QNetworkReply):
# the device to avoid getting a warning.
self.setOpenMode(QIODevice.ReadOnly)
self.setError(error, errorstring)
QTimer.singleShot(0, lambda:
self.error.emit(error)) # type: ignore[attr-defined]
QTimer.singleShot(0, lambda:
self.finished.emit()) # type: ignore[attr-defined]
QTimer.singleShot(0, lambda: self.error.emit(error))
QTimer.singleShot(0, lambda: self.finished.emit())
def abort(self):
"""Do nothing since it's a fake reply."""
@ -152,8 +144,7 @@ class RedirectNetworkReply(QNetworkReply):
def __init__(self, new_url, parent=None):
super().__init__(parent)
self.setAttribute(QNetworkRequest.RedirectionTargetAttribute, new_url)
QTimer.singleShot(0, lambda:
self.finished.emit()) # type: ignore[attr-defined]
QTimer.singleShot(0, lambda: self.finished.emit())
def abort(self):
"""Called when there's e.g. a redirection limit."""

View File

@ -31,6 +31,8 @@ class WebKitInspector(inspector.AbstractWebInspector):
"""A web inspector for QtWebKit."""
_widget = QWebInspector
def __init__(self, splitter: miscwidgets.InspectorSplitter,
win_id: int,
parent: QWidget = None) -> None:
@ -38,7 +40,7 @@ class WebKitInspector(inspector.AbstractWebInspector):
qwebinspector = QWebInspector()
self._set_widget(qwebinspector)
def inspect(self, page: QWebPage) -> None: # type: ignore[override]
def inspect(self, page: QWebPage) -> None:
settings = QWebSettings.globalSettings()
settings.setAttribute(QWebSettings.DeveloperExtrasEnabled, True)
self._widget.setPage(page)

View File

@ -32,8 +32,9 @@ from PyQt5.QtWebKit import QWebSettings, QWebHistory, QWebElement
from PyQt5.QtPrintSupport import QPrinter
from qutebrowser.browser import browsertab, shared
from qutebrowser.browser.webkit import (webview, tabhistory, webkitelem,
from qutebrowser.browser.webkit import (webview, webpage, tabhistory, webkitelem,
webkitsettings, webkitinspector)
from qutebrowser.browser.webkit.network import networkmanager
from qutebrowser.utils import qtutils, usertypes, utils, log, debug, resources
from qutebrowser.keyinput import modeman
from qutebrowser.qt import sip
@ -46,6 +47,8 @@ class WebKitAction(browsertab.AbstractAction):
action_class = QWebPage
action_base = QWebPage.WebAction
_widget: webview.WebView
def exit_fullscreen(self):
raise browsertab.UnsupportedOperationError
@ -69,7 +72,7 @@ class WebKitAction(browsertab.AbstractAction):
'Unselect': QWebPage.ToggleVideoFullscreen + 2,
}
if name in new_actions:
self._widget.triggerPageAction(new_actions[name])
self._widget.triggerPageAction(new_actions[name]) # type: ignore[arg-type]
return
super().run_string(name)
@ -79,6 +82,8 @@ class WebKitPrinting(browsertab.AbstractPrinting):
"""QtWebKit implementations related to printing."""
_widget: webview.WebView
def check_pdf_support(self):
pass
@ -101,6 +106,8 @@ class WebKitSearch(browsertab.AbstractSearch):
"""QtWebKit implementations related to searching on the page."""
_widget: webview.WebView
def __init__(self, tab, parent=None):
super().__init__(tab, parent)
self._flags = self._empty_flags()
@ -153,7 +160,8 @@ class WebKitSearch(browsertab.AbstractSearch):
self.search_displayed = False
# We first clear the marked text, then the highlights
self._widget.findText('')
self._widget.findText('', QWebPage.HighlightAllOccurrences)
self._widget.findText(
'', QWebPage.HighlightAllOccurrences) # type: ignore[arg-type]
def search(self, text, *, ignore_case=usertypes.IgnoreCase.never,
reverse=False, wrap=True, result_cb=None):
@ -179,7 +187,7 @@ class WebKitSearch(browsertab.AbstractSearch):
def next_result(self, *, result_cb=None):
self.search_displayed = True
found = self._widget.findText(self.text, self._flags)
found = self._widget.findText(self.text, self._flags) # type: ignore[arg-type]
self._call_cb(result_cb, found, self.text, self._flags, 'next_result')
def prev_result(self, *, result_cb=None):
@ -191,7 +199,7 @@ class WebKitSearch(browsertab.AbstractSearch):
flags &= ~QWebPage.FindBackward
else:
flags |= QWebPage.FindBackward
found = self._widget.findText(self.text, flags)
found = self._widget.findText(self.text, flags) # type: ignore[arg-type]
self._call_cb(result_cb, found, self.text, flags, 'prev_result')
@ -199,6 +207,8 @@ class WebKitCaret(browsertab.AbstractCaret):
"""QtWebKit implementations related to moving the cursor/selection."""
_widget: webview.WebView
def __init__(self,
tab: 'WebKitTab',
mode_manager: modeman.ModeManager,
@ -515,6 +525,8 @@ class WebKitZoom(browsertab.AbstractZoom):
"""QtWebKit implementations related to zooming."""
_widget: webview.WebView
def _set_factor_internal(self, factor):
self._widget.setZoomFactor(factor)
@ -525,6 +537,8 @@ class WebKitScroller(browsertab.AbstractScroller):
# FIXME:qtwebengine When to use the main frame, when the current one?
_widget: webview.WebView
def pos_px(self):
return self._widget.page().mainFrame().scrollPosition()
@ -624,6 +638,8 @@ class WebKitHistoryPrivate(browsertab.AbstractHistoryPrivate):
"""History-related methods which are not part of the extension API."""
_history: QWebHistory
def __init__(self, tab: 'WebKitTab') -> None:
self._tab = tab
self._history = cast(QWebHistory, None)
@ -695,6 +711,7 @@ class WebKitElements(browsertab.AbstractElements):
"""QtWebKit implementations related to elements on the page."""
_tab: 'WebKitTab'
_widget: webview.WebView
def find_css(self, selector, callback, error_cb, *, only_visible=False):
utils.unused(error_cb)
@ -730,7 +747,7 @@ class WebKitElements(browsertab.AbstractElements):
self.find_css('#' + elem_id, find_id_cb, error_cb=lambda exc: None)
def find_focused(self, callback):
frame = self._widget.page().currentFrame()
frame = cast(Optional[QWebFrame], self._widget.page().currentFrame())
if frame is None:
callback(None)
return
@ -744,7 +761,7 @@ class WebKitElements(browsertab.AbstractElements):
def find_at_pos(self, pos, callback):
assert pos.x() >= 0
assert pos.y() >= 0
frame = self._widget.page().frameAt(pos)
frame = cast(Optional[QWebFrame], self._widget.page().frameAt(pos))
if frame is None:
# This happens when we click inside the webview, but not actually
# on the QWebPage - for example when clicking the scrollbar
@ -796,6 +813,8 @@ class WebKitTabPrivate(browsertab.AbstractTabPrivate):
"""QtWebKit-related methods which aren't part of the public API."""
_widget: webview.WebView
def networkaccessmanager(self):
return self._widget.page().networkAccessManager()
@ -821,6 +840,8 @@ class WebKitTab(browsertab.AbstractTab):
"""A QtWebKit tab in the browser."""
_widget: webview.WebView
def __init__(self, *, win_id, mode_manager, private, parent=None):
super().__init__(win_id=win_id,
mode_manager=mode_manager,
@ -912,6 +933,7 @@ class WebKitTab(browsertab.AbstractTab):
def _on_load_started(self):
super()._on_load_started()
nam = self._widget.page().networkAccessManager()
assert isinstance(nam, networkmanager.NetworkManager), nam
nam.netrc_used = False
# Make sure the icon is cleared when navigating to a page without one.
self.icon_changed.emit(QIcon())
@ -929,7 +951,9 @@ class WebKitTab(browsertab.AbstractTab):
when using error pages... See
https://github.com/qutebrowser/qutebrowser/issues/84
"""
self._on_load_finished(not self._widget.page().error_occurred)
page = self._widget.page()
assert isinstance(page, webpage.BrowserPage), page
self._on_load_finished(not page.error_occurred)
@pyqtSlot()
def _on_webkit_icon_changed(self):
@ -984,18 +1008,30 @@ class WebKitTab(browsertab.AbstractTab):
view = self._widget
page = view.page()
frame = page.mainFrame()
page.windowCloseRequested.connect(self.window_close_requested)
page.linkHovered.connect(self.link_hovered)
page.loadProgress.connect(self._on_load_progress)
frame.loadStarted.connect(self._on_load_started)
page.windowCloseRequested.connect( # type: ignore[attr-defined]
self.window_close_requested)
page.linkHovered.connect( # type: ignore[attr-defined]
self.link_hovered)
page.loadProgress.connect( # type: ignore[attr-defined]
self._on_load_progress)
frame.loadStarted.connect( # type: ignore[attr-defined]
self._on_load_started)
view.scroll_pos_changed.connect(self.scroller.perc_changed)
view.titleChanged.connect(self.title_changed)
view.urlChanged.connect(self._on_url_changed)
view.titleChanged.connect( # type: ignore[attr-defined]
self.title_changed)
view.urlChanged.connect( # type: ignore[attr-defined]
self._on_url_changed)
view.shutting_down.connect(self.shutting_down)
page.networkAccessManager().sslErrors.connect(self._on_ssl_errors)
frame.loadFinished.connect(self._on_frame_load_finished)
view.iconChanged.connect(self._on_webkit_icon_changed)
page.frameCreated.connect(self._on_frame_created)
frame.contentsSizeChanged.connect(self._on_contents_size_changed)
frame.initialLayoutCompleted.connect(self._on_history_trigger)
page.navigation_request.connect(self._on_navigation_request)
frame.loadFinished.connect( # type: ignore[attr-defined]
self._on_frame_load_finished)
view.iconChanged.connect( # type: ignore[attr-defined]
self._on_webkit_icon_changed)
page.frameCreated.connect( # type: ignore[attr-defined]
self._on_frame_created)
frame.contentsSizeChanged.connect( # type: ignore[attr-defined]
self._on_contents_size_changed)
frame.initialLayoutCompleted.connect( # type: ignore[attr-defined]
self._on_history_trigger)
page.navigation_request.connect( # type: ignore[attr-defined]
self._on_navigation_request)

View File

@ -21,7 +21,6 @@
import html
import functools
from typing import cast
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QUrl, QPoint
from PyQt5.QtGui import QDesktopServices
@ -241,6 +240,7 @@ class BrowserPage(QWebPage):
if download_manager.has_downloads_with_nam(nam):
nam.setParent(download_manager)
else:
assert isinstance(nam, networkmanager.NetworkManager), nam
nam.shutdown()
def display_content(self, reply, mimetype):
@ -371,11 +371,10 @@ class BrowserPage(QWebPage):
self.setFeaturePermission, frame, feature,
QWebPage.PermissionDeniedByUser)
url = frame.url().adjusted(cast(QUrl.FormattingOptions,
QUrl.RemoveUserInfo |
QUrl.RemovePath |
QUrl.RemoveQuery |
QUrl.RemoveFragment))
url = frame.url().adjusted(QUrl.RemoveUserInfo | # type: ignore[operator]
QUrl.RemovePath |
QUrl.RemoveQuery |
QUrl.RemoveFragment)
question = shared.feature_permission(
url=url,
option=options[feature], msg=messages[feature],

View File

@ -109,7 +109,9 @@ class WebView(QWebView):
settings = self.settings()
settings.setAttribute(QWebSettings.JavascriptEnabled, False)
self.stop()
self.page().shutdown()
page = self.page()
assert isinstance(page, webpage.BrowserPage), page
page.shutdown()
def createWindow(self, wintype):
"""Called by Qt when a page wants to create a new window.

View File

@ -63,8 +63,7 @@ class _QtFIFOReader(QObject):
self._fifo = os.fdopen(fd, 'r')
self._notifier = QSocketNotifier(cast(sip.voidptr, fd),
QSocketNotifier.Read, self)
self._notifier.activated.connect( # type: ignore[attr-defined]
self.read_line)
self._notifier.activated.connect(self.read_line)
@pyqtSlot()
def read_line(self):

View File

@ -29,6 +29,7 @@ from qutebrowser.commands import parser, cmdexc
from qutebrowser.misc import objects, split
from qutebrowser.utils import log, utils, debug, objreg
from qutebrowser.completion.models import miscmodels
from qutebrowser.completion import completionwidget
if TYPE_CHECKING:
from qutebrowser.browser import browsertab
@ -74,10 +75,15 @@ class Completer(QObject):
def __repr__(self):
return utils.get_repr(self)
def _completion(self) -> completionwidget.CompletionView:
"""Convenience method to get the current completion."""
completion = self.parent()
assert isinstance(completion, completionwidget.CompletionView), completion
return completion
def _model(self):
"""Convenience method to get the current completion model."""
completion = self.parent()
return completion.model()
return self._completion().model()
def _get_new_completion(self, before_cursor, under_cursor):
"""Get the completion function based on the current command text.
@ -230,7 +236,7 @@ class Completer(QObject):
@pyqtSlot()
def _update_completion(self):
"""Check if completions are available and activate them."""
completion = self.parent()
completion = self._completion()
if self._cmd.prefix() != ':':
# This is a search or gibberish, so we don't need to complete

View File

@ -33,6 +33,7 @@ from PyQt5.QtGui import (QIcon, QPalette, QTextDocument, QTextOption,
from qutebrowser.config import config
from qutebrowser.utils import qtutils
from qutebrowser.completion import completionwidget
class _Highlighter(QSyntaxHighlighter):
@ -233,6 +234,7 @@ class CompletionItemDelegate(QStyledItemDelegate):
if index.parent().isValid():
view = self.parent()
assert isinstance(view, completionwidget.CompletionView), view
pattern = view.pattern
columns_to_filter = index.model().columns_to_filter(index)
if index.column() in columns_to_filter and pattern:
@ -299,7 +301,7 @@ class CompletionItemDelegate(QStyledItemDelegate):
size = self._style.sizeFromContents(QStyle.CT_ItemViewItem, self._opt,
docsize, self._opt.widget)
qtutils.ensure_valid(size)
return size + QSize(10, 3) # type: ignore[operator]
return size + QSize(10, 3)
def paint(self, painter, option, index):
"""Override the QStyledItemDelegate paint function.

View File

@ -30,6 +30,7 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, QSize
from qutebrowser.config import config, stylesheet
from qutebrowser.completion import completiondelegate
from qutebrowser.completion.models import completionmodel
from qutebrowser.utils import utils, usertypes, debug, log, qtutils
from qutebrowser.api import cmdutils
if TYPE_CHECKING:
@ -148,6 +149,15 @@ class CompletionView(QTreeView):
def __repr__(self):
return utils.get_repr(self)
def _model(self) -> completionmodel.CompletionModel:
"""Get the current completion model.
Ensures the model is not None.
"""
model = self.model()
assert isinstance(model, completionmodel.CompletionModel), model
return model
@pyqtSlot(str)
def _on_config_changed(self, option):
if option in ['completion.height', 'completion.shrink']:
@ -158,7 +168,7 @@ class CompletionView(QTreeView):
if self.model() is None:
return
width = self.size().width()
column_widths = self.model().column_widths
column_widths = self._model().column_widths
pixel_widths = [(width * perc // 100) for perc in column_widths]
delta = self.verticalScrollBar().sizeHint().width()
@ -182,21 +192,22 @@ class CompletionView(QTreeView):
Return:
A QModelIndex.
"""
model = self._model()
idx = self.selectionModel().currentIndex()
if not idx.isValid():
# No item selected yet
if upwards:
return self.model().last_item()
return model.last_item()
else:
return self.model().first_item()
return model.first_item()
while True:
idx = self.indexAbove(idx) if upwards else self.indexBelow(idx)
# wrap around if we arrived at beginning/end
if not idx.isValid() and upwards:
return self.model().last_item()
return model.last_item()
elif not idx.isValid() and not upwards:
idx = self.model().first_item()
idx = model.first_item()
self.scrollTo(idx.parent())
return idx
elif idx.parent().isValid():
@ -216,7 +227,7 @@ class CompletionView(QTreeView):
"""
old_idx = self.selectionModel().currentIndex()
idx = old_idx
model = self.model()
model = self._model()
if not idx.isValid():
# No item selected yet
@ -259,6 +270,7 @@ class CompletionView(QTreeView):
A QModelIndex.
"""
idx = self.selectionModel().currentIndex()
model = self._model()
if not idx.isValid():
return self._next_idx(upwards).sibling(0, 0)
idx = idx.parent()
@ -267,10 +279,10 @@ class CompletionView(QTreeView):
idx = idx.sibling(idx.row() + direction, 0)
if not idx.isValid() and upwards:
# wrap around to the first item of the last category
return self.model().last_item().sibling(0, 0)
return model.last_item().sibling(0, 0)
elif not idx.isValid() and not upwards:
# wrap around to the first item of the first category
idx = self.model().first_item()
idx = model.first_item()
self.scrollTo(idx.parent())
return idx
elif idx.isValid() and idx.child(0, 0).isValid():
@ -327,7 +339,7 @@ class CompletionView(QTreeView):
selmodel.setCurrentIndex(
idx,
QItemSelectionModel.ClearAndSelect | # type: ignore[arg-type]
QItemSelectionModel.ClearAndSelect |
QItemSelectionModel.Rows)
# if the last item is focused, try to fetch more
@ -335,7 +347,7 @@ class CompletionView(QTreeView):
if not self.visualRect(next_idx).isValid():
self.expandAll()
count = self.model().count()
count = self._model().count()
if count == 0:
self.hide()
elif count == 1 and config.val.completion.quick:
@ -382,14 +394,14 @@ class CompletionView(QTreeView):
return
self.pattern = pattern
with debug.log_time(log.completion, 'Set pattern {}'.format(pattern)):
self.model().set_pattern(pattern)
self._model().set_pattern(pattern)
self.selectionModel().clear()
self._maybe_update_geometry()
self._maybe_show()
def _maybe_show(self):
if (config.val.completion.show == 'always' and
self.model().count() > 0):
self._model().count() > 0):
self.show()
else:
self.hide()
@ -435,7 +447,7 @@ class CompletionView(QTreeView):
indexes = selected.indexes()
if not indexes:
return
data = str(self.model().data(indexes[0]))
data = str(self._model().data(indexes[0]))
self.selection_changed.emit(data)
def resizeEvent(self, e):
@ -458,7 +470,7 @@ class CompletionView(QTreeView):
index = self.currentIndex()
if not index.isValid():
raise cmdutils.CommandError("No item selected!")
self.model().delete_cur_item(index)
self._model().delete_cur_item(index)
@cmdutils.register(instance='completion',
modes=[usertypes.KeyMode.command], scope='window')
@ -473,7 +485,7 @@ class CompletionView(QTreeView):
index = self.currentIndex()
if not index.isValid():
raise cmdutils.CommandError("No item selected!")
text = self.model().data(index)
text = self._model().data(index)
if not utils.supports_selection():
sel = False

View File

@ -180,10 +180,11 @@ class CompletionModel(QAbstractItemModel):
pattern: The filter pattern to set.
"""
log.completion.debug("Setting completion pattern '{}'".format(pattern))
self.layoutAboutToBeChanged.emit() # type: ignore[attr-defined]
self.layoutAboutToBeChanged.emit()
for cat in self._categories:
cat.set_pattern(pattern)
self.layoutChanged.emit() # type: ignore[attr-defined]
# FIXME:mypy define a Protocol for set_pattern?
cat.set_pattern(pattern) # type: ignore[attr-defined]
self.layoutChanged.emit()
def first_item(self):
"""Return the index of the first child (non-category) in the model."""

View File

@ -83,7 +83,7 @@ def _print_preview(tab: apitypes.Tab) -> None:
diag = QPrintPreviewDialog(tab)
diag.setAttribute(Qt.WA_DeleteOnClose)
diag.setWindowFlags(
diag.windowFlags() | # type: ignore[operator, arg-type]
diag.windowFlags() |
Qt.WindowMaximizeButtonHint |
Qt.WindowMinimizeButtonHint)
diag.paintRequested.connect(functools.partial(

View File

@ -42,7 +42,7 @@ class _ReadlineBridge:
"""Get the currently active QLineEdit."""
# FIXME add this to api.utils or so
qapp = QApplication.instance()
assert qapp is not None
assert isinstance(qapp, QApplication), qapp
w = qapp.focusWidget()
if isinstance(w, QLineEdit):

View File

@ -19,6 +19,7 @@
"""Scrolling-related commands."""
from typing import Dict, Callable
from qutebrowser.api import cmdutils, apitypes
@ -54,7 +55,8 @@ def scroll(tab: apitypes.Tab, direction: str, count: int = 1) -> None:
(up/down/left/right/top/bottom).
count: multiplier
"""
funcs = {
# FIXME:mypy Use a callback protocol to enforce having 'count'?
funcs: Dict[str, Callable[..., None]] = {
'up': tab.scroller.up,
'down': tab.scroller.down,
'left': tab.scroller.left,

View File

@ -23,13 +23,14 @@ import functools
from typing import Optional, FrozenSet
from PyQt5.QtCore import pyqtSlot, QObject
from PyQt5.QtWidgets import QWidget
from qutebrowser.config import config
from qutebrowser.misc import debugcachestats
from qutebrowser.utils import jinja, log
def set_register(obj: QObject,
def set_register(obj: QWidget,
stylesheet: str = None, *,
update: bool = True) -> None:
"""Set the stylesheet for an object.
@ -71,7 +72,7 @@ class _StyleSheetObserver(QObject):
None.
"""
def __init__(self, obj: QObject,
def __init__(self, obj: QWidget,
stylesheet: Optional[str], update: bool) -> None:
super().__init__()
self._obj = obj
@ -81,7 +82,7 @@ class _StyleSheetObserver(QObject):
if update:
self.setParent(self._obj)
if stylesheet is None:
self._stylesheet: str = obj.STYLESHEET
self._stylesheet: str = obj.STYLESHEET # type: ignore[attr-defined]
else:
self._stylesheet = stylesheet

View File

@ -156,7 +156,7 @@ def _assert_plain_key(key: Qt.Key) -> None:
def _assert_plain_modifier(key: _ModifierType) -> None:
"""Make sure this is a modifier without a key mixed in."""
mask = Qt.KeyboardModifierMask
assert not key & ~mask, hex(key) # type: ignore[operator]
assert not key & ~mask, hex(key)
def _is_printable(key: Qt.Key) -> bool:
@ -254,9 +254,9 @@ def _modifiers_to_string(modifiers: _ModifierType) -> str:
modifier.
"""
_assert_plain_modifier(modifiers)
altgr = Qt.GroupSwitchModifier
if modifiers & altgr: # type: ignore[operator]
modifiers &= ~altgr # type: ignore[operator, assignment]
altgr = cast(Qt.KeyboardModifiers, Qt.GroupSwitchModifier)
if modifiers & altgr:
modifiers &= ~altgr
result = 'AltGr+'
else:
result = ''
@ -416,7 +416,7 @@ class KeyInfo:
return ''
text = QKeySequence(self.key).toString()
if not self.modifiers & Qt.ShiftModifier: # type: ignore[operator]
if not self.modifiers & Qt.ShiftModifier:
text = text.lower()
return text
@ -473,7 +473,7 @@ class KeySequence:
"""Iterate over KeyInfo objects."""
for key_and_modifiers in self._iter_keys():
key = Qt.Key(int(key_and_modifiers) & ~Qt.KeyboardModifierMask)
modifiers = Qt.KeyboardModifiers( # type: ignore[call-overload]
modifiers = Qt.KeyboardModifiers(
int(key_and_modifiers) & Qt.KeyboardModifierMask)
yield KeyInfo(key=key, modifiers=modifiers)
@ -619,14 +619,15 @@ class KeySequence:
# (or "Cmd") in a key binding name to actually represent what's on the
# keyboard.
if utils.is_mac:
# FIXME:qt6 Reevaluate the type ignores below
if modifiers & Qt.ControlModifier and modifiers & Qt.MetaModifier:
pass
elif modifiers & Qt.ControlModifier:
modifiers &= ~Qt.ControlModifier
modifiers |= Qt.MetaModifier
modifiers |= Qt.MetaModifier # type: ignore[assignment]
elif modifiers & Qt.MetaModifier:
modifiers &= ~Qt.MetaModifier
modifiers |= Qt.ControlModifier
modifiers |= Qt.ControlModifier # type: ignore[assignment]
keys = list(self._iter_keys())
keys.append(key | int(modifiers))

View File

@ -37,6 +37,7 @@ from qutebrowser.misc import objects
INPUT_MODES = [usertypes.KeyMode.insert, usertypes.KeyMode.passthrough]
PROMPT_MODES = [usertypes.KeyMode.prompt, usertypes.KeyMode.yesno]
# FIXME:mypy TypedDict?
ParserDictType = MutableMapping[usertypes.KeyMode, basekeyparser.BaseKeyParser]

View File

@ -92,7 +92,7 @@ def raise_window(window, alert=True):
window.setWindowState(window.windowState() | Qt.WindowActive)
window.raise_()
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-69568
QCoreApplication.processEvents( # type: ignore[call-overload]
QCoreApplication.processEvents(
QEventLoop.ExcludeUserInputEvents | QEventLoop.ExcludeSocketNotifiers)
if not sip.isdeleted(window):
@ -561,11 +561,11 @@ class MainWindow(QWidget):
def _set_decoration(self, hidden):
"""Set the visibility of the window decoration via Qt."""
window_flags: int = Qt.Window
window_flags = cast(Qt.WindowFlags, Qt.Window)
refresh_window = self.isVisible()
if hidden:
window_flags |= Qt.CustomizeWindowHint | Qt.NoDropShadowWindowHint
self.setWindowFlags(cast(Qt.WindowFlags, window_flags))
self.setWindowFlags(window_flags)
if refresh_window:
self.show()
@ -574,9 +574,7 @@ class MainWindow(QWidget):
if not config.val.content.fullscreen.window:
if on:
self.state_before_fullscreen = self.windowState()
self.setWindowState(
Qt.WindowFullScreen | # type: ignore[arg-type]
self.state_before_fullscreen) # type: ignore[operator]
self.setWindowState(Qt.WindowFullScreen | self.state_before_fullscreen)
elif self.isFullScreen():
self.setWindowState(self.state_before_fullscreen)
log.misc.debug('on: {}, state before fullscreen: {}'.format(
@ -602,7 +600,7 @@ class MainWindow(QWidget):
super().resizeEvent(e)
self._update_overlay_geometries()
self._downloadview.updateGeometry()
self.tabbed_browser.widget.tabBar().refresh()
self.tabbed_browser.widget.tab_bar().refresh()
def showEvent(self, e):
"""Extend showEvent to register us as the last-visible-main-window.

View File

@ -784,7 +784,7 @@ class FilenamePrompt(_BasePrompt):
selmodel.setCurrentIndex(
idx,
QItemSelectionModel.ClearAndSelect | # type: ignore[arg-type]
QItemSelectionModel.ClearAndSelect |
QItemSelectionModel.Rows)
self._insert_path(idx, clicked=False)
@ -808,7 +808,7 @@ class DownloadFilenamePrompt(FilenamePrompt):
def __init__(self, question, parent=None):
super().__init__(question, parent)
self._file_model.setFilter(
QDir.AllDirs | QDir.Drives | QDir.NoDotAndDotDot) # type: ignore[arg-type]
QDir.AllDirs | QDir.Drives | QDir.NoDotAndDotDot)
def accept(self, value=None, save=False):
done = super().accept(value, save)

View File

@ -22,8 +22,7 @@
import enum
import dataclasses
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, # type: ignore[attr-defined]
pyqtProperty, Qt, QSize, QTimer)
from PyQt5.QtCore import pyqtSignal, pyqtSlot, pyqtProperty, Qt, QSize, QTimer
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QStackedLayout, QSizePolicy
from qutebrowser.browser import browsertab
@ -300,7 +299,7 @@ class StatusBar(QWidget):
padding = config.val.statusbar.padding
self._hbox.setContentsMargins(padding.left, 0, padding.right, 0)
@pyqtProperty('QStringList')
@pyqtProperty('QStringList') # type: ignore[type-var]
def color_flags(self):
"""Getter for self.color_flags, so it can be used as Qt property."""
return self._color_flags.to_stringlist()

View File

@ -19,6 +19,7 @@
"""The commandline in the statusbar."""
from typing import Optional
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QSize
from PyQt5.QtGui import QKeyEvent
@ -240,7 +241,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
self.clear_completion_selection.emit()
self.hide_completion.emit()
def setText(self, text: str) -> None:
def setText(self, text: Optional[str]) -> None:
"""Extend setText to set prefix and make sure the prompt is ok."""
if not text:
pass

View File

@ -21,8 +21,7 @@
import enum
from PyQt5.QtCore import (pyqtSlot, pyqtProperty, # type: ignore[attr-defined]
QUrl)
from PyQt5.QtCore import pyqtSlot, pyqtProperty, QUrl
from qutebrowser.mainwindow.statusbar import textbase
from qutebrowser.config import stylesheet
@ -92,7 +91,7 @@ class UrlText(textbase.TextBase):
self._normal_url = None
self._normal_url_type = UrlType.normal
@pyqtProperty(str)
@pyqtProperty(str) # type: ignore[type-var]
def urltype(self):
"""Getter for self.urltype, so it can be used as Qt property.

View File

@ -28,7 +28,7 @@ from typing import (
Any, Deque, List, Mapping, MutableMapping, MutableSequence, Optional, Tuple)
from PyQt5.QtWidgets import QSizePolicy, QWidget, QApplication
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QTimer, QUrl
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QTimer, QUrl, QPoint
from qutebrowser.config import config
from qutebrowser.keyinput import modeman
@ -73,14 +73,14 @@ class TabDeque:
size = config.val.tabs.focus_stack_size
if size < 0:
size = None
self._stack: Deque[weakref.ReferenceType[QWidget]] = collections.deque(
maxlen=size)
self._stack: Deque[weakref.ReferenceType[browsertab.AbstractTab]] = (
collections.deque(maxlen=size))
# Items that have been removed from the primary stack.
self._stack_deleted: List[weakref.ReferenceType[QWidget]] = []
self._stack_deleted: List[weakref.ReferenceType[browsertab.AbstractTab]] = []
self._ignore_next = False
self._keep_deleted_next = False
def on_switch(self, old_tab: QWidget) -> None:
def on_switch(self, old_tab: browsertab.AbstractTab) -> None:
"""Record tab switch events."""
if self._ignore_next:
self._ignore_next = False
@ -92,24 +92,29 @@ class TabDeque:
self._keep_deleted_next = False
self._stack.append(tab)
def prev(self, cur_tab: QWidget) -> QWidget:
def prev(self, cur_tab: browsertab.AbstractTab) -> browsertab.AbstractTab:
"""Get the 'previous' tab in the stack.
Throws IndexError on failure.
"""
tab: Optional[QWidget] = None
tab: Optional[browsertab.AbstractTab] = None
while tab is None or tab.pending_removal or tab is cur_tab:
tab = self._stack.pop()()
self._stack_deleted.append(weakref.ref(cur_tab))
self._ignore_next = True
return tab
def next(self, cur_tab: QWidget, *, keep_overflow: bool = True) -> QWidget:
def next(
self,
cur_tab: browsertab.AbstractTab,
*,
keep_overflow: bool = True,
) -> browsertab.AbstractTab:
"""Get the 'next' tab in the stack.
Throws IndexError on failure.
"""
tab: Optional[QWidget] = None
tab: Optional[browsertab.AbstractTab] = None
while tab is None or tab.pending_removal or tab is cur_tab:
tab = self._stack_deleted.pop()()
# On next tab-switch, current tab will be added to stack as normal.
@ -118,7 +123,7 @@ class TabDeque:
self._keep_deleted_next = True
return tab
def last(self, cur_tab: QWidget) -> QWidget:
def last(self, cur_tab: browsertab.AbstractTab) -> browsertab.AbstractTab:
"""Get the last tab.
Throws IndexError on failure.
@ -216,7 +221,8 @@ class TabbedBrowser(QWidget):
self.widget.tabCloseRequested.connect(self.on_tab_close_requested)
self.widget.new_tab_requested.connect(self.tabopen)
self.widget.currentChanged.connect(self._on_current_changed)
self.cur_fullscreen_requested.connect(self.widget.tabBar().maybe_hide)
self.cur_fullscreen_requested.connect(self.widget.tab_bar().maybe_hide)
self.widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# load_finished instead of load_started as WORKAROUND for
@ -235,8 +241,8 @@ class TabbedBrowser(QWidget):
self._now_focused = None
self.search_text = None
self.search_options: Mapping[str, Any] = {}
self._local_marks: MutableMapping[QUrl, MutableMapping[str, int]] = {}
self._global_marks: MutableMapping[str, Tuple[int, QUrl]] = {}
self._local_marks: MutableMapping[QUrl, MutableMapping[str, QPoint]] = {}
self._global_marks: MutableMapping[str, Tuple[QPoint, QUrl]] = {}
self.default_window_icon = self.widget.window().windowIcon()
self.is_private = private
self.tab_deque = TabDeque()
@ -373,6 +379,25 @@ class TabbedBrowser(QWidget):
tab.history_item_triggered.connect(
history.web_history.add_from_tab)
def _current_tab(self) -> browsertab.AbstractTab:
"""Get the current browser tab.
Note: The assert ensures the current tab is never None.
"""
tab = self.widget.currentWidget()
assert isinstance(tab, browsertab.AbstractTab), tab
return tab
def _tab_by_idx(self, idx: int) -> Optional[browsertab.AbstractTab]:
"""Get a browser tab by index.
If no tab was found at the given index, None is returned.
"""
tab = self.widget.widget(idx)
if tab is not None:
assert isinstance(tab, browsertab.AbstractTab), tab
return tab
def current_url(self):
"""Get the URL of the current tab.
@ -503,13 +528,15 @@ class TabbedBrowser(QWidget):
]
only_one_tab_open = self.widget.count() == 1
if only_one_tab_open and last_close_replaces:
no_history = len(self.widget.widget(0).history) == 1
tab = self._tab_by_idx(0)
assert tab is not None
no_history = len(tab.history) == 1
urls = {
'blank': QUrl('about:blank'),
'startpage': config.val.url.start_pages[0],
'default-page': config.val.url.default_page,
}
first_tab_url = self.widget.widget(0).url()
first_tab_url = tab.url()
last_close_urlstr = urls[last_close].toString().rstrip('/')
first_tab_urlstr = first_tab_url.toString().rstrip('/')
last_close_url_used = first_tab_urlstr == last_close_urlstr
@ -520,7 +547,8 @@ class TabbedBrowser(QWidget):
for entry in reversed(entries):
if use_current_tab:
newtab = self.widget.widget(0)
newtab = self._tab_by_idx(0)
assert newtab is not None
use_current_tab = False
else:
newtab = self.tabopen(background=False, idx=entry.index)
@ -540,14 +568,14 @@ class TabbedBrowser(QWidget):
if newtab or self.widget.currentWidget() is None:
self.tabopen(url, background=False)
else:
self.widget.currentWidget().load_url(url)
self._current_tab().load_url(url)
@pyqtSlot(int)
def on_tab_close_requested(self, idx):
"""Close a tab via an index."""
tab = self.widget.widget(idx)
tab = self._tab_by_idx(idx)
if tab is None:
log.webview.debug( # type: ignore[unreachable]
log.webview.debug(
"Got invalid tab {} for index {}!".format(tab, idx))
return
self.tab_close_prompt_if_pinned(
@ -820,6 +848,7 @@ class TabbedBrowser(QWidget):
mode in modeman.INPUT_MODES):
tab = self.widget.currentWidget()
if tab is not None:
assert isinstance(tab, browsertab.AbstractTab), tab
tab.data.input_mode = mode
@pyqtSlot(usertypes.KeyMode)
@ -833,6 +862,7 @@ class TabbedBrowser(QWidget):
widget))
widget.setFocus()
if config.val.tabs.mode_on_change == 'restore':
assert isinstance(widget, browsertab.AbstractTab), widget
widget.data.input_mode = usertypes.KeyMode.normal
@pyqtSlot(int)
@ -842,9 +872,9 @@ class TabbedBrowser(QWidget):
if idx == -1 or self.is_shutting_down:
# closing the last tab (before quitting) or shutting down
return
tab = self.widget.widget(idx)
tab = self._tab_by_idx(idx)
if tab is None:
log.webview.debug( # type: ignore[unreachable]
log.webview.debug(
"on_current_changed got called with invalid index {}"
.format(idx))
return
@ -1022,7 +1052,7 @@ class TabbedBrowser(QWidget):
if key != "'":
message.error("Failed to set mark: url invalid")
return
point = self.widget.currentWidget().scroller.pos_px()
point = self._current_tab().scroller.pos_px()
if key.isupper():
self._global_marks[key] = point, url
@ -1043,7 +1073,7 @@ class TabbedBrowser(QWidget):
except qtutils.QtValueError:
urlkey = None
tab = self.widget.currentWidget()
tab = self._current_tab()
if key.isupper():
if key in self._global_marks:

View File

@ -22,13 +22,13 @@
import functools
import contextlib
import dataclasses
from typing import Optional, cast
from typing import Optional, Dict, Any
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QSize, QRect, QPoint,
QTimer, QUrl)
from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle,
QStyle, QStylePainter, QStyleOptionTab,
QStyleFactory, QWidget)
QStyleFactory)
from PyQt5.QtGui import QIcon, QPalette, QColor
from qutebrowser.utils import qtutils, objreg, utils, usertypes, log
@ -76,17 +76,30 @@ class TabWidget(QTabWidget):
@config.change_filter('tabs')
def _init_config(self):
"""Initialize attributes based on the config."""
tabbar = self.tabBar()
self.setMovable(True)
self.setTabsClosable(False)
position = config.val.tabs.position
selection_behavior = config.val.tabs.select_on_remove
self.setTabPosition(position)
tabbar.vertical = position in [ # type: ignore[attr-defined]
QTabWidget.West, QTabWidget.East]
tabbar = self.tab_bar()
tabbar.vertical = position in [QTabWidget.West, QTabWidget.East]
tabbar.setSelectionBehaviorOnRemove(selection_behavior)
tabbar.refresh()
def tab_bar(self) -> "TabBar":
"""Get the TabBar for this TabWidget."""
bar = self.tabBar()
assert isinstance(bar, TabBar), bar
return bar
def _tab_by_idx(self, idx: int) -> Optional[browsertab.AbstractTab]:
"""Get the tab at the given index."""
tab = self.widget(idx)
if tab is not None:
assert isinstance(tab, browsertab.AbstractTab), tab
return tab
def set_tab_indicator_color(self, idx, color):
"""Set the tab indicator color.
@ -94,17 +107,17 @@ class TabWidget(QTabWidget):
idx: The tab index.
color: A QColor.
"""
bar = self.tabBar()
bar = self.tab_bar()
bar.set_tab_data(idx, 'indicator-color', color)
bar.update(bar.tabRect(idx))
def tab_indicator_color(self, idx):
"""Get the tab indicator color for the given index."""
return self.tabBar().tab_indicator_color(idx)
return self.tab_bar().tab_indicator_color(idx)
def set_page_title(self, idx, title):
"""Set the tab title user data."""
tabbar = self.tabBar()
tabbar = self.tab_bar()
if config.cache['tabs.tooltips']:
# always show only plain title in tooltips
@ -115,7 +128,7 @@ class TabWidget(QTabWidget):
def page_title(self, idx):
"""Get the tab title user data."""
return self.tabBar().page_title(idx)
return self.tab_bar().page_title(idx)
def update_tab_title(self, idx, field=None):
"""Update the tab text for the given tab.
@ -126,7 +139,8 @@ class TabWidget(QTabWidget):
is only set if the given field is in the template.
"""
assert idx != -1
tab = self.widget(idx)
tab = self._tab_by_idx(idx)
assert tab is not None
if tab.data.pinned:
fmt = config.cache['tabs.title.format_pinned']
else:
@ -142,7 +156,7 @@ class TabWidget(QTabWidget):
def left_align(num):
return str(num).ljust(len(str(self.count())))
bar = self.tabBar()
bar = self.tab_bar()
cur_idx = bar.currentIndex()
if idx == cur_idx:
rel_idx = left_align(idx + 1) + " "
@ -164,14 +178,12 @@ class TabWidget(QTabWidget):
def get_tab_fields(self, idx):
"""Get the tab field data."""
tab = self.widget(idx)
if tab is None:
log.misc.debug( # type: ignore[unreachable]
"Got None-tab in get_tab_fields!")
tab = self._tab_by_idx(idx)
assert tab is not None
page_title = self.page_title(idx)
fields = {}
fields: Dict[str, Any] = {}
fields['id'] = tab.tab_id
fields['current_title'] = page_title
fields['title_sep'] = ' - ' if page_title else ''
@ -206,9 +218,7 @@ class TabWidget(QTabWidget):
fields['protocol'] = url.scheme()
y = tab.scroller.pos_perc()[1]
if y is None:
scroll_pos = '???'
elif y <= 0:
if y <= 0:
scroll_pos = 'top'
elif y >= 100:
scroll_pos = 'bot'
@ -228,7 +238,7 @@ class TabWidget(QTabWidget):
non-visible. To avoid flickering, disable repaint updates while we
work.
"""
bar = self.tabBar()
bar = self.tab_bar()
toggle = (self.count() > 10 and
not bar.drag_in_progress and
bar.isVisible())
@ -317,7 +327,7 @@ class TabWidget(QTabWidget):
@pyqtSlot(int)
def _on_current_changed(self, index):
"""Emit the tab_index_changed signal if the current tab changed."""
self.tabBar().on_current_changed()
self.tab_bar().on_current_changed()
self.update_tab_titles()
self.tab_index_changed.emit(index, self.count())
@ -332,16 +342,13 @@ class TabWidget(QTabWidget):
Return:
The tab URL as QUrl.
"""
tab = self.widget(idx)
if tab is None:
url = QUrl() # type: ignore[unreachable]
else:
url = tab.url()
tab = self._tab_by_idx(idx)
url = QUrl() if tab is None else tab.url()
# It's possible for url to be invalid, but the caller will handle that.
qtutils.ensure_valid(url)
return url
def update_tab_favicon(self, tab: QWidget) -> None:
def update_tab_favicon(self, tab: browsertab.AbstractTab) -> None:
"""Update favicon of the given tab."""
idx = self.indexOf(tab)
@ -353,11 +360,11 @@ class TabWidget(QTabWidget):
def setTabIcon(self, idx: int, icon: QIcon) -> None:
"""Always show tab icons for pinned tabs in some circumstances."""
tab = cast(Optional[browsertab.AbstractTab], self.widget(idx))
tab = self._tab_by_idx(idx)
if (icon.isNull() and
config.cache['tabs.favicons.show'] != 'never' and
config.cache['tabs.pinned.shrink'] and
not self.tabBar().vertical and
not self.tab_bar().vertical and
tab is not None and tab.data.pinned):
icon = self.style().standardIcon(QStyle.SP_FileIcon)
super().setTabIcon(idx, icon)
@ -404,7 +411,10 @@ class TabBar(QTabBar):
self._auto_hide_timer.timeout.connect(self.maybe_hide)
self._on_show_switching_delay_changed()
self.setAutoFillBackground(True)
self.drag_in_progress = False
# FIXME:mypy Is it a mypy bug that we need to specify bool here?
# Otherwise, we get "Cannot determine type of "drag_in_progress" in
# TabWidget._toggle_visibility below.
self.drag_in_progress: bool = False
stylesheet.set_register(self)
self.ensurePolished()
config.instance.changed.connect(self._on_config_changed)
@ -423,9 +433,15 @@ class TabBar(QTabBar):
def __repr__(self):
return utils.get_repr(self, count=self.count())
def _tab_widget(self):
"""Get the TabWidget we're in."""
parent = self.parent()
assert isinstance(parent, TabWidget), parent
return parent
def _current_tab(self):
"""Get the current tab object."""
return self.parent().currentWidget()
return self._tab_widget().currentWidget()
@pyqtSlot(str)
def _on_config_changed(self, option: str) -> None:
@ -627,7 +643,7 @@ class TabBar(QTabBar):
raise IndexError("Tab index ({}) out of range ({})!".format(
index, self.count()))
widget = self.parent().widget(index)
widget = self._tab_widget().widget(index)
if widget is None:
# This could happen when Qt calls tabSizeHint while initializing
# tabs.

View File

@ -361,8 +361,7 @@ class SignalHandler(QObject):
self._notifier = QSocketNotifier(cast(sip.voidptr, read_fd),
QSocketNotifier.Read,
self)
self._notifier.activated.connect( # type: ignore[attr-defined]
self.handle_signal_wakeup)
self._notifier.activated.connect(self.handle_signal_wakeup)
self._orig_wakeup_fd = signal.set_wakeup_fd(write_fd)
# pylint: enable=import-error,no-member,useless-suppression
else:

View File

@ -207,8 +207,7 @@ class ExternalEditor(QObject):
if not ok:
log.procs.error("Failed to watch path: {}"
.format(self._filename))
self._watcher.fileChanged.connect( # type: ignore[attr-defined]
self._on_file_changed)
self._watcher.fileChanged.connect(self._on_file_changed)
args = [self._sub_placeholder(arg, line, column) for arg in editor[1:]]
log.procs.debug("Calling \"{}\" with args {}".format(executable, args))

View File

@ -191,8 +191,7 @@ class IPCServer(QObject):
self._atime_timer.setTimerType(Qt.VeryCoarseTimer)
self._server = QLocalServer(self)
self._server.newConnection.connect( # type: ignore[attr-defined]
self.handle_connection)
self._server.newConnection.connect(self.handle_connection)
self._socket = None
self._old_socket = None
@ -270,18 +269,16 @@ class IPCServer(QObject):
log.ipc.debug("Client connected (socket 0x{:x}).".format(id(socket)))
self._socket = socket
self._timer.start()
socket.readyRead.connect( # type: ignore[attr-defined]
self.on_ready_read)
socket.readyRead.connect(self.on_ready_read)
if socket.canReadLine():
log.ipc.debug("We can read a line immediately.")
self.on_ready_read()
socket.error.connect(self.on_error) # type: ignore[attr-defined]
if socket.error() not in [QLocalSocket.UnknownSocketError,
QLocalSocket.PeerClosedError]:
socket.error.connect(self.on_error)
if socket.error() not in [ # type: ignore[operator]
QLocalSocket.UnknownSocketError, QLocalSocket.PeerClosedError]:
log.ipc.debug("We got an error immediately.")
self.on_error(socket.error())
socket.disconnected.connect( # type: ignore[attr-defined]
self.on_disconnected)
self.on_error(socket.error()) # type: ignore[operator]
socket.disconnected.connect(self.on_disconnected)
if socket.state() == QLocalSocket.UnconnectedState:
log.ipc.debug("Socket was disconnected immediately.")
self.on_disconnected()

View File

@ -26,7 +26,7 @@ import argparse
from typing import TYPE_CHECKING, Any, Dict, Set, Union, cast
if TYPE_CHECKING:
from PyQt5.QtWidgets import QApplication
from qutebrowser import app
from qutebrowser.utils import usertypes
from qutebrowser.commands import command
@ -47,4 +47,4 @@ backend: Union['usertypes.Backend', NoBackend] = NoBackend()
commands: Dict[str, 'command.Command'] = {}
debug_flags: Set[str] = set()
args = cast(argparse.Namespace, None)
qapp = cast('QApplication', None)
qapp = cast('app.Application', None)

View File

@ -170,7 +170,7 @@ def _get_tab_registry(win_id: _WindowTab,
window: Optional[QWidget] = QApplication.activeWindow()
if window is None or not hasattr(window, 'win_id'):
raise RegistryUnavailableError('tab')
win_id = window.win_id
win_id = window.win_id # type: ignore[attr-defined]
elif win_id is None:
raise TypeError("window is None with scope tab!")
@ -205,7 +205,7 @@ def _get_window_registry(window: _WindowTab) -> ObjectRegistry:
raise RegistryUnavailableError('window')
try:
return win.registry
return win.registry # type: ignore[attr-defined]
except AttributeError:
raise RegistryUnavailableError('window')

View File

@ -25,7 +25,7 @@ import pytest
from PyQt5.QtCore import QObject
from PyQt5.QtGui import QStandardItemModel
from qutebrowser.completion import completer
from qutebrowser.completion import completer, completionwidget
from qutebrowser.commands import command
from qutebrowser.api import cmdutils
@ -48,12 +48,12 @@ class FakeCompletionModel(QStandardItemModel):
self.info = info
class CompletionWidgetStub(QObject):
class CompletionWidgetStub(completionwidget.CompletionView):
"""Stub for the CompletionView."""
def __init__(self, parent=None):
super().__init__(parent)
super().__init__(cmd=None, win_id=0, parent=parent)
self.hide = unittest.mock.Mock()
self.show = unittest.mock.Mock()
self.set_pattern = unittest.mock.Mock()