diff options
author | Florian Bruhin <me@the-compiler.org> | 2019-10-10 09:28:46 +0200 |
---|---|---|
committer | Florian Bruhin <me@the-compiler.org> | 2019-10-10 09:37:29 +0200 |
commit | 00c341fe6dfaffa69d846a469fc8e48d1c20fe6c (patch) | |
tree | 148c9780622deb03f2cb5cc36458915d0e873496 /qutebrowser/browser/hints.py | |
parent | 62dd08f6d6c2ce97272286b6af3e1bc42abd29af (diff) | |
download | qutebrowser-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.py | 231 |
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) |