def first_opened_window() -> 'mainwindow.MainWindow': """Get the first opened window object.""" for idx in range(0, len(window_registry)+1): window = _window_by_index(idx) if not window.tabbed_browser.is_shutting_down: return window raise utils.Unreachable()
def _get_widget_from_config(self, key): """Return the widget that fits with config string key.""" if key == 'url': return self.url elif key == 'scroll': return self.percentage elif key == 'scroll_raw': return self.percentage elif key == 'history': return self.backforward elif key == 'tabs': return self.tabindex elif key == 'keypress': return self.keystring elif key == 'progress': return self.prog elif key == 'search_match': return self.search_match elif key.startswith('text:'): new_text_widget = textbase.TextBase() self._text_widgets.append(new_text_widget) return new_text_widget elif key.startswith('clock:') or key == 'clock': return self.clock else: raise utils.Unreachable(key)
def _variant(versions: version.WebEngineVersions) -> Variant: """Get the dark mode variant based on the underlying Qt version.""" env_var = os.environ.get('QUTE_DARKMODE_VARIANT') if env_var is not None: try: return Variant[env_var] except KeyError: log.init.warning( f"Ignoring invalid QUTE_DARKMODE_VARIANT={env_var}") if (versions.webengine == utils.VersionNumber(5, 15, 2) and versions.chromium_major == 87): # WORKAROUND for Gentoo packaging something newer as 5.15.2... return Variant.qt_515_3 elif versions.webengine >= utils.VersionNumber(5, 15, 3): return Variant.qt_515_3 elif versions.webengine >= utils.VersionNumber(5, 15, 2): return Variant.qt_515_2 elif versions.webengine == utils.VersionNumber(5, 15, 1): return Variant.qt_515_1 elif versions.webengine == utils.VersionNumber(5, 15): return Variant.qt_515_0 elif versions.webengine >= utils.VersionNumber(5, 14): return Variant.qt_514 elif versions.webengine >= utils.VersionNumber(5, 11): return Variant.qt_511_to_513 raise utils.Unreachable(versions.webengine)
def create(*, splitter: 'miscwidgets.InspectorSplitter', win_id: int, parent: QWidget = None) -> 'AbstractWebInspector': """Get a WebKitInspector/WebEngineInspector. Args: splitter: InspectorSplitter where the inspector can be placed. win_id: The window ID this inspector is associated with. parent: The Qt parent to set. """ # Importing modules here so we don't depend on QtWebEngine without the # argument and to avoid circular imports. if objects.backend == usertypes.Backend.QtWebEngine: from qutebrowser.browser.webengine import webengineinspector if webengineinspector.supports_new(): return webengineinspector.WebEngineInspector( splitter, win_id, parent) else: return webengineinspector.LegacyWebEngineInspector( splitter, win_id, parent) elif objects.backend == usertypes.Backend.QtWebKit: from qutebrowser.browser.webkit import webkitinspector return webkitinspector.WebKitInspector(splitter, win_id, parent) raise utils.Unreachable(objects.backend)
def matches(self, other): """Check whether the given KeySequence matches with this one. We store multiple QKeySequences with <= 4 keys each, so we need to match those pair-wise, and account for an unequal amount of sequences as well. """ # pylint: disable=protected-access if len(self._sequences) > len(other._sequences): # If we entered more sequences than there are in the config, # there's no way there can be a match. return QKeySequence.NoMatch for entered, configured in zip(self._sequences, other._sequences): # If we get NoMatch/PartialMatch in a sequence, we can abort there. match = entered.matches(configured) if match != QKeySequence.ExactMatch: return match # We checked all common sequences and they had an ExactMatch. # # If there's still more sequences configured than entered, that's a # PartialMatch, as more keypresses can still follow and new sequences # will appear which we didn't check above. # # If there's the same amount of sequences configured and entered, # that's an EqualMatch. if len(self._sequences) == len(other._sequences): return QKeySequence.ExactMatch elif len(self._sequences) < len(other._sequences): return QKeySequence.PartialMatch else: raise utils.Unreachable("self={!r} other={!r}".format(self, other))
def test_config_source(self, tmp_path, commands, config_stub, config_tmpdir, location, clear): assert config_stub.val.content.javascript.enabled config_stub.val.search.ignore_case = 'always' if location == 'default': pyfile = config_tmpdir / 'config.py' arg = None elif location == 'absolute': pyfile = tmp_path / 'sourced.py' arg = str(pyfile) elif location == 'relative': pyfile = config_tmpdir / 'sourced.py' arg = 'sourced.py' else: raise utils.Unreachable(location) pyfile.write_text('\n'.join([ 'config.load_autoconfig(False)', 'c.content.javascript.enabled = False' ]), encoding='utf-8') commands.config_source(arg, clear=clear) assert not config_stub.val.content.javascript.enabled ignore_case = config_stub.val.search.ignore_case assert ignore_case == (usertypes.IgnoreCase.smart if clear else usertypes.IgnoreCase.always)
def process(tab: apitypes.Tab, pid: int = None, action: str = 'show') -> None: """Manage processes spawned by qutebrowser. Note that processes with a successful exit get cleaned up after 1h. Args: pid: The process ID of the process to manage. action: What to do with the given process: - show: Show information about the process. - terminate: Try to gracefully terminate the process (SIGTERM). - kill: Kill the process forcefully (SIGKILL). """ if pid is None: if last_pid is None: raise cmdutils.CommandError("No process executed yet!") pid = last_pid try: proc = all_processes[pid] except KeyError: raise cmdutils.CommandError(f"No process found with pid {pid}") if proc is None: raise cmdutils.CommandError(f"Data for process {pid} got cleaned up") if action == 'show': tab.load_url(QUrl(f'qute://process/{pid}')) elif action == 'terminate': proc.terminate() elif action == 'kill': proc.terminate(kill=True) else: raise utils.Unreachable(action)
def _variant() -> Variant: """Get the dark mode variant based on the underlying Qt version.""" if PYQT_WEBENGINE_VERSION is not None: # Available with Qt >= 5.13 if PYQT_WEBENGINE_VERSION >= 0x050f02: return Variant.qt_515_2 elif PYQT_WEBENGINE_VERSION == 0x050f01: return Variant.qt_515_1 elif PYQT_WEBENGINE_VERSION == 0x050f00: return Variant.qt_515_0 elif PYQT_WEBENGINE_VERSION >= 0x050e00: return Variant.qt_514 elif PYQT_WEBENGINE_VERSION >= 0x050d00: return Variant.qt_511_to_513 raise utils.Unreachable(hex(PYQT_WEBENGINE_VERSION)) # If we don't have PYQT_WEBENGINE_VERSION, we'll need to assume based on the Qt # version. assert not qtutils.version_check( # type: ignore[unreachable] '5.13', compiled=False) if qtutils.version_check('5.11', compiled=False): return Variant.qt_511_to_513 elif qtutils.version_check('5.10', compiled=False): return Variant.qt_510 return Variant.unavailable
def create(win_id: int, private: bool, parent: QWidget = None) -> 'AbstractTab': """Get a QtWebKit/QtWebEngine tab object. Args: win_id: The window ID where the tab will be shown. private: Whether the tab is a private/off the record tab. parent: The Qt parent to set. """ # Importing modules here so we don't depend on QtWebEngine without the # argument and to avoid circular imports. mode_manager = modeman.instance(win_id) if objects.backend == usertypes.Backend.QtWebEngine: from qutebrowser.browser.webengine import webenginetab tab_class: Type[AbstractTab] = webenginetab.WebEngineTab elif objects.backend == usertypes.Backend.QtWebKit: from qutebrowser.browser.webkit import webkittab tab_class = webkittab.WebKitTab else: raise utils.Unreachable(objects.backend) return tab_class(win_id=win_id, mode_manager=mode_manager, private=private, parent=parent)
def _show_dialog(*args, **kwargs): """Show a dialog for a backend problem.""" cmd_args = objreg.get('args') if cmd_args.no_err_windows: text = _error_text(*args, **kwargs) print(text, file=sys.stderr) sys.exit(usertypes.Exit.err_init) dialog = _Dialog(*args, **kwargs) status = dialog.exec_() quitter = objreg.get('quitter') if status in [_Result.quit, QDialog.Rejected]: pass elif status == _Result.restart_webkit: quitter.restart(override_args={'backend': 'webkit'}) elif status == _Result.restart_webengine: quitter.restart(override_args={'backend': 'webengine'}) elif status == _Result.restart: quitter.restart() else: raise utils.Unreachable(status) sys.exit(usertypes.Exit.err_init)
def _select_backend(config): """Select the backend for running tests. The backend is auto-selected in the following manner: 1. Use QtWebKit if available 2. Otherwise use QtWebEngine as a fallback Auto-selection is overridden by either passing a backend via `--qute-backend=<backend>` or setting the environment variable `QUTE_TESTS_BACKEND=<backend>`. Args: config: pytest config Raises: ImportError if the selected backend is not available. Returns: The selected backend as a string (e.g. 'webkit'). """ backend_arg = config.getoption('--qute-backend') backend_env = os.environ.get('QUTE_TESTS_BACKEND') backend = backend_arg or backend_env or _auto_select_backend() # Fail early if selected backend is not available if backend == 'webkit': import PyQt5.QtWebKitWidgets elif backend == 'webengine': import PyQt5.QtWebEngineWidgets else: raise utils.Unreachable(backend) return backend
def _variant() -> Variant: """Get the dark mode variant based on the underlying Qt version.""" env_var = os.environ.get('QUTE_DARKMODE_VARIANT') if env_var is not None: try: return Variant[env_var] except KeyError: log.init.warning( f"Ignoring invalid QUTE_DARKMODE_VARIANT={env_var}") if PYQT_WEBENGINE_VERSION is not None: # Available with Qt >= 5.13 if PYQT_WEBENGINE_VERSION >= 0x050f02: return Variant.qt_515_2 elif PYQT_WEBENGINE_VERSION == 0x050f01: return Variant.qt_515_1 elif PYQT_WEBENGINE_VERSION == 0x050f00: return Variant.qt_515_0 elif PYQT_WEBENGINE_VERSION >= 0x050e00: return Variant.qt_514 elif PYQT_WEBENGINE_VERSION >= 0x050d00: return Variant.qt_511_to_513 raise utils.Unreachable(hex(PYQT_WEBENGINE_VERSION)) # If we don't have PYQT_WEBENGINE_VERSION, we're on 5.12 (or older, but 5.12 is the # oldest supported version). assert not qtutils.version_check( # type: ignore[unreachable] '5.13', compiled=False) return Variant.qt_511_to_513
def queryProxy(self, query): """Get the QNetworkProxies for a query. Args: query: The QNetworkProxyQuery to get a proxy for. Return: A list of QNetworkProxy objects in order of preference. """ proxy = config.val.content.proxy if proxy is configtypes.SYSTEM_PROXY: # On Linux, use "export http_proxy=socks5://host:port" to manually # set system proxy. # ref. https://doc.qt.io/qt-5/qnetworkproxyfactory.html#systemProxyForQuery proxies = QNetworkProxyFactory.systemProxyForQuery(query) elif isinstance(proxy, pac.PACFetcher): if objects.backend == usertypes.Backend.QtWebEngine: # Looks like query.url() is always invalid on QtWebEngine... proxy = urlutils.proxy_from_url(QUrl('direct://')) assert not isinstance(proxy, pac.PACFetcher) proxies = [proxy] elif objects.backend == usertypes.Backend.QtWebKit: proxies = proxy.resolve(query) else: raise utils.Unreachable(objects.backend) else: proxies = [proxy] for proxy in proxies: self._set_capabilities(proxy) return proxies
def _process_text(self, data: QByteArray, attr: str) -> None: """Process new stdout/stderr text. Arguments: data: The new process data. attr: Either 'stdout' or 'stderr'. """ text = self._decode_data(data) if '\r' in text and not utils.is_windows: # Crude handling of CR for e.g. progress output. # Discard everything before the last \r in the new input, then discard # everything after the last \n in self.stdout/self.stderr. text = text.rsplit('\r', maxsplit=1)[-1] existing = getattr(self, attr) if '\n' in existing: new = existing.rsplit('\n', maxsplit=1)[0] + '\n' else: new = '' setattr(self, attr, new) if attr == 'stdout': self.stdout += text elif attr == 'stderr': self.stderr += text else: raise utils.Unreachable(attr)
def _partition(self): """Divide the commandline text into chunks around the cursor position. Return: ([parts_before_cursor], 'part_under_cursor', [parts_after_cursor]) """ text = self._cmd.text()[len(self._cmd.prefix()):] if not text or not text.strip(): # Only ":", empty part under the cursor with nothing before/after return [], '', [] parser = runners.CommandParser() result = parser.parse(text, fallback=True, keep=True) parts = [x for x in result.cmdline if x] pos = self._cmd.cursorPosition() - len(self._cmd.prefix()) pos = min(pos, len(text)) # Qt treats 2-byte UTF-16 chars as 2 chars log.completion.debug('partitioning {} around position {}'.format(parts, pos)) for i, part in enumerate(parts): pos -= len(part) if pos <= 0: if part[pos-1:pos+1].isspace(): # cursor is in a space between two existing words parts.insert(i, '') prefix = [x.strip() for x in parts[:i]] center = parts[i].strip() # strip trailing whitespace included as a separate token postfix = [x.strip() for x in parts[i+1:] if not x.isspace()] log.completion.debug( "partitioned: {} '{}' {}".format(prefix, center, postfix)) return prefix, center, postfix raise utils.Unreachable("Not all parts consumed: {}".format(parts))
def _handle_special_call_arg(self, *, pos, param, win_id, args, kwargs): """Check whether the argument is special, and if so, fill it in. Args: pos: The position of the argument. param: The argparse.Parameter. win_id: The window ID the command is run in. args/kwargs: The args/kwargs to fill. Return: True if it was a special arg, False otherwise. """ arg_info = self.get_arg_info(param) if pos == 0 and self._instance is not None: assert param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD self_value = self._get_objreg(win_id=win_id, name=self._instance, scope=self._scope) self._add_special_arg(value=self_value, param=param, args=args, kwargs=kwargs) return True elif arg_info.value == usertypes.CommandValue.count: if self._count is None: assert param.default is not inspect.Parameter.empty value = param.default else: value = self._count self._add_special_arg(value=value, param=param, args=args, kwargs=kwargs) return True elif arg_info.value == usertypes.CommandValue.win_id: self._add_special_arg(value=win_id, param=param, args=args, kwargs=kwargs) return True elif arg_info.value == usertypes.CommandValue.cur_tab: tab = self._get_objreg(win_id=win_id, name='tab', scope='tab') self._add_special_arg(value=tab, param=param, args=args, kwargs=kwargs) return True elif arg_info.value == usertypes.CommandValue.count_tab: self._add_count_tab(win_id=win_id, param=param, args=args, kwargs=kwargs) return True elif arg_info.value is None: pass else: raise utils.Unreachable(arg_info) return False
def resource_root(self, request): """Resource files packaged either directly or via a zip.""" if request.param == 'pathlib': request.getfixturevalue('html_path') return request.getfixturevalue('package_path') elif request.param == 'zipfile': return request.getfixturevalue('html_zip') raise utils.Unreachable(request.param)
def _backend() -> str: """Get the backend line with relevant information.""" if objects.backend == usertypes.Backend.QtWebKit: return 'new QtWebKit (WebKit {})'.format(qWebKitVersion()) elif objects.backend == usertypes.Backend.QtWebEngine: webengine = usertypes.Backend.QtWebEngine assert objects.backend == webengine, objects.backend return 'QtWebEngine (Chromium {})'.format(_chromium_version()) raise utils.Unreachable(objects.backend)
def _backend() -> str: """Get the backend line with relevant information.""" if objects.backend == usertypes.Backend.QtWebKit: return 'new QtWebKit (WebKit {})'.format(qWebKitVersion()) elif objects.backend == usertypes.Backend.QtWebEngine: return str( qtwebengine_versions( avoid_init='avoid-chromium-init' in objects.debug_flags)) raise utils.Unreachable(objects.backend)
def setText(self, text: str) -> None: """Extend setText to set prefix and make sure the prompt is ok.""" if not text: pass elif text[0] in modeparsers.STARTCHARS: super().set_prompt(text[0]) else: raise utils.Unreachable("setText got called with invalid text " "'{}'!".format(text)) super().setText(text)
def shutdown() -> None: """Shut down QWeb(Engine)Settings.""" if objects.backend == usertypes.Backend.QtWebEngine: from qutebrowser.browser.webengine import webenginesettings webenginesettings.shutdown() elif objects.backend == usertypes.Backend.QtWebKit: from qutebrowser.browser.webkit import webkitsettings webkitsettings.shutdown() else: raise utils.Unreachable(objects.backend)
def _handle_single_key(self, e): """Handle a new keypress with a single key (no modifiers). Separate the keypress into count/command, then check if it matches any possible command, and either run the command, ignore it, or display an error. Args: e: the KeyPressEvent from Qt. Return: A self.Match member. """ txt = e.text() key = e.key() self._debug_log("Got key: 0x{:x} / text: '{}'".format(key, txt)) if len(txt) == 1: category = unicodedata.category(txt) is_control_char = (category == 'Cc') else: is_control_char = False if (not txt) or is_control_char: self._debug_log("Ignoring, no text char") return self.Match.none count, cmd_input = self._split_count(self._keystring + txt) match, binding = self._match_key(cmd_input) if match == self.Match.none: mappings = config.val.bindings.key_mappings mapped = mappings.get(txt, None) if mapped is not None: txt = mapped count, cmd_input = self._split_count(self._keystring + txt) match, binding = self._match_key(cmd_input) self._keystring += txt if match == self.Match.definitive: self._debug_log("Definitive match for '{}'.".format( self._keystring)) self.clear_keystring() self.execute(binding, self.Type.chain, count) elif match == self.Match.partial: self._debug_log("No match for '{}' (added {})".format( self._keystring, txt)) elif match == self.Match.none: self._debug_log("Giving up with '{}', no matches".format( self._keystring)) self.clear_keystring() elif match == self.Match.other: pass else: raise utils.Unreachable("Invalid match value {!r}".format(match)) return match
def _fake_version(name): if name == 'PyQtWebEngine-Qt': outcome = qt elif name == 'PyQtWebEngine-Qt5': outcome = qt5 else: raise utils.Unreachable(outcome) if outcome is None: raise importlib_metadata.PackageNotFoundError(name) return outcome
def clear_private_data() -> None: """Clear cookies, cache and related data for private browsing sessions.""" if objects.backend == usertypes.Backend.QtWebEngine: from qutebrowser.browser.webengine import webenginesettings webenginesettings.init_private_profile() elif objects.backend == usertypes.Backend.QtWebKit: from qutebrowser.browser.webkit import cookies assert cookies.ram_cookie_jar is not None cookies.ram_cookie_jar.setAllCookies([]) else: raise utils.Unreachable(objects.backend)
def _init_adapter(self) -> None: """Initialize the adapter to use based on the config.""" setting = config.val.content.notifications.presenter log.misc.debug(f"Setting up notification adapter ({setting})...") if setting == "qt": message.error( "Can't switch to qt notification presenter at runtime.") setting = "auto" if setting in ["auto", "libnotify"]: candidates = [ DBusNotificationAdapter, SystrayNotificationAdapter, MessagesNotificationAdapter, ] elif setting == "systray": candidates = [ SystrayNotificationAdapter, DBusNotificationAdapter, MessagesNotificationAdapter, ] elif setting == "herbe": candidates = [ HerbeNotificationAdapter, DBusNotificationAdapter, SystrayNotificationAdapter, MessagesNotificationAdapter, ] elif setting == "messages": candidates = [MessagesNotificationAdapter] # always succeeds else: raise utils.Unreachable(setting) for candidate in candidates: try: self._adapter = candidate() except Error as e: msg = f"Failed to initialize {candidate.NAME} notification adapter: {e}" if candidate.NAME == setting: # We picked this one explicitly message.error(msg) else: # automatic fallback log.misc.debug(msg) else: log.misc.debug( f"Initialized {self._adapter.NAME} notification adapter") break assert self._adapter is not None self._adapter.click_id.connect(self._on_adapter_clicked) self._adapter.close_id.connect(self._on_adapter_closed) self._adapter.error.connect(self._on_adapter_error) self._adapter.clear_all.connect(self._on_adapter_clear_all)
def _get_version_tag(tag): """Handle tags like pyqt>=5.3.1 for BDD tests. This transforms e.g. pyqt>=5.3.1 into an appropriate @pytest.mark.skip marker, and falls back to pytest-bdd's implementation for all other casesinto an appropriate @pytest.mark.skip marker, and falls back to """ version_re = re.compile( r""" (?P<package>qt|pyqt|pyqtwebengine) (?P<operator>==|>=|!=|<) (?P<version>\d+\.\d+(\.\d+)?) """, re.VERBOSE) match = version_re.fullmatch(tag) if not match: return None package = match.group('package') version = match.group('version') if package == 'qt': op = match.group('operator') do_skip = { '==': not qtutils.version_check(version, exact=True, compiled=False), '>=': not qtutils.version_check(version, compiled=False), '<': qtutils.version_check(version, compiled=False), '!=': qtutils.version_check(version, exact=True, compiled=False), } return pytest.mark.skipif(do_skip[op], reason='Needs ' + tag) elif package == 'pyqt': return pytest.mark.skipif( not _check_hex_version(op_str=match.group('operator'), running_version=PYQT_VERSION, version=version), reason='Needs ' + tag, ) elif package == 'pyqtwebengine': try: from PyQt5.QtWebEngine import PYQT_WEBENGINE_VERSION except ImportError: running_version = PYQT_VERSION else: running_version = PYQT_WEBENGINE_VERSION return pytest.mark.skipif( not _check_hex_version(op_str=match.group('operator'), running_version=running_version, version=version), reason='Needs ' + tag, ) else: raise utils.Unreachable(package)
def send_event(self, evt): """Send the given event to the underlying widget. The event will be sent via QApplication.postEvent. Note that a posted event may not be re-used in any way! """ # This only gives us some mild protection against re-using events, but # it's certainly better than a segfault. if getattr(evt, 'posted', False): raise utils.Unreachable("Can't re-use an event which was already " "posted!") recipient = self.event_target() evt.posted = True QApplication.postEvent(recipient, evt)
def init(args: argparse.Namespace) -> None: """Initialize all QWeb(Engine)Settings.""" if objects.backend == usertypes.Backend.QtWebEngine: from qutebrowser.browser.webengine import webenginesettings webenginesettings.init() elif objects.backend == usertypes.Backend.QtWebKit: from qutebrowser.browser.webkit import webkitsettings webkitsettings.init() else: raise utils.Unreachable(objects.backend) # Make sure special URLs always get JS support for pattern in ['chrome://*/*', 'qute://*/*']: config.instance.set_obj('content.javascript.enabled', True, pattern=urlmatch.UrlPattern(pattern), hide_userconfig=True)
def _draw_widgets(self): """Draw statusbar widgets.""" self._clear_widgets() tab = self._current_tab() # Read the list and set widgets accordingly for segment in config.val.statusbar.widgets: if segment == 'url': self._hbox.addWidget(self.url) self.url.show() elif segment == 'scroll': self._hbox.addWidget(self.percentage) self.percentage.show() elif segment == 'scroll_raw': self._hbox.addWidget(self.percentage) self.percentage.set_raw() self.percentage.show() elif segment == 'history': self._hbox.addWidget(self.backforward) self.backforward.enabled = True if tab: self.backforward.on_tab_changed(tab) elif segment == 'tabs': self._hbox.addWidget(self.tabindex) self.tabindex.show() elif segment == 'keypress': self._hbox.addWidget(self.keystring) self.keystring.show() elif segment == 'progress': self._hbox.addWidget(self.prog) self.prog.enabled = True if tab: self.prog.on_tab_changed(tab) elif segment.startswith('text:'): cur_widget = textbase.TextBase() self._text_widgets.append(cur_widget) cur_widget.setText(segment.split(':', maxsplit=1)[1]) self._hbox.addWidget(cur_widget) cur_widget.show() else: raise utils.Unreachable(segment)
def send_event(self, evt: QEvent) -> None: """Send the given event to the underlying widget. The event will be sent via QApplication.postEvent. Note that a posted event must not be re-used in any way! """ # This only gives us some mild protection against re-using events, but # it's certainly better than a segfault. if getattr(evt, 'posted', False): raise utils.Unreachable("Can't re-use an event which was already " "posted!") recipient = self.private_api.event_target() if recipient is None: # https://github.com/qutebrowser/qutebrowser/issues/3888 log.webview.warning("Unable to find event target!") return evt.posted = True # type: ignore[attr-defined] QApplication.postEvent(recipient, evt)