diff options
31 files changed, 515 insertions, 148 deletions
diff --git a/.github/workflows/bleeding.yml b/.github/workflows/bleeding.yml index 766f535d7..435141e56 100644 --- a/.github/workflows/bleeding.yml +++ b/.github/workflows/bleeding.yml @@ -58,7 +58,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: "3.10" - name: Get asciidoc uses: actions/checkout@v2 with: diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index f86b84622..09096934a 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -24,6 +24,23 @@ Changed - Improved message if a spawned process wasn't found and a Flatpak container is in use. +- The `:tab-move` command now takes `start` and `end` as `index` to move a tab + to the first/last position. +- Tests now automatically pick the backend (QtWebKit/QtWebEngine) based on + what's available. The `QUTE_BDD_WEBENGINE` environment variable and + `--qute-bdd-webengine` argument got replaced by `QUTE_TESTS_BACKEND` and + `--qute-backend` respectively, which can be set to either `webengine` or + `webkit`. +- Using `:tab-give` or `:tab-take` on the last tab in a window now always + closes that window, no matter what `tabs.last_close` is set to. +- Redesigned `qute://settings` (`:set`) page with buttons for options with + fixed values. + +Added +~~~~~ + +- New `input.match_counts` option which allows to turn off count matching for + more emacs-like bindings. [[v2.4.1]] v2.4.1 (unreleased) @@ -36,6 +53,7 @@ Fixed binaries (in certain rare environments). - Speculative fix for a qutebrowser crash when the notification daemon crashes while showing the notification. +- Fix crash when using `:screenshot` with an invalid `--rect` argument. [[v2.4.0]] v2.4.0 (2021-10-21) diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 8c11e15cc..442c136a7 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -1431,6 +1431,7 @@ If neither is given, move it to the first position. ==== positional arguments * +'index'+: `+` or `-` to move relative to the current tab by count, or a default of 1 space. A tab index to move to that index. + `start` and `end` to move to the start and the end. ==== count diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 60c229078..4ca5c2517 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -269,6 +269,7 @@ |<<input.insert_mode.leave_on_load,input.insert_mode.leave_on_load>>|Leave insert mode when starting a new page load. |<<input.insert_mode.plugins,input.insert_mode.plugins>>|Switch to insert mode when clicking flash and other plugins. |<<input.links_included_in_focus_chain,input.links_included_in_focus_chain>>|Include hyperlinks in the keyboard focus chain when tabbing. +|<<input.match_counts,input.match_counts>>|Interpret number prefixes as counts for bindings. |<<input.media_keys,input.media_keys>>|Whether the underlying Chromium should handle media keys. |<<input.mouse.back_forward_buttons,input.mouse.back_forward_buttons>>|Enable back and forward buttons on the mouse. |<<input.mouse.rocker_gestures,input.mouse.rocker_gestures>>|Enable Opera-like mouse rocker gestures. @@ -3557,6 +3558,15 @@ Type: <<types,Bool>> Default: +pass:[true]+ +[[input.match_counts]] +=== input.match_counts +Interpret number prefixes as counts for bindings. +This enables for vi-like bindings that can be prefixed with a number to indicate a count. Disabling it allows for emacs-like bindings where number keys are passed through (according to `input.forward_unbound_keys`) instead. + +Type: <<types,Bool>> + +Default: +pass:[true]+ + [[input.media_keys]] === input.media_keys Whether the underlying Chromium should handle media keys. diff --git a/misc/requirements/requirements-check-manifest.txt b/misc/requirements/requirements-check-manifest.txt index 21843c4ae..c61218ba3 100644 --- a/misc/requirements/requirements-check-manifest.txt +++ b/misc/requirements/requirements-check-manifest.txt @@ -2,8 +2,8 @@ build==0.7.0 check-manifest==0.47 -packaging==21.2 +packaging==21.3 pep517==0.12.0 -pyparsing==2.4.7 +pyparsing==3.0.6 toml==0.10.2 tomli==1.2.2 diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt index 088604a77..0dd45cebc 100644 --- a/misc/requirements/requirements-dev.txt +++ b/misc/requirements/requirements-dev.txt @@ -3,18 +3,18 @@ bump2version==1.0.1 certifi==2021.10.8 cffi==1.15.0 -charset-normalizer==2.0.7 -cryptography==35.0.0 +charset-normalizer==2.0.8 +cryptography==36.0.0 Deprecated==1.2.13 github3.py==3.0.0 hunter==3.3.8 idna==3.3 jwcrypto==1.0 manhole==1.8.0 -packaging==21.2 -pycparser==2.20 +packaging==21.3 +pycparser==2.21 Pympler==0.9 -pyparsing==2.4.7 +pyparsing==3.0.6 PyQt-builder==1.12.2 python-dateutil==2.8.2 requests==2.26.0 diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt index 08b75e2bf..1d1f5eebc 100644 --- a/misc/requirements/requirements-flake8.txt +++ b/misc/requirements/requirements-flake8.txt @@ -2,7 +2,7 @@ attrs==21.2.0 flake8==4.0.1 -flake8-bugbear==21.9.2 +flake8-bugbear==21.11.28 flake8-builtins==1.5.3 flake8-comprehensions==3.7.0 flake8-copyright==0.2.2 @@ -21,4 +21,4 @@ pycodestyle==2.8.0 pydocstyle==6.1.1 pyflakes==2.4.0 six==1.16.0 -snowballstemmer==2.1.0 +snowballstemmer==2.2.0 diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt index 5aa36d659..ce64972b3 100644 --- a/misc/requirements/requirements-mypy.txt +++ b/misc/requirements/requirements-mypy.txt @@ -2,12 +2,12 @@ chardet==4.0.0 diff-cover==6.4.2 -importlib-metadata==4.8.1 +importlib-metadata==4.8.2 importlib-resources==5.4.0 inflect==5.3.0 -Jinja2==3.0.2 +Jinja2==3.0.3 jinja2-pluralize==0.3.0 -lxml==4.6.3 +lxml==4.6.4 MarkupSafe==2.0.1 mypy==0.910 mypy-extensions==0.4.3 @@ -16,6 +16,6 @@ Pygments==2.10.0 PyQt5-stubs==5.15.2.0 toml==0.10.2 types-dataclasses==0.6.1 -types-PyYAML==6.0.0 -typing-extensions==3.10.0.2 +types-PyYAML==6.0.1 +typing_extensions==4.0.0 zipp==3.6.0 diff --git a/misc/requirements/requirements-pyinstaller.txt b/misc/requirements/requirements-pyinstaller.txt index 8d5567e67..9a53c11cd 100644 --- a/misc/requirements/requirements-pyinstaller.txt +++ b/misc/requirements/requirements-pyinstaller.txt @@ -1,5 +1,5 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py altgraph==0.17.2 -pyinstaller==4.6 +pyinstaller==4.7 pyinstaller-hooks-contrib==2021.3 diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index abc6c2812..c26af6406 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -3,8 +3,8 @@ astroid==2.3.3 # rq.filter: < 2.4 certifi==2021.10.8 cffi==1.15.0 -charset-normalizer==2.0.7 -cryptography==35.0.0 +charset-normalizer==2.0.8 +cryptography==36.0.0 Deprecated==1.2.13 future==0.18.2 github3.py==3.0.0 @@ -14,7 +14,7 @@ jwcrypto==1.0 lazy-object-proxy==1.4.3 mccabe==0.6.1 pefile==2021.9.3 -pycparser==2.20 +pycparser==2.21 pylint==2.4.4 # rq.filter: < 2.5 python-dateutil==2.8.2 ./scripts/dev/pylint_checkers diff --git a/misc/requirements/requirements-pyroma.txt b/misc/requirements/requirements-pyroma.txt index 8849014be..a76402053 100644 --- a/misc/requirements/requirements-pyroma.txt +++ b/misc/requirements/requirements-pyroma.txt @@ -1,8 +1,8 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py certifi==2021.10.8 -charset-normalizer==2.0.7 -docutils==0.18 +charset-normalizer==2.0.8 +docutils==0.18.1 idna==3.3 Pygments==2.10.0 pyroma==3.2 diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt index 86553bb4c..b7f013853 100644 --- a/misc/requirements/requirements-sphinx.txt +++ b/misc/requirements/requirements-sphinx.txt @@ -3,19 +3,19 @@ alabaster==0.7.12 Babel==2.9.1 certifi==2021.10.8 -charset-normalizer==2.0.7 +charset-normalizer==2.0.8 docutils==0.17.1 idna==3.3 -imagesize==1.2.0 -Jinja2==3.0.2 +imagesize==1.3.0 +Jinja2==3.0.3 MarkupSafe==2.0.1 -packaging==21.2 +packaging==21.3 Pygments==2.10.0 -pyparsing==2.4.7 +pyparsing==3.0.6 pytz==2021.3 requests==2.26.0 -snowballstemmer==2.1.0 -Sphinx==4.2.0 +snowballstemmer==2.2.0 +Sphinx==4.3.1 sphinxcontrib-applehelp==1.0.2 sphinxcontrib-devhelp==1.0.2 sphinxcontrib-htmlhelp==2.0.0 diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 23fb69402..b15a23c08 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -3,36 +3,36 @@ attrs==21.2.0 beautifulsoup4==4.10.0 certifi==2021.10.8 -charset-normalizer==2.0.7 +charset-normalizer==2.0.8 cheroot==8.5.2 click==8.0.3 -coverage==6.1.1 +coverage==6.2 EasyProcess==0.3 execnet==1.9.0 -filelock==3.3.2 +filelock==3.4.0 Flask==2.0.2 glob2==0.7 hunter==3.3.8 -hypothesis==6.24.1 +hypothesis==6.28.1 icdiff==2.0.4 idna==3.3 iniconfig==1.1.1 itsdangerous==2.0.1 jaraco.functools==3.4.0 -# Jinja2==3.0.2 -Mako==1.1.5 +# Jinja2==3.0.3 +Mako==1.1.6 manhole==1.8.0 # MarkupSafe==2.0.1 -more-itertools==8.10.0 -packaging==21.2 +more-itertools==8.12.0 +packaging==21.3 parse==1.19.0 parse-type==0.5.2 pluggy==1.0.0 pprintpp==0.4.0 -py==1.10.0 +py==1.11.0 py-cpuinfo==8.0.0 Pygments==2.10.0 -pyparsing==2.4.7 +pyparsing==3.0.6 pytest==6.2.5 pytest-bdd==4.1.0 pytest-benchmark==3.4.1 @@ -51,7 +51,7 @@ requests==2.26.0 requests-file==1.5.1 six==1.16.0 sortedcontainers==2.4.0 -soupsieve==2.2.1 +soupsieve==2.3.1 tldextract==3.1.2 toml==0.10.2 tomli==1.2.2 diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt index 248c850c2..a2a57808b 100644 --- a/misc/requirements/requirements-tox.txt +++ b/misc/requirements/requirements-tox.txt @@ -1,17 +1,17 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -backports.entry-points-selectable==1.1.0 +backports.entry-points-selectable==1.1.1 distlib==0.3.3 -filelock==3.3.2 -packaging==21.2 +filelock==3.4.0 +packaging==21.3 pip==21.3.1 platformdirs==2.4.0 pluggy==1.0.0 -py==1.10.0 -pyparsing==2.4.7 -setuptools==58.4.0 +py==1.11.0 +pyparsing==3.0.6 +setuptools==59.4.0 six==1.16.0 toml==0.10.2 tox==3.24.4 -virtualenv==20.9.0 +virtualenv==20.10.0 wheel==0.37.0 diff --git a/misc/userscripts/password_fill b/misc/userscripts/password_fill index c46253d41..3ea8fd9f6 100755 --- a/misc/userscripts/password_fill +++ b/misc/userscripts/password_fill @@ -241,7 +241,7 @@ pass_backend() { if $GPG "${GPG_OPTS[@]}" -d "$passfile" \ | grep --max-count=1 -iE "${match_line_pattern}${url}" > /dev/null then - passfile="${passfile#$PREFIX}" + passfile="${passfile#"$PREFIX"}" passfile="${passfile#/}" files+=( "${passfile%.gpg}" ) fi @@ -250,7 +250,7 @@ pass_backend() { if ((match_filename)) ; then # add entries with matching filepath while read -r passfile ; do - passfile="${passfile#$PREFIX}" + passfile="${passfile#"$PREFIX"}" passfile="${passfile#/}" files+=( "${passfile%.gpg}" ) done < <(find -L "$PREFIX" -iname '*.gpg' | grep "$url") @@ -267,7 +267,7 @@ pass_backend() { else if [[ $line =~ $user_pattern ]] ; then # remove the matching prefix "user: " from the beginning of the line - username=${line#${BASH_REMATCH[0]}} + username=${line#"${BASH_REMATCH[0]}"} break fi fi diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index f3438aaa8..796bb2eb3 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -451,7 +451,7 @@ class CommandDispatcher: self._open(tab.url(), tab=True) if not keep: - tabbed_browser.close_tab(tab, add_undo=False) + tabbed_browser.close_tab(tab, add_undo=False, transfer=True) @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('win_id', completion=miscmodels.window) @@ -500,7 +500,8 @@ class CommandDispatcher: tabbed_browser.tabopen(self._current_url()) if not keep: self._tabbed_browser.close_tab(self._current_widget(), - add_undo=False) + add_undo=False, + transfer=True) def _back_forward(self, tab, bg, window, count, forward, index=None): """Helper function for :back/:forward.""" @@ -1004,11 +1005,10 @@ class CommandDispatcher: raise cmdutils.CommandError("There's no tab with index {}!".format( index)) - @cmdutils.register(instance='command-dispatcher', scope='window') - @cmdutils.argument('index', choices=['+', '-']) - @cmdutils.argument('count', value=cmdutils.Value.count) - def tab_move(self, index: Union[str, int] = None, - count: int = None) -> None: + @cmdutils.register(instance="command-dispatcher", scope="window") + @cmdutils.argument("index", choices=["+", "-", "start", "end"]) + @cmdutils.argument("count", value=cmdutils.Value.count) + def tab_move(self, index: Union[str, int] = None, count: int = None) -> None: """Move the current tab according to the argument and [count]. If neither is given, move it to the first position. @@ -1017,24 +1017,29 @@ class CommandDispatcher: index: `+` or `-` to move relative to the current tab by count, or a default of 1 space. A tab index to move to that index. + `start` and `end` to move to the start and the end. count: If moving relatively: Offset. If moving absolutely: New position (default: 0). This overrides the index argument, if given. """ - if index in ['+', '-']: + if index in ["+", "-"]: # relative moving new_idx = self._current_index() delta = 1 if count is None else count - if index == '-': + if index == "-": new_idx -= delta - elif index == '+': # pragma: no branch + elif index == "+": # pragma: no branch new_idx += delta if config.val.tabs.wrap: new_idx %= self._count() else: # absolute moving - if count is not None: + if index == "start": + new_idx = 0 + elif index == "end": + new_idx = self._count() - 1 + elif count is not None: new_idx = count - 1 elif index is not None: assert isinstance(index, int) diff --git a/qutebrowser/components/misccommands.py b/qutebrowser/components/misccommands.py index 120806bfe..8eaae045f 100644 --- a/qutebrowser/components/misccommands.py +++ b/qutebrowser/components/misccommands.py @@ -183,7 +183,10 @@ def screenshot( raise cmdutils.CommandError( f"File {filename} already exists (use --force to overwrite)") - qrect = None if rect is None else utils.parse_rect(rect) + try: + qrect = None if rect is None else utils.parse_rect(rect) + except ValueError as e: + raise cmdutils.CommandError(str(e)) pic = tab.grab_pixmap(qrect) if pic is None: diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index e034fe8f5..3c5dc6f2f 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -1794,6 +1794,17 @@ input.media_keys: On Linux, disabling this also disables Chromium's MPRIS integration. +input.match_counts: + default: true + type: Bool + desc: >- + Interpret number prefixes as counts for bindings. + + This enables for vi-like bindings that can be prefixed with a number to + indicate a count. + Disabling it allows for emacs-like bindings where number keys are passed + through (according to `input.forward_unbound_keys`) instead. + ## keyhint keyhint.blacklist: diff --git a/qutebrowser/html/settings.html b/qutebrowser/html/settings.html index 44824eeac..dfbc5c168 100644 --- a/qutebrowser/html/settings.html +++ b/qutebrowser/html/settings.html @@ -13,22 +13,112 @@ var cset = function(option, value) { {% endblock %} {% block style %} -table { border: 1px solid grey; border-collapse: collapse; } -pre { margin: 2px; } -th, td { border: 1px solid grey; padding: 0px 5px; } -th { background: lightgrey; } -th pre { color: grey; text-align: left; } -input { width: 98%; } -.setting { width: 75%; } -.value { width: 25%; text-align: center; } -.noscript, .noscript-text { color:red; } -.noscript-text { margin-bottom: 5cm; } -.option_description { margin: .5ex 0; color: grey; font-size: 80%; font-style: italic; white-space: pre-line; } +table { + border-spacing: 10px; +} + +tbody tr:nth-child(odd) { + background: #eaf4fb; +} + +pre { + margin: 2px; +} + +th { + padding: 10px; + border-radius: 5px; + background: #a6dfff; + text-align: left; + font-weight: normal; + font-size: 1.5rem; + color: #084c88; +} + +td { + padding: 5px 5px; +} + +th pre { + color: grey; + text-align: left; +} + +input { + padding: 8px; + width: 98%; + box-sizing: border-box; + border-radius: 4px; + border: 1px solid #01cdd0; + font-size: 0.9rem; + font-family: DejaVu, serif; +} + +input:focus { + outline: none; + border: 2px solid #7a589ea6; +} + +input[type="radio"] { + position: absolute; /* Positions the radio button relative to the edges of its containing element */ + -webkit-appearance: none; /* Removes its native styling */ + width: min-content; + margin: 0; + border: none; +} + +label { + cursor: pointer; + margin-bottom: 2px; + padding: 5px 10px; + border-radius: 5px; + background-color: #dddddd; + color: #666666; +} + +input[type="radio"]:checked + label { + background-color: #a6dfff; + color: #084c88; +} + +.setting { + width: 60%; +} + +.value { + width: 25%; + text-align: center; +} + +.valid-value { + text-align: center; +} + +.noscript, .noscript-text { + color: red; +} + +.noscript-text { + margin-bottom: 5cm; +} + +.option-description { + margin: .5ex 0; + color: #635d5dcf; + font-size: 80%; + font-style: italic; + white-space: pre-line; +} + +.radio-button { + position: relative; /* The absolutely positioned element inside this tag (the radio button) gets positioned relative to it. */ + display: inline-flex; + margin: 3px 1px; +} {% endblock %} {% block content %} <noscript><h1 class="noscript">View Only</h1><p class="noscript-text">Changing settings requires javascript to be enabled!</p></noscript> -<header><h1>{{ title }}</h1></header> <table> <tr> <th>Setting</th> @@ -37,18 +127,36 @@ input { width: 98%; } {% for option in configdata.DATA.values()|sort(attribute='name') if not option.no_autoconfig %} <tr> <!-- FIXME: convert to string properly --> - <td class="setting">{{ option.name }} (Current: {{ confget(option.name) | string |truncate(100) }}) + <td class="setting">{{ option.name }} {% if option.description %} - <p class="option_description">{{ option.description|e }}</p> + <p class="option-description">{{ option.description|e }}</p> {% endif %} </td> - <td class="value"> - <input type="text" - id="input-{{ option.name }}" - onblur="cset('{{ option.name }}', this.value)" - value="{{ confget(option.name) }}"> - </input> - </td> + {% if option.typ.valid_values is not none %} + <td class="valid-value"> + {% for value in option.typ.valid_values.values %} + <div class="radio-button"> + <input type="radio" id="input-{{ option.name }}-{{ loop.index0 }}" + name="{{ option.name }}" value="{{ value }}" + onclick="cset('{{ option.name }}', this.value)" + {% if confget(option.name) == value %} + checked + {% endif %}> + <label for="input-{{ option.name }}-{{ loop.index0 }}"> + {{ value }} + </label> + </div> + {% endfor %} + </td> + {% else %} + <td class="value"> + <input type="text" + id="input-{{ option.name }}" + onblur="cset('{{ option.name }}', this.value)" + value="{{ confget(option.name) }}"> + </input> + </td> + {% endif %} </tr> {% endfor %} </table> diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 7e688dab1..4db1d5d76 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -254,6 +254,9 @@ class BaseKeyParser(QObject): def _match_count(self, sequence: keyutils.KeySequence, dry_run: bool) -> bool: """Try to match a key as count.""" + if not config.val.input.match_counts: + return False + txt = str(sequence[-1]) # To account for sequences changed above. if (txt in string.digits and self._supports_count and not (not self._count and txt == '0')): diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index e081284ee..8c6ac2424 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -406,15 +406,16 @@ class TabbedBrowser(QWidget): else: yes_action() - def close_tab(self, tab, *, add_undo=True, new_undo=True): + def close_tab(self, tab, *, add_undo=True, new_undo=True, transfer=False): """Close a tab. Args: tab: The QWebView to be closed. add_undo: Whether the tab close can be undone. new_undo: Whether the undo entry should be a new item in the stack. + transfer: Whether the tab is closing because it is moving to a new window. """ - if config.val.tabs.tabs_are_windows: + if config.val.tabs.tabs_are_windows or transfer: last_close = 'close' else: last_close = config.val.tabs.last_close diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 8cd244fca..3beb6fb83 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -773,8 +773,6 @@ def _backend() -> str: 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 str(qtwebengine_versions( avoid_init='avoid-chromium-init' in objects.debug_flags)) raise utils.Unreachable(objects.backend) diff --git a/requirements.txt b/requirements.txt index 0805ad6cc..e088ca805 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,11 +3,11 @@ adblock==0.5.0 colorama==0.4.4 dataclasses==0.6 ; python_version<"3.7" -importlib-metadata==4.8.1 ; python_version<"3.8" +importlib-metadata==4.8.2 ; python_version<"3.8" importlib-resources==5.4.0 ; python_version<"3.9" -Jinja2==3.0.2 +Jinja2==3.0.3 MarkupSafe==2.0.1 Pygments==2.10.0 PyYAML==6.0 -typing-extensions==3.10.0.2 +typing_extensions==4.0.0 ; python_version<"3.8" zipp==3.6.0 diff --git a/scripts/dev/recompile_requirements.py b/scripts/dev/recompile_requirements.py index c013346ae..1b9759eb8 100644 --- a/scripts/dev/recompile_requirements.py +++ b/scripts/dev/recompile_requirements.py @@ -133,7 +133,7 @@ CHANGELOG_URLS = { 'six': 'https://github.com/benjaminp/six/blob/master/CHANGES', 'altgraph': 'https://github.com/ronaldoussoren/altgraph/blob/master/doc/changelog.rst', 'urllib3': 'https://github.com/urllib3/urllib3/blob/master/CHANGES.rst', - 'lxml': 'https://lxml.de/index.html#old-versions', + 'lxml': 'https://github.com/lxml/lxml/blob/master/CHANGES.txt', 'jwcrypto': 'https://github.com/latchset/jwcrypto/commits/master', 'wrapt': 'https://github.com/GrahamDumpleton/wrapt/blob/develop/docs/changes.rst', 'pep517': 'https://github.com/pypa/pep517/blob/master/doc/changelog.rst', @@ -155,11 +155,11 @@ CHANGELOG_URLS = { 'cheroot': 'https://cheroot.cherrypy.org/en/latest/history.html', 'certifi': 'https://ccadb-public.secure.force.com/mozilla/IncludedCACertificateReport', 'chardet': 'https://github.com/chardet/chardet/releases', - 'charset-normalizer': 'https://github.com/Ousret/charset_normalizer/commits/master', + 'charset-normalizer': 'https://github.com/Ousret/charset_normalizer/blob/master/CHANGELOG.md', 'idna': 'https://github.com/kjd/idna/blob/master/HISTORY.rst', 'tldextract': 'https://github.com/john-kurkowski/tldextract/blob/master/CHANGELOG.md', 'backports.entry-points-selectable': 'https://github.com/jaraco/backports.entry_points_selectable/blob/main/CHANGES.rst', - 'typing-extensions': 'https://github.com/python/typing/commits/master/typing_extensions', + 'typing_extensions': 'https://github.com/python/typing/commits/master/typing_extensions', 'diff-cover': 'https://github.com/Bachmann1234/diff_cover/blob/master/CHANGELOG', 'pytest-icdiff': 'https://github.com/hjwp/pytest-icdiff/blob/master/HISTORY.rst', 'icdiff': 'https://github.com/jeffkaufman/icdiff/blob/master/ChangeLog', diff --git a/tests/conftest.py b/tests/conftest.py index 40631af34..26cc04345 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -214,20 +214,74 @@ def pytest_addoption(parser): help="Delay between qutebrowser commands.") parser.addoption('--qute-profile-subprocs', action='store_true', default=False, help="Run cProfile for subprocesses.") - parser.addoption('--qute-bdd-webengine', action='store_true', - help='Use QtWebEngine for BDD tests') + parser.addoption('--qute-backend', action='store', + choices=['webkit', 'webengine'], help='Set backend for BDD tests') def pytest_configure(config): - webengine_arg = config.getoption('--qute-bdd-webengine') - webengine_env = os.environ.get('QUTE_BDD_WEBENGINE', 'false') - config.webengine = webengine_arg or webengine_env == 'true' - # Fail early if QtWebEngine is not available - if config.webengine: - import PyQt5.QtWebEngineWidgets + backend = _select_backend(config) + config.webengine = backend == 'webengine' + earlyinit.configure_pyqt() +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 _auto_select_backend(): + try: + # Try to use QtWebKit as the default backend + import PyQt5.QtWebKitWidgets + return 'webkit' + except ImportError: + # Try to use QtWebEngine as a fallback and fail early + # if that's also not available + import PyQt5.QtWebEngineWidgets + return 'webengine' + + +def pytest_report_header(config): + if config.webengine: + backend_version = version.qtwebengine_versions(avoid_init=True) + else: + backend_version = version.qWebKitVersion() + + return f'backend: {backend_version}' + + @pytest.fixture(scope='session', autouse=True) def check_display(request): if utils.is_linux and not os.environ.get('DISPLAY', ''): diff --git a/tests/end2end/conftest.py b/tests/end2end/conftest.py index a4a089cea..16170d460 100644 --- a/tests/end2end/conftest.py +++ b/tests/end2end/conftest.py @@ -165,7 +165,7 @@ if not getattr(sys, 'frozen', False): def pytest_collection_modifyitems(config, items): - """Apply @qtwebengine_* markers; skip unittests with QUTE_BDD_WEBENGINE.""" + """Apply @qtwebengine_* markers.""" # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-75884 # (note this isn't actually fixed properly before Qt 5.15) header_bug_fixed = qtutils.version_check('5.15', compiled=False) diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature index c9d983755..3715d5765 100644 --- a/tests/end2end/features/tabs.feature +++ b/tests/end2end/features/tabs.feature @@ -633,6 +633,27 @@ Feature: Tab management - data/numbers/1.txt (active) - data/numbers/3.txt + Scenario: :tab-move with absolute position + When I open data/numbers/1.txt + And I open data/numbers/2.txt in a new tab + And I open data/numbers/3.txt in a new tab + And I run :tab-focus 1 + And I run :tab-move end + Then the following tabs should be open: + - data/numbers/2.txt + - data/numbers/3.txt + - data/numbers/1.txt (active) + + Scenario: :tab-move with absolute position + When I open data/numbers/1.txt + And I open data/numbers/2.txt in a new tab + And I open data/numbers/3.txt in a new tab + And I run :tab-move start + Then the following tabs should be open: + - data/numbers/3.txt (active) + - data/numbers/1.txt + - data/numbers/2.txt + Scenario: Make sure :tab-move retains metadata When I open data/title.html And I open data/hello.txt in a new tab @@ -1349,6 +1370,25 @@ Feature: Tab management And I run :tab-take 0/1 Then the error "Can't take tabs when using windows as tabs" should be shown + @windows_skip + Scenario: Close the last tab of a window when taken by another window + Given I have a fresh instance + When I open data/numbers/1.txt + And I run :tab-only + And I open data/numbers/2.txt in a new window + And I set tabs.last_close to ignore + And I run :tab-take 1/1 + And I wait until data/numbers/2.txt is loaded + Then the session should look like: + windows: + - tabs: + - history: + - url: about:blank + - url: http://localhost:*/data/numbers/1.txt + - active: true + history: + - url: http://localhost:*/data/numbers/2.txt + # :tab-give @xfail_norun # Needs qutewm @@ -1406,6 +1446,24 @@ Feature: Tab management And I run :tab-give 0 Then the error "Can't give tabs when using windows as tabs" should be shown + @windows_skip + Scenario: Close the last tab of a window when given to another window + Given I have a fresh instance + When I open data/numbers/1.txt + And I run :tab-only + And I open data/numbers/2.txt in a new window + And I set tabs.last_close to ignore + And I run :tab-give 1 + And I wait until data/numbers/1.txt is loaded + Then the session should look like: + windows: + - tabs: + - active: true + history: + - url: http://localhost:*/data/numbers/2.txt + - history: + - url: http://localhost:*/data/numbers/1.txt + # Other Scenario: Using :tab-next after closing last tab (#1448) diff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py index 30ee36301..84068bf47 100644 --- a/tests/unit/keyinput/test_basekeyparser.py +++ b/tests/unit/keyinput/test_basekeyparser.py @@ -346,3 +346,14 @@ def test_clear_keystring_empty(qtbot, keyparser): keyparser._sequence = keyseq('') with qtbot.assert_not_emitted(keyparser.keystring_updated): keyparser.clear_keystring() + + +def test_respect_config_when_matching_counts(keyparser, config_stub): + """Don't match counts if disabled in the config.""" + config_stub.val.input.match_counts = False + + info = keyutils.KeyInfo(Qt.Key_1, Qt.NoModifier) + keyparser.handle(info.to_event()) + + assert not keyparser._sequence + assert not keyparser._count diff --git a/tests/unit/utils/test_urlmatch.py b/tests/unit/utils/test_urlmatch.py index 35ccc94fe..caf52c76d 100644 --- a/tests/unit/utils/test_urlmatch.py +++ b/tests/unit/utils/test_urlmatch.py @@ -37,24 +37,30 @@ from PyQt5.QtCore import QUrl from qutebrowser.utils import urlmatch +# pylint: disable=line-too-long + @pytest.mark.parametrize('pattern, error', [ ### Chromium: kMissingSchemeSeparator ## TEST(ExtensionURLPatternTest, ParseInvalid) # ("http", "No scheme given"), - ("http:", "Invalid port: Port is empty"), - ("http:/", "Invalid port: Port is empty"), - ("about://", "Pattern without path"), - ("http:/bar", "Invalid port: Port is empty"), + pytest.param("http:", "Invalid port: Port is empty", id='scheme-no-slash'), + pytest.param("http:/", "Invalid port: Port is empty", id='scheme-single-slash'), + pytest.param("about://", "Pattern without path", id='scheme-no-path'), + pytest.param( + "http:/bar", + "Invalid port: Port is empty", + id='scheme-single-slash-path', + ), ### Chromium: kEmptyHost ## TEST(ExtensionURLPatternTest, ParseInvalid) - ("http://", "Pattern without host"), - ("http:///", "Pattern without host"), - ("http://:1234/", "Pattern without host"), - ("http://*./", "Pattern without host"), + pytest.param("http://", "Pattern without host", id='host-double-slash'), + pytest.param("http:///", "Pattern without host", id='host-triple-slash'), + pytest.param("http://:1234/", "Pattern without host", id='host-port'), + pytest.param("http://*./", "Pattern without host", id='host-pattern'), ## TEST(ExtensionURLPatternTest, IPv6Patterns) - ("http://[]:8888/*", "Pattern without host"), + pytest.param("http://[]:8888/*", "Pattern without host", id='host-ipv6'), ### Chromium: kEmptyPath ## TEST(ExtensionURLPatternTest, ParseInvalid) @@ -63,53 +69,132 @@ from qutebrowser.utils import urlmatch ### Chromium: kInvalidHost ## TEST(ExtensionURLPatternTest, ParseInvalid) - ("http://\0www/", "May not contain NUL byte"), + pytest.param("http://\0www/", "May not contain NUL byte", id='host-nul'), ## TEST(ExtensionURLPatternTest, IPv6Patterns) # No closing bracket (`]`). - ("http://[2607:f8b0:4005:805::200e/*", "Invalid IPv6 URL"), + pytest.param( + "http://[2607:f8b0:4005:805::200e/*", + "Invalid IPv6 URL", + id='host-ipv6-no-closing', + ), # Two closing brackets (`]]`). - pytest.param("http://[2607:f8b0:4005:805::200e]]/*", "Invalid IPv6 URL", marks=pytest.mark.xfail(reason="https://bugs.python.org/issue34360")), + pytest.param( + "http://[2607:f8b0:4005:805::200e]]/*", + "Invalid IPv6 URL", + marks=pytest.mark.xfail(reason="https://bugs.python.org/issue34360"), + id='host-ipv6-two-closing', + ), # Two open brackets (`[[`). - ("http://[[2607:f8b0:4005:805::200e]/*", r"""Expected '\]' to match '\[' in hostname; source was "\[2607:f8b0:4005:805::200e"; host = """""), + pytest.param( + "http://[[2607:f8b0:4005:805::200e]/*", + r"""Expected '\]' to match '\[' in hostname; source was "\[2607:f8b0:4005:805::200e"; host = """"", + id='host-ipv6-two-open', + ), # Too few colons in the last chunk. - ("http://[2607:f8b0:4005:805:200e]/*", 'Invalid IPv6 address; source was "2607:f8b0:4005:805:200e"; host = ""'), + pytest.param( + "http://[2607:f8b0:4005:805:200e]/*", + 'Invalid IPv6 address; source was "2607:f8b0:4005:805:200e"; host = ""', + id='host-ipv6-colons', + ), # Non-hex piece. - ("http://[2607:f8b0:4005:805:200e:12:bogus]/*", 'Invalid IPv6 address; source was "2607:f8b0:4005:805:200e:12:bogus"; host = ""'), + pytest.param( + "http://[2607:f8b0:4005:805:200e:12:bogus]/*", + 'Invalid IPv6 address; source was "2607:f8b0:4005:805:200e:12:bogus"; host = ""', + id='host-ipv6-non-hex', + ), ### Chromium: kInvalidHostWildcard ## TEST(ExtensionURLPatternTest, ParseInvalid) - ("http://*foo/bar", "Invalid host wildcard"), - ("http://foo.*.bar/baz", "Invalid host wildcard"), - ("http://fo.*.ba:123/baz", "Invalid host wildcard"), - ("http://foo.*/bar", "Invalid host wildcard"), + pytest.param("http://*foo/bar", "Invalid host wildcard", id='host-wildcard-no-dot'), + pytest.param( + "http://foo.*.bar/baz", + "Invalid host wildcard", + id='host-wildcard-middle', + ), + pytest.param( + "http://fo.*.ba:123/baz", + "Invalid host wildcard", + id='host-wildcard-middle-port', + ), + pytest.param("http://foo.*/bar", "Invalid host wildcard", id='host-wildcard-end'), ### Chromium: kInvalidPort ## TEST(ExtensionURLPatternTest, Ports) - ("http://foo:/", "Invalid port: Port is empty"), - ("http://*.foo:/", "Invalid port: Port is empty"), - ("http://foo:com/", "Invalid port: .* 'com'"), - ("http://foo:123456/", "Invalid port: Port out of range 0-65535"), - ("http://foo:80:80/monkey", "Invalid port: .* '80:80'"), - ("chrome://foo:1234/bar", "Ports are unsupported with chrome scheme"), + pytest.param("http://foo:/", "Invalid port: Port is empty", id='port-empty'), + pytest.param( + "http://*.foo:/", + "Invalid port: Port is empty", + id='port-empty-wildcard', + ), + pytest.param("http://foo:com/", "Invalid port: .* 'com'", id='port-alpha'), + pytest.param( + "http://foo:123456/", + "Invalid port: Port out of range 0-65535", + id='port-range', + ), + pytest.param( + "http://foo:80:80/monkey", + "Invalid port: .* '80:80'", + id='port-double', + ), + pytest.param( + "chrome://foo:1234/bar", + "Ports are unsupported with chrome scheme", + id='port-chrome', + ), # No port specified, but port separator. - ("http://[2607:f8b0:4005:805::200e]:/*", "Invalid port: Port is empty"), + pytest.param( + "http://[2607:f8b0:4005:805::200e]:/*", + "Invalid port: Port is empty", + id='port-empty-ipv6', + ), ### Additional tests - ("http://[", "Invalid IPv6 URL"), - ("http://[fc2e::bb88::edac]", 'Invalid IPv6 address; source was "fc2e::bb88::edac"; host = ""'), - ("http://[fc2e:0e35:bb88::edac:fc2e:0e35:bb88:edac]", 'Invalid IPv6 address; source was "fc2e:0e35:bb88::edac:fc2e:0e35:bb88:edac"; host = ""'), - ("http://[fc2e:0e35:bb88:af:edac:fc2e:0e35:bb88:edac]", 'Invalid IPv6 address; source was "fc2e:0e35:bb88:af:edac:fc2e:0e35:bb88:edac"; host = ""'), - ("http://[127.0.0.1:fc2e::bb88:edac]", r'Invalid IPv6 address; source was "127\.0\.0\.1:fc2e::bb88:edac'), - ("http://[fc2e::bb88", "Invalid IPv6 URL"), - ("http://[fc2e:bb88:edac]", 'Invalid IPv6 address; source was "fc2e:bb88:edac"; host = ""'), - ("http://[fc2e:bb88:edac::z]", 'Invalid IPv6 address; source was "fc2e:bb88:edac::z"; host = ""'), - ("http://[fc2e:bb88:edac::2]:2a2", "Invalid port: .* '2a2'"), - ("://", "Missing scheme"), + pytest.param("http://[", "Invalid IPv6 URL", id='ipv6-single-open'), + pytest.param( + "http://[fc2e::bb88::edac]", + 'Invalid IPv6 address; source was "fc2e::bb88::edac"; host = ""', + id='ipv6-double-double', + ), + pytest.param( + "http://[fc2e:0e35:bb88::edac:fc2e:0e35:bb88:edac]", + 'Invalid IPv6 address; source was "fc2e:0e35:bb88::edac:fc2e:0e35:bb88:edac"; host = ""', + id='ipv6-long-double', + ), + pytest.param( + "http://[fc2e:0e35:bb88:af:edac:fc2e:0e35:bb88:edac]", + 'Invalid IPv6 address; source was "fc2e:0e35:bb88:af:edac:fc2e:0e35:bb88:edac"; host = ""', + id='ipv6-long', + ), + pytest.param( + "http://[127.0.0.1:fc2e::bb88:edac]", + r'Invalid IPv6 address; source was "127\.0\.0\.1:fc2e::bb88:edac', + id='ipv6-ipv4', + ), + pytest.param("http://[fc2e::bb88", "Invalid IPv6 URL", id='ipv6-trailing'), + pytest.param( + "http://[fc2e:bb88:edac]", + 'Invalid IPv6 address; source was "fc2e:bb88:edac"; host = ""', + id='ipv6-short', + ), + pytest.param( + "http://[fc2e:bb88:edac::z]", + 'Invalid IPv6 address; source was "fc2e:bb88:edac::z"; host = ""', + id='ipv6-z', + ), + pytest.param( + "http://[fc2e:bb88:edac::2]:2a2", + "Invalid port: .* '2a2'", + id='ipv6-port', + ), + pytest.param("://", "Missing scheme", id='scheme-naked'), ]) def test_invalid_patterns(pattern, error): with pytest.raises(urlmatch.ParseError, match=error): urlmatch.UrlPattern(pattern) +# pylint: enable=line-too-long + @pytest.mark.parametrize('host', ['.', ' ', ' .', '. ', '. .', '. . .', ' . ']) def test_whitespace_hosts(host): diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py index 6c57cb3d3..1ffbe3c09 100644 --- a/tests/unit/utils/test_version.py +++ b/tests/unit/utils/test_version.py @@ -484,17 +484,20 @@ class TestGitStrSubprocess: @needs_git def test_real_git(self, git_repo): """Test with a real git repository.""" - branch_name = subprocess.run( - ['git', 'config', 'init.defaultBranch'], - check=False, - stdout=subprocess.PIPE, - encoding='utf-8', - ).stdout.strip() - if not branch_name: - branch_name = 'master' + def _get_git_setting(name, default): + return subprocess.run( + ['git', 'config', '--default', default, name], + check=True, + stdout=subprocess.PIPE, + encoding='utf-8', + ).stdout.strip() ret = version._git_str_subprocess(str(git_repo)) - assert ret == f'6e4b65a on {branch_name} (1970-01-01 01:00:00 +0100)' + branch_name = _get_git_setting('init.defaultBranch', 'master') + abbrev_length = int(_get_git_setting('core.abbrev', '7')) + expected_sha = '6e4b65a529c0ab78fb370c1527d5809f7436b8f3'[:abbrev_length] + + assert ret == f'{expected_sha} on {branch_name} (1970-01-01 01:00:00 +0100)' def test_missing_dir(self, tmp_path): """Test with a directory which doesn't exist.""" @@ -13,7 +13,6 @@ minversion = 3.15 setenv = PYTEST_QT_API=pyqt5 pyqt{,512,513,514,515,5150}: LINK_PYQT_SKIP=true - pyqt{,512,513,514,515,5150}: QUTE_BDD_WEBENGINE=true cov: PYTEST_ADDOPTS=--cov --cov-report xml --cov-report=html --cov-report= passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER CI XDG_* QUTE_* DOCKER QT_QUICK_BACKEND PY_COLORS DBUS_SESSION_BUS_ADDRESS basepython = @@ -42,7 +41,6 @@ commands = basepython = {env:PYTHON:python3} setenv = PYTEST_QT_API=pyqt5 - QUTE_BDD_WEBENGINE=true pip_pre = true deps = -r{toxinidir}/misc/requirements/requirements-tests-bleeding.txt commands_pre = pip install --index-url https://www.riverbankcomputing.com/pypi/simple/ --pre --upgrade PyQt5 PyQtWebEngine |