summaryrefslogtreecommitdiff
path: root/qutebrowser/browser/hints.py
diff options
context:
space:
mode:
authorFlorian Bruhin <me@the-compiler.org>2019-10-10 09:28:46 +0200
committerFlorian Bruhin <me@the-compiler.org>2019-10-10 09:37:29 +0200
commit00c341fe6dfaffa69d846a469fc8e48d1c20fe6c (patch)
tree148c9780622deb03f2cb5cc36458915d0e873496 /qutebrowser/browser/hints.py
parent62dd08f6d6c2ce97272286b6af3e1bc42abd29af (diff)
downloadqutebrowser-00c341fe6dfaffa69d846a469fc8e48d1c20fe6c.tar.gz
qutebrowser-00c341fe6dfaffa69d846a469fc8e48d1c20fe6c.zip
Add type hints for browser.hints
Diffstat (limited to 'qutebrowser/browser/hints.py')
-rw-r--r--qutebrowser/browser/hints.py231
1 files changed, 133 insertions, 98 deletions
diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py
index b7568546e..f7d75c09f 100644
--- a/qutebrowser/browser/hints.py
+++ b/qutebrowser/browser/hints.py
@@ -20,6 +20,7 @@
"""A HintManager to draw hints over links."""
import collections
+import typing
import functools
import os
import re
@@ -37,6 +38,8 @@ from qutebrowser.browser import webelem
from qutebrowser.commands import userscripts, runners
from qutebrowser.api import cmdutils
from qutebrowser.utils import usertypes, log, qtutils, message, objreg, utils
+if typing.TYPE_CHECKING:
+ from qutebrowser.browser import browsertab
Target = enum.Enum('Target', ['normal', 'current', 'tab', 'tab_fg', 'tab_bg',
@@ -50,7 +53,7 @@ class HintingError(Exception):
"""Exception raised on errors during hinting."""
-def on_mode_entered(mode, win_id):
+def on_mode_entered(mode: usertypes.KeyMode, win_id: int) -> None:
"""Stop hinting when insert mode was entered."""
if mode == usertypes.KeyMode.insert:
modeman.leave(win_id, usertypes.KeyMode.hint, 'insert mode',
@@ -66,7 +69,8 @@ class HintLabel(QLabel):
_context: The current hinting context.
"""
- def __init__(self, elem, context):
+ def __init__(self, elem: webelem.AbstractWebElement,
+ context: 'HintContext') -> None:
super().__init__(parent=context.tab)
self._context = context
self.elem = elem
@@ -81,14 +85,14 @@ class HintLabel(QLabel):
self._move_to_elem()
self.show()
- def __repr__(self):
+ def __repr__(self) -> str:
try:
text = self.text()
except RuntimeError:
text = '<deleted>'
return utils.get_repr(self, elem=self.elem, text=text)
- def update_text(self, matched, unmatched):
+ def update_text(self, matched: str, unmatched: str) -> None:
"""Set the text for the hint.
Args:
@@ -112,7 +116,7 @@ class HintLabel(QLabel):
self.adjustSize()
@pyqtSlot()
- def _move_to_elem(self):
+ def _move_to_elem(self) -> None:
"""Reposition the label to its element."""
if not self.elem.has_frame():
# This sometimes happens for some reason...
@@ -123,7 +127,7 @@ class HintLabel(QLabel):
rect = self.elem.rect_on_view(no_js=no_js)
self.move(rect.x(), rect.y())
- def cleanup(self):
+ def cleanup(self) -> None:
"""Clean up this element and hide it."""
self.hide()
self.deleteLater()
@@ -159,22 +163,22 @@ class HintContext:
group: The group of web elements to hint.
"""
- all_labels = attr.ib(attr.Factory(list))
- labels = attr.ib(attr.Factory(dict))
- target = attr.ib(None)
- baseurl = attr.ib(None)
- to_follow = attr.ib(None)
- rapid = attr.ib(False)
- first_run = attr.ib(True)
- add_history = attr.ib(False)
- filterstr = attr.ib(None)
- args = attr.ib(attr.Factory(list))
- tab = attr.ib(None)
- group = attr.ib(None)
- hint_mode = attr.ib(None)
- first = attr.ib(False)
-
- def get_args(self, urlstr):
+ all_labels = attr.ib(attr.Factory(list)) # type: typing.List[HintLabel]
+ labels = attr.ib(attr.Factory(dict)) # type: typing.Dict[str, HintLabel]
+ target = attr.ib(None) # type: Target
+ baseurl = attr.ib(None) # type: QUrl
+ to_follow = attr.ib(None) # type: str
+ rapid = attr.ib(False) # type: bool
+ first_run = attr.ib(True) # type: bool
+ add_history = attr.ib(False) # type: bool
+ filterstr = attr.ib(None) # type: str
+ args = attr.ib(attr.Factory(list)) # type: typing.List[str]
+ tab = attr.ib(None) # type: browsertab.AbstractTab
+ group = attr.ib(None) # type: str
+ hint_mode = attr.ib(None) # type: str
+ first = attr.ib(False) # type: bool
+
+ def get_args(self, urlstr: str) -> typing.Sequence[str]:
"""Get the arguments, with {hint-url} replaced by the given URL."""
args = []
for arg in self.args:
@@ -187,16 +191,12 @@ class HintActions:
"""Actions which can be done after selecting a hint."""
- def __init__(self, win_id):
+ def __init__(self, win_id: int) -> None:
self._win_id = win_id
- def click(self, elem, context):
- """Click an element.
-
- Args:
- elem: The QWebElement to click.
- context: The HintContext to use.
- """
+ def click(self, elem: webelem.AbstractWebElement,
+ context: HintContext) -> None:
+ """Click an element."""
target_mapping = {
Target.normal: usertypes.ClickTarget.normal,
Target.current: usertypes.ClickTarget.normal,
@@ -225,20 +225,15 @@ class HintActions:
except webelem.Error as e:
raise HintingError(str(e))
- def yank(self, url, context):
- """Yank an element to the clipboard or primary selection.
-
- Args:
- url: The URL to open as a QUrl.
- context: The HintContext to use.
- """
+ def yank(self, url: QUrl, context: HintContext) -> None:
+ """Yank an element to the clipboard or primary selection."""
sel = (context.target == Target.yank_primary and
utils.supports_selection())
flags = QUrl.FullyEncoded | QUrl.RemovePassword
if url.scheme() == 'mailto':
flags |= QUrl.RemoveScheme
- urlstr = url.toString(flags)
+ urlstr = url.toString(flags) # type: ignore
new_content = urlstr
@@ -257,26 +252,16 @@ class HintActions:
urlstr)
message.info(msg)
- def run_cmd(self, url, context):
- """Run the command based on a hint URL.
-
- Args:
- url: The URL to open as a QUrl.
- context: The HintContext to use.
- """
- urlstr = url.toString(QUrl.FullyEncoded)
+ def run_cmd(self, url: QUrl, context: HintContext) -> None:
+ """Run the command based on a hint URL."""
+ urlstr = url.toString(QUrl.FullyEncoded) # type: ignore
args = context.get_args(urlstr)
commandrunner = runners.CommandRunner(self._win_id)
commandrunner.run_safely(' '.join(args))
- def preset_cmd_text(self, url, context):
- """Preset a commandline text based on a hint URL.
-
- Args:
- url: The URL to open as a QUrl.
- context: The HintContext to use.
- """
- urlstr = url.toDisplayString(QUrl.FullyEncoded)
+ def preset_cmd_text(self, url: QUrl, context: HintContext) -> None:
+ """Preset a commandline text based on a hint URL."""
+ urlstr = url.toDisplayString(QUrl.FullyEncoded) # type: ignore
args = context.get_args(urlstr)
text = ' '.join(args)
if text[0] not in modeparsers.STARTCHARS:
@@ -285,7 +270,8 @@ class HintActions:
cmd = objreg.get('status-command', scope='window', window=self._win_id)
cmd.set_cmd_text(text)
- def download(self, elem, context):
+ def download(self, elem: webelem.AbstractWebElement,
+ context: HintContext) -> None:
"""Download a hint URL.
Args:
@@ -305,7 +291,8 @@ class HintActions:
download_manager.get(url, qnam=qnam, user_agent=user_agent,
prompt_download_directory=prompt)
- def call_userscript(self, elem, context):
+ def call_userscript(self, elem: webelem.AbstractWebElement,
+ context: HintContext) -> None:
"""Call a userscript from a hint.
Args:
@@ -321,7 +308,7 @@ class HintActions:
}
url = elem.resolve_url(context.baseurl)
if url is not None:
- env['QUTE_URL'] = url.toString(QUrl.FullyEncoded)
+ env['QUTE_URL'] = url.toString(QUrl.FullyEncoded) # type: ignore
try:
userscripts.run_async(context.tab, cmd, *args, win_id=self._win_id,
@@ -329,22 +316,28 @@ class HintActions:
except userscripts.Error as e:
raise HintingError(str(e))
- def delete(self, elem, _context):
+ def delete(self, elem: webelem.AbstractWebElement,
+ _context: HintContext) -> None:
elem.delete()
- def spawn(self, url, context):
+ def spawn(self, url: QUrl, context: HintContext) -> None:
"""Spawn a simple command from a hint.
Args:
url: The URL to open as a QUrl.
context: The HintContext to use.
"""
- urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
+ urlstr = url.toString(
+ QUrl.FullyEncoded | QUrl.RemovePassword) # type: ignore
args = context.get_args(urlstr)
commandrunner = runners.CommandRunner(self._win_id)
commandrunner.run_safely('spawn ' + ' '.join(args))
+_ElemsType = typing.Sequence[webelem.AbstractWebElement]
+_HintStringsType = typing.MutableSequence[str]
+
+
class HintManager(QObject):
"""Manage drawing hints over links or other elements.
@@ -379,11 +372,11 @@ class HintManager(QObject):
Target.delete: "Delete an element",
}
- def __init__(self, win_id, parent=None):
+ def __init__(self, win_id: int, parent: QObject = None) -> None:
"""Constructor."""
super().__init__(parent)
self._win_id = win_id
- self._context = None
+ self._context = None # type: typing.Optional[HintContext]
self._word_hinter = WordHinter()
self._actions = HintActions(win_id)
@@ -392,16 +385,18 @@ class HintManager(QObject):
window=win_id)
mode_manager.left.connect(self.on_mode_left)
- def _get_text(self):
+ def _get_text(self) -> str:
"""Get a hint text based on the current context."""
+ assert self._context is not None
text = self.HINT_TEXTS[self._context.target]
if self._context.rapid:
text += ' (rapid mode)'
text += '...'
return text
- def _cleanup(self):
+ def _cleanup(self) -> None:
"""Clean up after hinting."""
+ assert self._context is not None
for label in self._context.all_labels:
label.cleanup()
@@ -411,7 +406,7 @@ class HintManager(QObject):
message_bridge.maybe_reset_text(text)
self._context = None
- def _hint_strings(self, elems):
+ def _hint_strings(self, elems: _ElemsType) -> _HintStringsType:
"""Calculate the hint strings for elems.
Inspired by Vimium.
@@ -424,6 +419,8 @@ class HintManager(QObject):
"""
if not elems:
return []
+
+ assert self._context is not None
hint_mode = self._context.hint_mode
if hint_mode == 'word':
try:
@@ -441,7 +438,9 @@ class HintManager(QObject):
else:
return self._hint_linear(min_chars, chars, elems)
- def _hint_scattered(self, min_chars, chars, elems):
+ def _hint_scattered(self, min_chars: int,
+ chars: str,
+ elems: _ElemsType) -> _HintStringsType:
"""Produce scattered hint labels with variable length (like Vimium).
Args:
@@ -478,7 +477,9 @@ class HintManager(QObject):
return self._shuffle_hints(strings, len(chars))
- def _hint_linear(self, min_chars, chars, elems):
+ def _hint_linear(self, min_chars: int,
+ chars: str,
+ elems: _ElemsType) -> _HintStringsType:
"""Produce linear hint labels with constant length (like dwb).
Args:
@@ -492,7 +493,8 @@ class HintManager(QObject):
strings.append(self._number_to_hint_str(i, chars, needed))
return strings
- def _shuffle_hints(self, hints, length):
+ def _shuffle_hints(self, hints: _HintStringsType,
+ length: int) -> _HintStringsType:
"""Shuffle the given set of hints so that they're scattered.
Hints starting with the same character will be spread evenly throughout
@@ -507,15 +509,19 @@ class HintManager(QObject):
Return:
A list of shuffled hint strings.
"""
- buckets = [[] for i in range(length)]
+ buckets = [
+ [] for i in range(length)
+ ] # type: typing.Sequence[_HintStringsType]
for i, hint in enumerate(hints):
buckets[i % len(buckets)].append(hint)
- result = []
+ result = [] # type: _HintStringsType
for bucket in buckets:
result += bucket
return result
- def _number_to_hint_str(self, number, chars, digits=0):
+ def _number_to_hint_str(self, number: int,
+ chars: str,
+ digits: int = 0) -> str:
"""Convert a number like "8" into a hint string like "JK".
This is used to sequentially generate all of the hint text.
@@ -533,7 +539,7 @@ class HintManager(QObject):
A hint string.
"""
base = len(chars)
- hintstr = []
+ hintstr = [] # type: typing.MutableSequence[str]
remainder = 0
while True:
remainder = number % base
@@ -547,7 +553,7 @@ class HintManager(QObject):
hintstr.insert(0, chars[0])
return ''.join(hintstr)
- def _check_args(self, target, *args):
+ def _check_args(self, target: Target, *args: str) -> None:
"""Check the arguments passed to start() and raise if they're wrong.
Args:
@@ -567,7 +573,7 @@ class HintManager(QObject):
raise cmdutils.CommandError(
"'args' is only allowed with target userscript/spawn.")
- def _filter_matches(self, filterstr, elemstr):
+ def _filter_matches(self, filterstr: str, elemstr: str) -> bool:
"""Return True if `filterstr` matches `elemstr`."""
# Empty string and None always match
if not filterstr:
@@ -577,7 +583,7 @@ class HintManager(QObject):
# Do multi-word matching
return all(word in elemstr for word in filterstr.split())
- def _filter_matches_exactly(self, filterstr, elemstr):
+ def _filter_matches_exactly(self, filterstr: str, elemstr: str) -> bool:
"""Return True if `filterstr` exactly matches `elemstr`."""
# Empty string and None never match
if not filterstr:
@@ -586,7 +592,7 @@ class HintManager(QObject):
elemstr = elemstr.casefold()
return filterstr == elemstr
- def _start_cb(self, elems):
+ def _start_cb(self, elems: _ElemsType) -> None:
"""Initialize the elements and labels based on the context set."""
if self._context is None:
log.hints.debug("In _start_cb without context!")
@@ -637,8 +643,13 @@ class HintManager(QObject):
@cmdutils.register(instance='hintmanager', scope='window', name='hint',
star_args_optional=True, maxsplit=2)
def start(self, # pylint: disable=keyword-arg-before-vararg
- group='all', target=Target.normal, *args, mode=None,
- add_history=False, rapid=False, first=False):
+ group: str = 'all',
+ target: Target = Target.normal,
+ *args: str,
+ mode: str = None,
+ add_history: bool = False,
+ rapid: bool = False,
+ first: bool = False) -> None:
"""Start hinting.
Args:
@@ -754,7 +765,7 @@ class HintManager(QObject):
error_cb=lambda err: message.error(str(err)),
only_visible=True)
- def _get_hint_mode(self, mode):
+ def _get_hint_mode(self, mode: typing.Optional[str]) -> str:
"""Get the hinting mode to use based on a mode argument."""
if mode is None:
return config.val.hints.mode
@@ -766,15 +777,22 @@ class HintManager(QObject):
raise cmdutils.CommandError("Invalid mode: {}".format(e))
return mode
- def current_mode(self):
+ def current_mode(self) -> typing.Optional[str]:
"""Return the currently active hinting mode (or None otherwise)."""
if self._context is None:
return None
return self._context.hint_mode
- def _handle_auto_follow(self, keystr="", filterstr="", visible=None):
+ def _handle_auto_follow(
+ self,
+ keystr: str = "",
+ filterstr: str = "",
+ visible: typing.Mapping[str, HintLabel] = None
+ ) -> None:
"""Handle the auto_follow option."""
+ assert self._context is not None
+
if visible is None:
visible = {string: label
for string, label in self._context.labels.items()
@@ -788,7 +806,7 @@ class HintManager(QObject):
if auto_follow == "always":
follow = True
elif auto_follow == "unique-match":
- follow = keystr or filterstr
+ follow = bool(keystr or filterstr)
elif auto_follow == "full-match":
elemstr = str(list(visible.values())[0].elem)
filter_match = self._filter_matches_exactly(filterstr, elemstr)
@@ -810,7 +828,7 @@ class HintManager(QObject):
self._fire(*visible)
@pyqtSlot(str)
- def handle_partial_key(self, keystr):
+ def handle_partial_key(self, keystr: str) -> None:
"""Handle a new partial keypress."""
if self._context is None:
log.hints.debug("Got key without context!")
@@ -834,7 +852,7 @@ class HintManager(QObject):
pass
self._handle_auto_follow(keystr=keystr)
- def filter_hints(self, filterstr):
+ def filter_hints(self, filterstr: typing.Optional[str]) -> None:
"""Filter displayed hints according to a text.
Args:
@@ -844,6 +862,8 @@ class HintManager(QObject):
and `self._context.filterstr` are None, all hints are
shown.
"""
+ assert self._context is not None
+
if filterstr is None:
filterstr = self._context.filterstr
else:
@@ -889,12 +909,13 @@ class HintManager(QObject):
self._handle_auto_follow(filterstr=filterstr,
visible=self._context.labels)
- def _fire(self, keystr):
+ def _fire(self, keystr: str) -> None:
"""Fire a completed hint.
Args:
keystr: The keychain string to follow.
"""
+ assert self._context is not None
# Handlers which take a QWebElement
elem_handlers = {
Target.normal: self._actions.click,
@@ -958,13 +979,14 @@ class HintManager(QObject):
@cmdutils.register(instance='hintmanager', scope='window',
modes=[usertypes.KeyMode.hint])
- def follow_hint(self, select=False, keystring=None):
+ def follow_hint(self, select: bool = False, keystring: str = None) -> None:
"""Follow a hint.
Args:
select: Only select the given hint, don't necessarily follow it.
keystring: The hint to follow, or None.
"""
+ assert self._context is not None
if keystring is None:
if self._context.to_follow is None:
raise cmdutils.CommandError("No hint to follow")
@@ -980,7 +1002,7 @@ class HintManager(QObject):
self._fire(keystring)
@pyqtSlot(usertypes.KeyMode)
- def on_mode_left(self, mode):
+ def on_mode_left(self, mode: usertypes.KeyMode) -> None:
"""Stop hinting when hinting mode was left."""
if mode != usertypes.KeyMode.hint or self._context is None:
# We have one HintManager per tab, so when this gets called,
@@ -999,12 +1021,12 @@ class WordHinter:
derived from the hinted element.
"""
- def __init__(self):
+ def __init__(self) -> None:
# will be initialized on first use.
- self.words = set()
+ self.words = set() # type: typing.Set[str]
self.dictionary = None
- def ensure_initialized(self):
+ def ensure_initialized(self) -> None:
"""Generate the used words if yet uninitialized."""
dictionary = config.val.hints.dictionary
if not self.words or self.dictionary != dictionary:
@@ -1031,8 +1053,11 @@ class WordHinter:
error = "Word hints requires reading the file at {}: {}"
raise HintingError(error.format(dictionary, str(e)))
- def extract_tag_words(self, elem):
+ def extract_tag_words(
+ self, elem: webelem.AbstractWebElement
+ ) -> typing.Iterator[str]:
"""Extract tag words form the given element."""
+ _extractor_type = typing.Callable[[webelem.AbstractWebElement], str]
attr_extractors = {
"alt": lambda elem: elem["alt"],
"name": lambda elem: elem["name"],
@@ -1041,7 +1066,7 @@ class WordHinter:
"src": lambda elem: elem["src"].split('/')[-1],
"href": lambda elem: elem["href"].split('/')[-1],
"text": str,
- }
+ } # type: typing.Mapping[str, _extractor_type]
extractable_attrs = collections.defaultdict(list, {
"img": ["alt", "title", "src"],
@@ -1055,7 +1080,10 @@ class WordHinter:
for attr in extractable_attrs[elem.tag_name()]
if attr in elem or attr == "text")
- def tag_words_to_hints(self, words):
+ def tag_words_to_hints(
+ self,
+ words: typing.Iterable[str]
+ ) -> typing.Iterator[str]:
"""Take words and transform them to proper hints if possible."""
for candidate in words:
if not candidate:
@@ -1066,13 +1094,20 @@ class WordHinter:
if 4 < match.end() - match.start() < 8:
yield candidate[match.start():match.end()].lower()
- def any_prefix(self, hint, existing):
+ def any_prefix(self, hint: str, existing: typing.Iterable[str]) -> bool:
return any(hint.startswith(e) or e.startswith(hint) for e in existing)
- def filter_prefixes(self, hints, existing):
+ def filter_prefixes(
+ self,
+ hints: typing.Iterable[str],
+ existing: typing.Iterable[str]
+ ) -> typing.Iterator[str]:
+ """Filter hints which don't start with the given prefix."""
return (h for h in hints if not self.any_prefix(h, existing))
- def new_hint_for(self, elem, existing, fallback):
+ def new_hint_for(self, elem: webelem.AbstractWebElement,
+ existing: typing.Iterable[str],
+ fallback: typing.Iterable[str]) -> typing.Optional[str]:
"""Return a hint for elem, not conflicting with the existing."""
new = self.tag_words_to_hints(self.extract_tag_words(elem))
new_no_prefixes = self.filter_prefixes(new, existing)
@@ -1081,7 +1116,7 @@ class WordHinter:
return (next(new_no_prefixes, None) or
next(fallback_no_prefixes, None))
- def hint(self, elems):
+ def hint(self, elems: _ElemsType) -> _HintStringsType:
"""Produce hint labels based on the html tags.
Produce hint words based on the link text and random words
@@ -1096,7 +1131,7 @@ class WordHinter:
"""
self.ensure_initialized()
hints = []
- used_hints = set()
+ used_hints = set() # type: typing.Set[str]
words = iter(self.words)
for elem in elems:
hint = self.new_hint_for(elem, used_hints, words)