diff options
110 files changed, 869 insertions, 464 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: @@ -5,15 +5,19 @@ ignore=resources.py extension-pkg-whitelist=PyQt5,sip load-plugins=qute_pylint.config, qute_pylint.modeline, - qute_pylint.openencoding, pylint.extensions.docstyle, pylint.extensions.emptystring, - pylint.extensions.broad_try_clause, pylint.extensions.overlapping_exceptions, -persistent=n + pylint.extensions.code_style, + pylint.extensions.comparison_placement, + pylint.extensions.for_any_all, + pylint.extensions.docstyle, + pylint.extensions.check_elif, + pylint.extensions.typing, + pylint.extensions.docparams, -[broad_try_clause] -max-try-statements=7 +persistent=n +py-version=3.6 [MESSAGES CONTROL] enable=all @@ -46,7 +50,16 @@ disable=locally-disabled, too-many-statements, too-few-public-methods, import-outside-toplevel, - bad-continuation # This lint disagrees with Black + bad-continuation, # This lint disagrees with Black + consider-using-f-string, + logging-fstring-interpolation, + raise-missing-from, + consider-using-tuple, + consider-using-namedtuple-or-dataclass, + missing-raises-doc, + missing-type-doc, + missing-param-doc, + useless-param-doc, [BASIC] function-rgx=[a-z_][a-z0-9_]{2,50}$ @@ -58,6 +71,7 @@ argument-rgx=[a-z_][a-z0-9_]{0,30}$ variable-rgx=[a-z_][a-z0-9_]{0,30}$ docstring-min-length=3 no-docstring-rgx=(^_|^main$) +class-const-naming-style = snake_case [FORMAT] max-line-length=88 diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index f86b84622..52198d4eb 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -24,6 +24,34 @@ 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. +- The default `hint.selectors` now match more ARIA roles (`tab`, `checkbox`, + `menuitem`, `menuitemcheckbox` and `menuitemradio`). + +Added +~~~~~ + +- New `input.match_counts` option which allows to turn off count matching for + more emacs-like bindings. + +Fixed +~~~~~ + +- When `search.incremental` is disabled, searching using `/text` followed by a + backwards search via `?text` (or vice-versa) now correctly changes the search + direction. +- Elements getting a hint due to a `tabindex` now are skipped if it's set to + `-1`, reducing some false-positives. [[v2.4.1]] v2.4.1 (unreleased) @@ -36,6 +64,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..7435fd31f 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. @@ -3424,11 +3425,16 @@ Default: * +pass:[[role="link"\]]+ * +pass:[[role="option"\]]+ * +pass:[[role="button"\]]+ +* +pass:[[role="tab"\]]+ +* +pass:[[role="checkbox"\]]+ +* +pass:[[role="menuitem"\]]+ +* +pass:[[role="menuitemcheckbox"\]]+ +* +pass:[[role="menuitemradio"\]]+ * +pass:[[ng-click\]]+ * +pass:[[ngClick\]]+ * +pass:[[data-ng-click\]]+ * +pass:[[x-ng-click\]]+ -* +pass:[[tabindex\]]+ +* +pass:[[tabindex\]:not([tabindex="-1"\])]+ - +pass:[images]+: * +pass:[img]+ @@ -3557,6 +3563,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..126924092 100644 --- a/misc/requirements/requirements-dev.txt +++ b/misc/requirements/requirements-dev.txt @@ -1,26 +1,45 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py +bleach==4.1.0 +build==0.7.0 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 +colorama==0.4.4 +cryptography==36.0.0 Deprecated==1.2.13 +docutils==0.18.1 github3.py==3.0.0 hunter==3.3.8 idna==3.3 +importlib-metadata==4.8.2 +jeepney==0.7.1 jwcrypto==1.0 +keyring==23.4.0 manhole==1.8.0 -packaging==21.2 -pycparser==2.20 +packaging==21.3 +pep517==0.12.0 +pkginfo==1.8.2 +pycparser==2.21 +Pygments==2.10.0 Pympler==0.9 -pyparsing==2.4.7 +pyparsing==3.0.6 PyQt-builder==1.12.2 python-dateutil==2.8.2 +readme-renderer==30.0 requests==2.26.0 -sip==6.4.0 +requests-toolbelt==0.9.1 +rfc3986==1.5.0 +SecretStorage==3.3.1 +sip==6.5.0 six==1.16.0 toml==0.10.2 +tomli==1.2.2 +tqdm==4.62.3 +twine==3.7.0 uritemplate==4.1.1 # urllib3==1.26.7 +webencodings==0.5.1 wrapt==1.13.3 +zipp==3.6.0 diff --git a/misc/requirements/requirements-dev.txt-raw b/misc/requirements/requirements-dev.txt-raw index fd840bab1..261f4459f 100644 --- a/misc/requirements/requirements-dev.txt-raw +++ b/misc/requirements/requirements-dev.txt-raw @@ -4,6 +4,8 @@ github3.py bump2version requests pyqt-builder +build +twine # Already included via test requirements #@ ignore: urllib3 diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt index 08b75e2bf..fc7568e9a 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.29 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..367e039b7 100644 --- a/misc/requirements/requirements-mypy.txt +++ b/misc/requirements/requirements-mypy.txt @@ -1,13 +1,11 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py chardet==4.0.0 -diff-cover==6.4.2 -importlib-metadata==4.8.1 +diff-cover==6.4.3 +importlib-metadata==4.8.2 importlib-resources==5.4.0 -inflect==5.3.0 -Jinja2==3.0.2 -jinja2-pluralize==0.3.0 -lxml==4.6.3 +Jinja2==3.0.3 +lxml==4.6.4 MarkupSafe==2.0.1 mypy==0.910 mypy-extensions==0.4.3 @@ -16,6 +14,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.1 zipp==3.6.0 diff --git a/misc/requirements/requirements-pyinstaller.txt b/misc/requirements/requirements-pyinstaller.txt index 8d5567e67..b7c84e3be 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-hooks-contrib==2021.3 +pyinstaller==4.7 +pyinstaller-hooks-contrib==2021.4 diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index abc6c2812..d5247145d 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -1,26 +1,29 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -astroid==2.3.3 # rq.filter: < 2.4 +astroid==2.9.0 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 idna==3.3 -isort==4.3.21 +isort==5.10.1 jwcrypto==1.0 -lazy-object-proxy==1.4.3 +lazy-object-proxy==1.6.0 mccabe==0.6.1 pefile==2021.9.3 -pycparser==2.20 -pylint==2.4.4 # rq.filter: < 2.5 +platformdirs==2.4.0 +pycparser==2.21 +pylint==2.12.1 python-dateutil==2.8.2 ./scripts/dev/pylint_checkers requests==2.26.0 six==1.16.0 -typed-ast==1.4.3 ; python_version<"3.8" +toml==0.10.2 +typed-ast==1.5.0 ; python_version<"3.8" +typing_extensions==4.0.1 uritemplate==4.1.1 # urllib3==1.26.7 -wrapt==1.11.2 +wrapt==1.13.3 diff --git a/misc/requirements/requirements-pylint.txt-raw b/misc/requirements/requirements-pylint.txt-raw index ccee2ac10..9a2498267 100644 --- a/misc/requirements/requirements-pylint.txt-raw +++ b/misc/requirements/requirements-pylint.txt-raw @@ -1,4 +1,4 @@ -pylint<2.5 +pylint ./scripts/dev/pylint_checkers requests github3.py @@ -7,8 +7,6 @@ pefile # fix qute-pylint location #@ replace: qute-pylint.* ./scripts/dev/pylint_checkers #@ markers: typed-ast python_version<"3.8" -#@ filter: pylint < 2.5 -#@ filter: astroid < 2.4 # Already included via test requirements #@ ignore: urllib3 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..4a6eaeacc 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.30.0 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/browsertab.py b/qutebrowser/browser/browsertab.py index b1827dbf4..661c5f68b 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -779,6 +779,7 @@ class AbstractAudio(QObject): """Set this tab as muted or not. Arguments: + muted: Whether the tab is currently muted. override: If set to True, muting/unmuting was done manually and overrides future automatic mute/unmute changes based on the URL. diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index f3438aaa8..395d8e8a4 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,30 @@ 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: + # pylint: disable=else-if-used # 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) @@ -1515,6 +1521,7 @@ class CommandDispatcher: Callback for GUIProcess when the edited text was updated. Args: + ed: The editor.ExternalEditor instance elem: The WebElementWrapper which was modified. text: The new text to insert. """ diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index 4f7897c9d..32bfd2693 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -1352,6 +1352,7 @@ class TempDownloadManager: The tempfile.TemporaryDirectory that is used. """ if self._tmpdir is None: + # pylint: disable=consider-using-with self._tmpdir = tempfile.TemporaryDirectory( prefix='qutebrowser-downloads-') return self._tmpdir @@ -1373,6 +1374,7 @@ class TempDownloadManager: suggested_name = utils.sanitize_filename(suggested_name) # Make sure that the filename is not too long suggested_name = utils.elide_filename(suggested_name, 50) + # pylint: disable=consider-using-with fobj = tempfile.NamedTemporaryFile(dir=tmpdir.name, delete=False, suffix='_' + suggested_name) self.files.append(fobj) diff --git a/qutebrowser/browser/greasemonkey.py b/qutebrowser/browser/greasemonkey.py index d0245937f..5abb9a137 100644 --- a/qutebrowser/browser/greasemonkey.py +++ b/qutebrowser/browser/greasemonkey.py @@ -324,6 +324,7 @@ class GreasemonkeyManager(QObject): """Add a GreasemonkeyScript to this manager. Args: + script: The GreasemonkeyScript to add. force: Fetch and overwrite any dependencies which are already locally cached. """ diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 6ac44adbc..2e4e8e4b4 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -299,7 +299,7 @@ class HintActions: Args: elem: The QWebElement to download. - _context: The HintContext to use. + context: The HintContext to use. """ url = elem.resolve_url(context.baseurl) if url is None: @@ -596,6 +596,7 @@ class HintManager(QObject): "'args' is required with target userscript/spawn/run/" "fill.") else: + # pylint: disable=else-if-used if args: raise cmdutils.CommandError( "'args' is only allowed with target userscript/spawn.") @@ -870,12 +871,11 @@ class HintManager(QObject): label.update_text(matched, rest) # Show label again if it was hidden before label.show() - else: + elif (not self._context.rapid or + config.val.hints.hide_unmatched_rapid_hints): # element doesn't match anymore -> hide it, unless in rapid # mode and hide_unmatched_rapid_hints is false (see #1799) - if (not self._context.rapid or - config.val.hints.hide_unmatched_rapid_hints): - label.hide() + label.hide() except webelem.Error: pass self._handle_auto_follow(keystr=keystr) @@ -1154,7 +1154,6 @@ class WordHinter: from the words arg as fallback. Args: - words: Words to use as fallback when no link text can be used. elems: The elements to get hint strings for. Return: diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py index 559992327..d2046345f 100644 --- a/qutebrowser/browser/history.py +++ b/qutebrowser/browser/history.py @@ -405,6 +405,7 @@ class WebHistory(sql.SqlTable): Args: url: A url (as QUrl) to add to the history. + title: The tab title to add. redirect: Whether the entry was redirected to another URL (hidden in completion) atime: Override the atime used to add the entry diff --git a/qutebrowser/browser/navigate.py b/qutebrowser/browser/navigate.py index 82bf57136..6217c8d00 100644 --- a/qutebrowser/browser/navigate.py +++ b/qutebrowser/browser/navigate.py @@ -92,9 +92,6 @@ def incdec(url, count, inc_or_dec): url: The current url. count: How much to increment or decrement by. inc_or_dec: Either 'increment' or 'decrement'. - tab: Whether to open the link in a new tab. - background: Open the link in a new background tab. - window: Open the link in a new window. """ urlutils.ensure_valid(url) segments: Optional[Set[str]] = ( diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py index f048d293d..8adb7ea20 100644 --- a/qutebrowser/browser/qtnetworkdownloads.py +++ b/qutebrowser/browser/qtnetworkdownloads.py @@ -110,6 +110,7 @@ class DownloadItem(downloads.AbstractDownloadItem): """Create a file object using the internal filename.""" assert self._filename is not None try: + # pylint: disable=consider-using-with fileobj = open(self._filename, 'wb') except OSError as e: self._die(e.strerror) @@ -502,6 +503,7 @@ class DownloadManager(downloads.AbstractDownloadManager): Args: request: The QNetworkRequest to download. target: Where to save the download as downloads.DownloadTarget. + suggested_fn: The filename to use for the file. **kwargs: Passed to _fetch_request. Return: @@ -546,6 +548,9 @@ class DownloadManager(downloads.AbstractDownloadManager): target: Where to save the download as downloads.DownloadTarget. auto_remove: Whether to remove the download even if downloads.remove_finished is set to -1. + suggested_filename: The filename to use for the file. + prompt_download_directory: Whether to prompt for a location to + download the file to. Return: The created DownloadItem. diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py index 8d3ebe730..41c971642 100644 --- a/qutebrowser/browser/shared.py +++ b/qutebrowser/browser/shared.py @@ -418,12 +418,11 @@ def choose_file(qb_mode: FileSelectionMode) -> List[str]: }[qb_mode] use_tmp_file = any('{}' in arg for arg in command[1:]) if use_tmp_file: - handle = tempfile.NamedTemporaryFile( + with tempfile.NamedTemporaryFile( prefix='qutebrowser-fileselect-', delete=False, - ) - handle.close() - tmpfilename = handle.name + ) as handle: + tmpfilename = handle.name with utils.cleanup_file(tmpfilename): command = ( command[:1] + @@ -509,6 +508,7 @@ def _validated_selected_files( ) continue else: + # pylint: disable=else-if-used if not os.path.isfile(selected_file): message.warning( f"Expected file but got folder, ignoring '{selected_file}'" diff --git a/qutebrowser/browser/signalfilter.py b/qutebrowser/browser/signalfilter.py index 0b002e345..88ac4a65d 100644 --- a/qutebrowser/browser/signalfilter.py +++ b/qutebrowser/browser/signalfilter.py @@ -88,6 +88,7 @@ class SignalFilter(QObject): debug.dbg_signal(signal, args), tabidx)) signal.emit(*args) else: + # pylint: disable=else-if-used if log_signal: log.signals.debug("ignoring: {} (tab {})".format( debug.dbg_signal(signal, args), tabidx)) diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py index 9ec29ce07..05b0eadb3 100644 --- a/qutebrowser/browser/webelem.py +++ b/qutebrowser/browser/webelem.py @@ -132,6 +132,7 @@ class AbstractWebElement(collections.abc.MutableMapping): # type: ignore[type-a """Dispatch an event to the element. Args: + event: The name of the event. bubbles: Whether this event should bubble. cancelable: Whether this event can be cancelled. composed: Whether the event will trigger listeners outside of a @@ -160,9 +161,6 @@ class AbstractWebElement(collections.abc.MutableMapping): # type: ignore[type-a def is_content_editable(self) -> bool: """Check if an element has a contenteditable attribute. - Args: - elem: The QWebElement to check. - Return: True if the element has a contenteditable attribute, False otherwise. @@ -233,6 +231,7 @@ class AbstractWebElement(collections.abc.MutableMapping): # type: ignore[type-a 'span': ['cm-'], # Jupyter Notebook } relevant_classes = classes[self.tag_name()] + # pylint: disable=consider-using-any-or-all for klass in self.classes(): if any(klass.strip().startswith(e) for e in relevant_classes): return True diff --git a/qutebrowser/browser/webengine/notification.py b/qutebrowser/browser/webengine/notification.py index f8e1a59b1..e943e44e9 100644 --- a/qutebrowser/browser/webengine/notification.py +++ b/qutebrowser/browser/webengine/notification.py @@ -722,7 +722,7 @@ class DBusNotificationAdapter(AbstractNotificationAdapter): } def __init__(self, parent: QObject = None) -> None: - super().__init__(bridge) + super().__init__(parent) assert _notifications_supported() if utils.is_windows: diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 4434d5302..e02b23d16 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -201,6 +201,14 @@ class WebEngineSearch(browsertab.AbstractSearch): def _empty_flags(self): return QWebEnginePage.FindFlags(0) # type: ignore[call-overload] + def _args_to_flags(self, reverse, ignore_case): + flags = self._empty_flags() + if self._is_case_sensitive(ignore_case): + flags |= QWebEnginePage.FindCaseSensitively + if reverse: + flags |= QWebEnginePage.FindBackward + return flags + def connect_signals(self): self._wrap_handler.connect_signal(self._widget.page()) @@ -247,17 +255,14 @@ class WebEngineSearch(browsertab.AbstractSearch): # Don't go to next entry on duplicate search if self.text == text and self.search_displayed: log.webview.debug("Ignoring duplicate search request" - " for {}".format(text)) + " for {}, but resetting flags".format(text)) + self._flags = self._args_to_flags(reverse, ignore_case) return self.text = text - self._flags = self._empty_flags() + self._flags = self._args_to_flags(reverse, ignore_case) self._wrap_handler.reset_match_data() self._wrap_handler.flag_wrap = wrap - if self._is_case_sensitive(ignore_case): - self._flags |= QWebEnginePage.FindCaseSensitively - if reverse: - self._flags |= QWebEnginePage.FindBackward self._find(text, self._flags, result_cb, 'search') diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index df3491ec2..7a41b995c 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -108,6 +108,16 @@ class WebKitSearch(browsertab.AbstractSearch): def _empty_flags(self): return QWebPage.FindFlags(0) # type: ignore[call-overload] + def _args_to_flags(self, reverse, ignore_case, wrap): + flags = self._empty_flags() + if self._is_case_sensitive(ignore_case): + flags |= QWebPage.FindCaseSensitively + if reverse: + flags |= QWebPage.FindBackward + if wrap: + flags |= QWebPage.FindWrapsAroundDocument + return flags + def _call_cb(self, callback, found, text, flags, caller): """Call the given callback if it's non-None. @@ -150,7 +160,8 @@ class WebKitSearch(browsertab.AbstractSearch): # Don't go to next entry on duplicate search if self.text == text and self.search_displayed: log.webview.debug("Ignoring duplicate search request" - " for {}".format(text)) + " for {}, but resetting flags".format(text)) + self._flags = self._args_to_flags(reverse, ignore_case, wrap) return # Clear old search results, this is done automatically on QtWebEngine. @@ -158,13 +169,7 @@ class WebKitSearch(browsertab.AbstractSearch): self.text = text self.search_displayed = True - self._flags = self._empty_flags() - if self._is_case_sensitive(ignore_case): - self._flags |= QWebPage.FindCaseSensitively - if reverse: - self._flags |= QWebPage.FindBackward - if wrap: - self._flags |= QWebPage.FindWrapsAroundDocument + self._flags = self._args_to_flags(reverse, ignore_case, wrap) # We actually search *twice* - once to highlight everything, then again # to get a mark so we can navigate. found = self._widget.findText(text, self._flags) diff --git a/qutebrowser/browser/webkit/webview.py b/qutebrowser/browser/webkit/webview.py index 0242bed0c..289e29920 100644 --- a/qutebrowser/browser/webkit/webview.py +++ b/qutebrowser/browser/webkit/webview.py @@ -152,9 +152,6 @@ class WebView(QWebView): Args: e: The QPaintEvent. - - Return: - The superclass event return value. """ frame = self.page().mainFrame() new_pos = (frame.scrollBarValue(Qt.Horizontal), @@ -186,9 +183,6 @@ class WebView(QWebView): Args: e: The QShowEvent. - - Return: - The superclass event return value. """ super().showEvent(e) self.page().setVisibilityState(QWebPage.VisibilityStateVisible) @@ -198,9 +192,6 @@ class WebView(QWebView): Args: e: The QHideEvent. - - Return: - The superclass event return value. """ super().hideEvent(e) self.page().setVisibilityState(QWebPage.VisibilityStateHidden) diff --git a/qutebrowser/commands/argparser.py b/qutebrowser/commands/argparser.py index f8f083b72..2a11589f9 100644 --- a/qutebrowser/commands/argparser.py +++ b/qutebrowser/commands/argparser.py @@ -101,7 +101,7 @@ def type_conv(param, typ, value, *, str_choices=None): Args: param: The argparse.Parameter we're checking - types: The allowed type + typ: The allowed type value: The value to convert str_choices: The allowed choices if the type ends up being a string diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 653f551ff..eee5b7cde 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -272,11 +272,10 @@ class Command: if is_bool: kwargs['action'] = 'store_true' + elif arg_info.metavar is not None: + kwargs['metavar'] = arg_info.metavar else: - if arg_info.metavar is not None: - kwargs['metavar'] = arg_info.metavar - else: - kwargs['metavar'] = argparser.arg_name(param.name) + kwargs['metavar'] = argparser.arg_name(param.name) if param.kind == inspect.Parameter.VAR_POSITIONAL: kwargs['nargs'] = '*' if self._star_args_optional else '+' @@ -320,9 +319,8 @@ class Command: self.opt_args[param.name] = long_flag, short_flag if not is_bool: self.flags_with_args += [short_flag, long_flag] - else: - if not arg_info.hide: - self.pos_args.append((param.name, name)) + elif not arg_info.hide: + self.pos_args.append((param.name, name)) return args def _get_type(self, param): diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py index 70c639207..8282aa7c7 100644 --- a/qutebrowser/commands/userscripts.py +++ b/qutebrowser/commands/userscripts.py @@ -342,9 +342,8 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner): self._kwargs = kwargs try: - handle = tempfile.NamedTemporaryFile(delete=False) - handle.close() - self._filepath = handle.name + with tempfile.NamedTemporaryFile(delete=False) as handle: + self._filepath = handle.name except OSError as e: message.error("Error while creating tempfile: {}".format(e)) return diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py index 778333854..fb61e48fb 100644 --- a/qutebrowser/completion/completer.py +++ b/qutebrowser/completion/completer.py @@ -39,7 +39,7 @@ class CompletionInfo: """Context passed into all completion functions.""" config: config.Config - keyconf: config.KeyConfig + keyconf: config.KeyConfig # pylint: disable=undefined-variable win_id: int cur_tab: 'browsertab.AbstractTab' diff --git a/qutebrowser/completion/models/completionmodel.py b/qutebrowser/completion/models/completionmodel.py index 81f4bba8e..236b25533 100644 --- a/qutebrowser/completion/models/completionmodel.py +++ b/qutebrowser/completion/models/completionmodel.py @@ -70,6 +70,7 @@ class CompletionModel(QAbstractItemModel): Args: index: The QModelIndex to get item flags for. + role: The Qt ItemRole to get the data for. Return: The item data, or None on an invalid index. """ diff --git a/qutebrowser/completion/models/configmodel.py b/qutebrowser/completion/models/configmodel.py index 736d09644..7c8473b3f 100644 --- a/qutebrowser/completion/models/configmodel.py +++ b/qutebrowser/completion/models/configmodel.py @@ -78,7 +78,7 @@ def value(optname, *values, info): Args: optname: The name of the config option this model shows. - values: The values already provided on the command line. + *values: The values already provided on the command line. info: A CompletionInfo instance. """ model = completionmodel.CompletionModel(column_widths=(30, 70, 0)) @@ -137,6 +137,7 @@ def bind(key, *, info): Args: key: the key being bound. + info: A CompletionInfo instance. """ model = completionmodel.CompletionModel(column_widths=(20, 60, 20)) data = _bind_current_default(key, info) diff --git a/qutebrowser/completion/models/miscmodels.py b/qutebrowser/completion/models/miscmodels.py index 7642bf904..d8ebafb29 100644 --- a/qutebrowser/completion/models/miscmodels.py +++ b/qutebrowser/completion/models/miscmodels.py @@ -230,9 +230,7 @@ def _qdatetime_to_completion_format(qdate): if not qdate.isValid(): ts = 0 else: - ts = qdate.toMSecsSinceEpoch() - if ts < 0: - ts = 0 + ts = max(qdate.toMSecsSinceEpoch(), 0) pydate = datetime.datetime.fromtimestamp(ts / 1000) return pydate.strftime(config.val.completion.timestamp_format) diff --git a/qutebrowser/components/braveadblock.py b/qutebrowser/components/braveadblock.py index 21319cb1b..b1e5b8a29 100644 --- a/qutebrowser/components/braveadblock.py +++ b/qutebrowser/components/braveadblock.py @@ -266,14 +266,13 @@ class BraveAdBlocker: except DeserializationError: message.error("Reading adblock filter data failed (corrupted data?). " "Please run :adblock-update.") - else: - if ( - config.val.content.blocking.adblock.lists - and not self._has_basedir - and config.val.content.blocking.enabled - and self.enabled - ): - message.info("Run :adblock-update to get adblock lists.") + elif ( + config.val.content.blocking.adblock.lists + and not self._has_basedir + and config.val.content.blocking.enabled + and self.enabled + ): + message.info("Run :adblock-update to get adblock lists.") def adblock_update(self) -> blockutils.BlocklistDownloads: """Update the adblock block lists.""" diff --git a/qutebrowser/components/hostblock.py b/qutebrowser/components/hostblock.py index 1860b734c..191719f10 100644 --- a/qutebrowser/components/hostblock.py +++ b/qutebrowser/components/hostblock.py @@ -64,9 +64,10 @@ def get_fileobj(byte_io: IO[bytes]) -> IO[bytes]: byte_io.seek(0) # rewind downloaded file if zipfile.is_zipfile(byte_io): byte_io.seek(0) # rewind what zipfile.is_zipfile did - zf = zipfile.ZipFile(byte_io) - filename = _guess_zip_filename(zf) - byte_io = zf.open(filename, mode="r") + with zipfile.ZipFile(byte_io) as zf: + filename = _guess_zip_filename(zf) + # pylint: disable=consider-using-with + byte_io = zf.open(filename, mode="r") else: byte_io.seek(0) # rewind what zipfile.is_zipfile did return byte_io diff --git a/qutebrowser/components/misccommands.py b/qutebrowser/components/misccommands.py index 120806bfe..fe908b7d2 100644 --- a/qutebrowser/components/misccommands.py +++ b/qutebrowser/components/misccommands.py @@ -17,6 +17,9 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. +# To allow count being documented +# pylint: disable=differing-param-doc + """Various commands.""" import os @@ -183,7 +186,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/components/utils/blockutils.py b/qutebrowser/components/utils/blockutils.py index bd27baece..98681a488 100644 --- a/qutebrowser/components/utils/blockutils.py +++ b/qutebrowser/components/utils/blockutils.py @@ -125,7 +125,7 @@ class BlocklistDownloads(QObject): filename: path to a local file to import. """ try: - fileobj = open(filename, "rb") + fileobj = open(filename, "rb") # pylint: disable=consider-using-with except OSError as e: message.error( "blockutils: Error while reading {}: {}".format(filename, e.strerror) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 437a54a33..e054c8010 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -195,7 +195,7 @@ class KeyConfig: See #5942. """ - cmd_to_keys: KeyConfig._ReverseBindings = {} + cmd_to_keys: "KeyConfig._ReverseBindings" = {} bindings = self.get_bindings_for(mode) for seq, full_cmd in sorted(bindings.items()): for cmdtext in full_cmd.split(';;'): @@ -387,6 +387,8 @@ class Config(QObject): """Get the given setting converted for Python code. Args: + name: The name of the setting to get. + url: The URL to get the value for. fallback: Use the global value if there's no URL-specific one. """ opt = self.get_opt(name) diff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py index 62fc87f81..417ffce00 100644 --- a/qutebrowser/config/configcommands.py +++ b/qutebrowser/config/configcommands.py @@ -204,7 +204,7 @@ class ConfigCommands: Args: option: The name of the option. - values: The values to cycle through. + *values: The values to cycle through. pattern: The link:configuring{outfilesuffix}#patterns[URL pattern] to use. temp: Set value temporarily until qutebrowser is closed. print_: Print the value after setting. diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index e034fe8f5..c2619ac1f 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -1629,11 +1629,16 @@ hints.selectors: - '[role="link"]' - '[role="option"]' - '[role="button"]' + - '[role="tab"]' + - '[role="checkbox"]' + - '[role="menuitem"]' + - '[role="menuitemcheckbox"]' + - '[role="menuitemradio"]' - '[ng-click]' - '[ngClick]' - '[data-ng-click]' - '[x-ng-click]' - - '[tabindex]' + - '[tabindex]:not([tabindex="-1"])' links: - 'a[href]' - 'area[href]' @@ -1794,6 +1799,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/config/configfiles.py b/qutebrowser/config/configfiles.py index 6f0d0b13c..89100ad52 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -70,7 +70,7 @@ class VersionChange(enum.Enum): This is intended to use filters like "major" (show major only), "minor" (show major/minor) or "patch" (show all changes). """ - allowed_values: Dict[str, List[VersionChange]] = { + allowed_values: Dict[str, List["VersionChange"]] = { 'major': [VersionChange.major], 'minor': [VersionChange.major, VersionChange.minor], 'patch': [VersionChange.major, VersionChange.minor, VersionChange.patch], diff --git a/qutebrowser/config/configutils.py b/qutebrowser/config/configutils.py index 480bbd85f..15f10b1ef 100644 --- a/qutebrowser/config/configutils.py +++ b/qutebrowser/config/configutils.py @@ -92,7 +92,7 @@ class Values: values: Sequence[ScopedValue] = ()) -> None: self.opt = opt self._vmap: MutableMapping[ - Values._VmapKeyType, ScopedValue] = collections.OrderedDict() + "Values._VmapKeyType", ScopedValue] = collections.OrderedDict() # A map from domain parts to rules that fall under them. self._domain_map: Dict[ Optional[str], Set[ScopedValue]] = collections.defaultdict(set) diff --git a/qutebrowser/config/qtargs.py b/qutebrowser/config/qtargs.py index c38ef5b01..2f93b7de5 100644 --- a/qutebrowser/config/qtargs.py +++ b/qutebrowser/config/qtargs.py @@ -260,9 +260,8 @@ def _qtwebengine_args( # Only actually available in Qt 5.12.5, but let's save another # check, as passing the option won't hurt. yield '--enable-in-process-stack-traces' - else: - if 'stack' not in namespace.debug_flags: - yield '--disable-in-process-stack-traces' + elif 'stack' not in namespace.debug_flags: + yield '--disable-in-process-stack-traces' lang_override = _get_lang_override( webengine_version=versions.webengine, diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py index 7556d2b6d..41aeec6a3 100644 --- a/qutebrowser/config/websettings.py +++ b/qutebrowser/config/websettings.py @@ -238,6 +238,7 @@ def user_agent(url: QUrl = None) -> str: def init(args: argparse.Namespace) -> None: """Initialize all QWeb(Engine)Settings.""" + utils.unused(args) if objects.backend == usertypes.Backend.QtWebEngine: from qutebrowser.browser.webengine import webenginesettings webenginesettings.init() 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..044c49278 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -75,7 +75,7 @@ class BindingTrie: __slots__ = 'children', 'command' def __init__(self) -> None: - self.children: MutableMapping[keyutils.KeyInfo, BindingTrie] = {} + self.children: MutableMapping[keyutils.KeyInfo, "BindingTrie"] = {} self.command: Optional[str] = None def __setitem__(self, sequence: keyutils.KeySequence, @@ -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/keyinput/keyutils.py b/qutebrowser/keyinput/keyutils.py index 6bd8c99b8..f74c59aa7 100644 --- a/qutebrowser/keyinput/keyutils.py +++ b/qutebrowser/keyinput/keyutils.py @@ -656,7 +656,6 @@ class KeySequence: @classmethod def parse(cls, keystr: str) -> 'KeySequence': """Parse a keystring like <Ctrl-x> or xyz and return a KeySequence.""" - # pylint: disable=protected-access new = cls() strings = list(_parse_keystring(keystr)) for sub in utils.chunk(strings, cls._MAX_LEN): @@ -666,6 +665,5 @@ class KeySequence: if keystr: assert new, keystr - # pylint: disable=protected-access new._validate(keystr) return new diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index e081284ee..c3f06e185 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 @@ -697,10 +698,9 @@ class TabbedBrowser(QWidget): """ if tab.data.keep_icon: tab.data.keep_icon = False - else: - if (config.cache['tabs.tabs_are_windows'] and - tab.data.should_show_icon()): - self.widget.window().setWindowIcon(self.default_window_icon) + elif (config.cache['tabs.tabs_are_windows'] and + tab.data.should_show_icon()): + self.widget.window().setWindowIcon(self.default_window_icon) @pyqtSlot() def _on_load_status_changed(self, tab): diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index 7983127d5..07b1e5bef 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -486,7 +486,6 @@ class TabBar(QTabBar): Args: idx: The tab index to get the title for. - handle_unset: Whether to return an empty string on KeyError. """ try: return self.tab_data(idx, 'page-title') diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py index 45c52f54c..f7578a07f 100644 --- a/qutebrowser/misc/crashsignal.py +++ b/qutebrowser/misc/crashsignal.py @@ -150,6 +150,7 @@ class CrashHandler(QObject): """Start a new logfile and redirect faulthandler to it.""" logname = os.path.join(standarddir.data(), 'crash.log') try: + # pylint: disable=consider-using-with self._crash_log_file = open(logname, 'w', encoding='ascii') except OSError: log.init.exception("Error while opening crash log file!") @@ -244,7 +245,7 @@ class CrashHandler(QObject): if 'pdb-postmortem' in objects.debug_flags: if tb is None: - pdb.set_trace() # noqa: T100 + pdb.set_trace() # noqa: T100 pylint: disable=forgotten-debug-statement else: pdb.post_mortem(tb) diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py index f27b7acfe..034d7ff74 100644 --- a/qutebrowser/misc/earlyinit.py +++ b/qutebrowser/misc/earlyinit.py @@ -109,7 +109,7 @@ def init_faulthandler(fileobj=sys.__stderr__): when sys.stderr got replaced, e.g. by "Python Tools for Visual Studio". Args: - fobj: An opened file object to write the traceback to. + fileobj: An opened file object to write the traceback to. """ try: faulthandler.enable(fileobj) @@ -211,7 +211,6 @@ def _check_modules(modules): for name, text in modules.items(): try: - # pylint: disable=bad-continuation with log.py_warning_filter( category=DeprecationWarning, message=r'invalid escape sequence' @@ -226,7 +225,6 @@ def _check_modules(modules): category=DeprecationWarning, message=r'Creating a LegacyVersion has been deprecated', ): - # pylint: enable=bad-continuation importlib.import_module(name) except ImportError as e: _die(text, e) diff --git a/qutebrowser/misc/guiprocess.py b/qutebrowser/misc/guiprocess.py index c93fad09b..8e9747ad8 100644 --- a/qutebrowser/misc/guiprocess.py +++ b/qutebrowser/misc/guiprocess.py @@ -262,7 +262,7 @@ class GUIProcess(QObject): QProcess.Crashed: f"{what.capitalize()} crashed", QProcess.Timedout: f"{what.capitalize()} timed out", QProcess.WriteError: f"Write error for {what}", - QProcess.WriteError: f"Read error for {what}", + QProcess.ReadError: f"Read error for {what}", } error_string = self._proc.errorString() msg = ': '.join([error_descriptions[error], error_string]) diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py index 4fcef72e4..93d9af09d 100644 --- a/qutebrowser/misc/keyhintwidget.py +++ b/qutebrowser/misc/keyhintwidget.py @@ -87,6 +87,7 @@ class KeyHintView(QLabel): """Show hints for the given prefix (or hide if prefix is empty). Args: + mode: The key mode to show the keyhints for. prefix: The current partial keystring. """ match = re.fullmatch(r'(\d*)(.*)', prefix) diff --git a/qutebrowser/misc/lineparser.py b/qutebrowser/misc/lineparser.py index 9d35692e9..fee87354f 100644 --- a/qutebrowser/misc/lineparser.py +++ b/qutebrowser/misc/lineparser.py @@ -99,6 +99,7 @@ class BaseLineParser(QObject): self._opened = True try: if self._binary: + # pylint: disable=unspecified-encoding with open(self._configfile, mode + 'b') as f: yield f else: diff --git a/qutebrowser/misc/miscwidgets.py b/qutebrowser/misc/miscwidgets.py index b89288fc7..4354ed2ab 100644 --- a/qutebrowser/misc/miscwidgets.py +++ b/qutebrowser/misc/miscwidgets.py @@ -213,7 +213,7 @@ class _FoldArrow(QWidget): """Paint the arrow. Args: - _paint: The QPaintEvent (unused). + _event: The QPaintEvent (unused). """ opt = QStyleOption() opt.initFrom(self) diff --git a/qutebrowser/misc/msgbox.py b/qutebrowser/misc/msgbox.py index 9d5fbf601..4271c2639 100644 --- a/qutebrowser/misc/msgbox.py +++ b/qutebrowser/misc/msgbox.py @@ -42,6 +42,7 @@ def msgbox(parent, title, text, *, icon, buttons=QMessageBox.Ok, parent: The parent to set for the message box. title: The title to set. text: The text to set. + icon: The QIcon to show in the box. buttons: The buttons to set (QMessageBox::StandardButtons) on_finished: A slot to connect to the 'finished' signal. plain_text: Whether to force plain text (True) or rich text (False). diff --git a/qutebrowser/misc/quitter.py b/qutebrowser/misc/quitter.py index a51891685..905429989 100644 --- a/qutebrowser/misc/quitter.py +++ b/qutebrowser/misc/quitter.py @@ -194,7 +194,7 @@ class Quitter(QObject): # Open a new process and immediately shutdown the existing one try: args = self._get_restart_args(pages, session, override_args) - subprocess.Popen(args) + subprocess.Popen(args) # pylint: disable=consider-using-with except OSError: log.destroy.exception("Failed to restart") return False diff --git a/qutebrowser/misc/savemanager.py b/qutebrowser/misc/savemanager.py index ee22ba14d..1b72734cb 100644 --- a/qutebrowser/misc/savemanager.py +++ b/qutebrowser/misc/savemanager.py @@ -157,6 +157,7 @@ class SaveManager(QObject): """Save a saveable by name. Args: + name: The name of the saveable to save. is_exit: Whether we're currently exiting qutebrowser. explicit: Whether this save operation was triggered explicitly. silent: Don't write information to log. Used to reduce log spam diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index 11af329e0..a28f3a848 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -206,12 +206,11 @@ class SessionManager(QObject): if item.title(): data['title'] = item.title() - else: + elif tab.history.current_idx() == idx: # https://github.com/qutebrowser/qutebrowser/issues/879 - if tab.history.current_idx() == idx: - data['title'] = tab.title() - else: - data['title'] = data['url'] + data['title'] = tab.title() + else: + data['title'] = data['url'] if item.originalUrl() != item.url(): encoded = item.originalUrl().toEncoded() diff --git a/qutebrowser/misc/split.py b/qutebrowser/misc/split.py index 4db91360e..c7d93e76d 100644 --- a/qutebrowser/misc/split.py +++ b/qutebrowser/misc/split.py @@ -128,6 +128,7 @@ def split(s, keep=False): """Split a string via ShellLexer. Args: + s: The string to split. keep: Whether to keep special chars in the split output. """ lexer = ShellLexer(s) diff --git a/qutebrowser/misc/sql.py b/qutebrowser/misc/sql.py index 814eb2bb0..8f3282a2f 100644 --- a/qutebrowser/misc/sql.py +++ b/qutebrowser/misc/sql.py @@ -479,9 +479,6 @@ class SqlTable(QObject): Args: field: Field to use as the key. value: Key value to delete. - - Return: - The number of rows deleted. """ q = self.database.query(f"DELETE FROM {self._name} where {field} = :val") q.run(val=value) diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 9cd07e2e3..54ca4029b 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -381,8 +381,8 @@ def qt_message_handler(msg_type: QtCore.QtMsgType, """Qt message handler to redirect qWarning etc. to the logging system. Args: - QtMsgType msg_type: The level of the message. - QMessageLogContext context: The source code location of the message. + msg_type: The level of the message. + context: The source code location of the message. msg: The message text. """ # Mapping from Qt logging levels to the matching logging module levels. diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py index 50a438637..c490aa4e8 100644 --- a/qutebrowser/utils/message.py +++ b/qutebrowser/utils/message.py @@ -18,7 +18,8 @@ # along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. # Because every method needs to have a log_stack argument -# pylint: disable=unused-argument +# and because we use *args a lot +# pylint: disable=unused-argument,differing-param-doc """Message singleton so we don't have to define unneeded signals.""" @@ -125,7 +126,7 @@ def ask(*args: Any, **kwargs: Any) -> Any: Return: The answer the user gave or None if the prompt was cancelled. """ - question = _build_question(*args, **kwargs) # pylint: disable=missing-kwoa + question = _build_question(*args, **kwargs) global_bridge.ask(question, blocking=True) answer = question.answer question.deleteLater() @@ -139,7 +140,7 @@ def ask_async(title: str, """Ask an async question in the statusbar. Args: - message: The message to display to the user. + title: The message to display to the user. mode: A PromptMode. handler: The function to get called with the answer as argument. default: The default value to display. @@ -174,7 +175,7 @@ def confirm_async(*, yes_action: _ActionType, The question object. """ kwargs['mode'] = usertypes.PromptMode.yesno - question = _build_question(**kwargs) # pylint: disable=missing-kwoa + question = _build_question(**kwargs) question.answered_yes.connect(yes_action) if no_action is not None: question.answered_no.connect(no_action) diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 002f10411..cfba2c1d8 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -95,6 +95,7 @@ def _parse_search_term(s: str) -> Tuple[Optional[str], Optional[str]]: engine = None term = s else: + # pylint: disable=else-if-used if config.val.url.open_base_url and s in config.val.url.searchengines: engine = s term = None @@ -328,6 +329,7 @@ def invalid_url_error(url: QUrl, action: str) -> None: """Display an error message for a URL. Args: + url: The URL to display a message for. action: The action which was interrupted by the error. """ if url.isValid(): diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index ee0f899cc..56c29899d 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -301,6 +301,7 @@ class Backend(enum.Enum): """The backend being used (usertypes.backend).""" + # pylint: disable=invalid-name QtWebKit = enum.auto() QtWebEngine = enum.auto() diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index f42515c5c..9c68932f3 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -382,7 +382,7 @@ def get_repr(obj: Any, constructor: bool = False, **attrs: Any) -> str: obj: The object to get a repr for. constructor: If True, show the Foo(one=1, two=2) form instead of <Foo one=1 two=2>. - attrs: The attributes to add. + **attrs: The attributes to add. """ cls = qualname(obj.__class__) parts = [] @@ -391,11 +391,10 @@ def get_repr(obj: Any, constructor: bool = False, **attrs: Any) -> str: parts.append('{}={!r}'.format(name, val)) if constructor: return '{}({})'.format(cls, ', '.join(parts)) + elif parts: + return '<{} {}>'.format(cls, ' '.join(parts)) else: - if parts: - return '<{} {}>'.format(cls, ' '.join(parts)) - else: - return '<{}>'.format(cls) + return '<{}>'.format(cls) def qualname(obj: Any) -> str: 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..204ccff9d 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.1 ; python_version<"3.8" zipp==3.6.0 diff --git a/scripts/dev/build_release.py b/scripts/dev/build_release.py index 4961cbdc8..5463441be 100755 --- a/scripts/dev/build_release.py +++ b/scripts/dev/build_release.py @@ -26,6 +26,7 @@ import os.path import sys import time import shutil +import pathlib import plistlib import subprocess import argparse @@ -480,24 +481,27 @@ def build_sdist(): """Build an sdist and list the contents.""" utils.print_title("Building sdist") - _maybe_remove('dist') + dist_path = pathlib.Path('dist') + _maybe_remove(dist_path) - subprocess.run([sys.executable, 'setup.py', 'sdist'], check=True) - dist_files = os.listdir(os.path.abspath('dist')) - assert len(dist_files) == 1 + subprocess.run([sys.executable, '-m', 'build'], check=True) - dist_file = os.path.join('dist', dist_files[0]) - subprocess.run(['gpg', '--detach-sign', '-a', dist_file], check=True) + dist_files = list(dist_path.glob('*.tar.gz')) + filename = 'qutebrowser-{}.tar.gz'.format(qutebrowser.__version__) + assert dist_files == [dist_path / filename], dist_files + dist_file = dist_files[0] + + subprocess.run(['gpg', '--detach-sign', '-a', str(dist_file)], check=True) - tar = tarfile.open(dist_file) by_ext = collections.defaultdict(list) - for tarinfo in tar.getmembers(): - if not tarinfo.isfile(): - continue - name = os.sep.join(tarinfo.name.split(os.sep)[1:]) - _base, ext = os.path.splitext(name) - by_ext[ext].append(name) + with tarfile.open(dist_file) as tar: + for tarinfo in tar.getmembers(): + if not tarinfo.isfile(): + continue + name = os.sep.join(tarinfo.name.split(os.sep)[1:]) + _base, ext = os.path.splitext(name) + by_ext[ext].append(name) assert '.pyc' not in by_ext @@ -507,11 +511,13 @@ def build_sdist(): utils.print_subtitle(ext) print('\n'.join(files)) - filename = 'qutebrowser-{}.tar.gz'.format(qutebrowser.__version__) artifacts = [ - (os.path.join('dist', filename), 'application/gzip', 'Source release'), - (os.path.join('dist', filename + '.asc'), 'application/pgp-signature', - 'Source release - PGP signature'), + (str(dist_file), 'application/gzip', 'Source release'), + ( + str(dist_file.with_suffix(dist_file.suffix + '.asc')), + 'application/pgp-signature', + 'Source release - PGP signature', + ), ] return artifacts @@ -550,6 +556,7 @@ def github_upload(artifacts, tag, gh_token): Args: artifacts: A list of (filename, mimetype, description) tuples tag: The name of the release tag + gh_token: The GitHub token to use """ import github3 import github3.exceptions @@ -599,15 +606,19 @@ def github_upload(artifacts, tag, gh_token): def pypi_upload(artifacts): """Upload the given artifacts to PyPI using twine.""" utils.print_title("Uploading to PyPI...") - filenames = [a[0] for a in artifacts] - subprocess.run([sys.executable, '-m', 'twine', 'upload'] + filenames, - check=True) + run_twine('upload', artifacts) -def upgrade_sdist_dependencies(): - """Make sure we have the latest tools for an sdist release.""" - subprocess.run([sys.executable, '-m', 'pip', 'install', '-U', 'twine', - 'pip', 'wheel', 'setuptools'], check=True) +def twine_check(artifacts): + """Check packages using 'twine check'.""" + utils.print_title("Running twine check...") + run_twine('check', artifacts, '--strict') + + +def run_twine(command, artifacts, *args): + filenames = [a[0] for a in artifacts] + subprocess.run([sys.executable, '-m', 'twine', command] + list(args) + filenames, + check=True) def main(): @@ -667,9 +678,9 @@ def main(): elif sys.platform == 'darwin': artifacts = build_mac(gh_token=gh_token, debug=args.debug) else: - upgrade_sdist_dependencies() test_makefile() artifacts = build_sdist() + twine_check(artifacts) upload_to_pypi = True if args.upload: diff --git a/scripts/dev/pylint_checkers/qute_pylint/modeline.py b/scripts/dev/pylint_checkers/qute_pylint/modeline.py index 114cfaf94..1df2c375e 100644 --- a/scripts/dev/pylint_checkers/qute_pylint/modeline.py +++ b/scripts/dev/pylint_checkers/qute_pylint/modeline.py @@ -31,9 +31,9 @@ class ModelineChecker(checkers.BaseChecker): __implements__ = interfaces.IRawChecker name = 'modeline' - msgs = {'W9002': ('Does not have vim modeline', 'modeline-missing', None), - 'W9003': ('Modeline is invalid', 'invalid-modeline', None), - 'W9004': ('Modeline position is wrong', 'modeline-position', None)} + msgs = {'W9102': ('Does not have vim modeline', 'modeline-missing', None), + 'W9103': ('Modeline is invalid', 'invalid-modeline', None), + 'W9104': ('Modeline position is wrong', 'modeline-position', None)} options = () priority = -1 diff --git a/scripts/dev/pylint_checkers/qute_pylint/openencoding.py b/scripts/dev/pylint_checkers/qute_pylint/openencoding.py deleted file mode 100644 index 972a55db8..000000000 --- a/scripts/dev/pylint_checkers/qute_pylint/openencoding.py +++ /dev/null @@ -1,83 +0,0 @@ -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2014-2021 Florian Bruhin (The Compiler) <mail@qutebrowser.org> - -# This file is part of qutebrowser. -# -# qutebrowser is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# qutebrowser is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. - -"""Make sure open() has an encoding set.""" - -import astroid -from pylint import interfaces, checkers -from pylint.checkers import utils - - -class OpenEncodingChecker(checkers.BaseChecker): - - """Checker to check open() has an encoding set.""" - - __implements__ = interfaces.IAstroidChecker - name = 'open-encoding' - - msgs = { - 'W9400': ('open() called without encoding', 'open-without-encoding', - None), - } - - @utils.check_messages('open-without-encoding') - def visit_call(self, node): - """Visit a Call node.""" - if hasattr(node, 'func'): - infer = utils.safe_infer(node.func) - if infer and infer.root().name == '_io': - if getattr(node.func, 'name', None) in ['open', 'file']: - self._check_open_encoding(node) - - def _check_open_encoding(self, node): - """Check that an open() call always has an encoding set.""" - try: - mode_arg = utils.get_argument_from_call(node, position=1, - keyword='mode') - except utils.NoSuchArgumentError: - mode_arg = None - _encoding = None - try: - _encoding = utils.get_argument_from_call(node, position=2) - except utils.NoSuchArgumentError: - try: - _encoding = utils.get_argument_from_call(node, - keyword='encoding') - except utils.NoSuchArgumentError: - pass - if _encoding is None: - if mode_arg is None: - mode = None - else: - mode = utils.safe_infer(mode_arg) - if mode is not None and not isinstance(mode, astroid.Const): - # We can't say what mode is exactly. - return - if mode is None: - self.add_message('open-without-encoding', node=node) - elif 'b' in getattr(mode, 'value', ''): - # Files opened as binary don't need an encoding. - return - else: - self.add_message('open-without-encoding', node=node) - - -def register(linter): - """Register this checker.""" - linter.register_checker(OpenEncodingChecker(linter)) diff --git a/scripts/dev/recompile_requirements.py b/scripts/dev/recompile_requirements.py index c013346ae..89b0fe515 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/blob/master/typing_extensions/CHANGELOG', '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', @@ -175,8 +175,6 @@ CHANGELOG_URLS = { 'python-dateutil': 'https://dateutil.readthedocs.io/en/stable/changelog.html', 'platformdirs': 'https://github.com/platformdirs/platformdirs/blob/main/CHANGES.rst', 'pluggy': 'https://github.com/pytest-dev/pluggy/blob/master/CHANGELOG.rst', - 'inflect': 'https://github.com/jazzband/inflect/blob/master/CHANGES.rst', - 'jinja2-pluralize': 'https://github.com/audreyfeldroy/jinja2_pluralize/blob/master/HISTORY.rst', 'mypy-extensions': 'https://github.com/python/mypy_extensions/commits/master', 'pyroma': 'https://github.com/regebro/pyroma/blob/master/HISTORY.txt', 'adblock': 'https://github.com/ArniDagur/python-adblock/blob/master/CHANGELOG.md', @@ -190,6 +188,17 @@ CHANGELOG_URLS = { 'future': 'https://python-future.org/whatsnew.html', 'pefile': 'https://github.com/erocarrera/pefile/commits/master', 'Deprecated': 'https://github.com/tantale/deprecated/blob/master/CHANGELOG.rst', + 'SecretStorage': 'https://github.com/mitya57/secretstorage/blob/master/changelog', + 'bleach': 'https://github.com/mozilla/bleach/blob/main/CHANGES', + 'jeepney': 'https://gitlab.com/takluyver/jeepney/-/blob/master/docs/release-notes.rst', + 'keyring': 'https://github.com/jaraco/keyring/blob/main/CHANGES.rst', + 'pkginfo': 'https://bazaar.launchpad.net/~tseaver/pkginfo/trunk/view/head:/CHANGES.txt', + 'readme-renderer': 'https://github.com/pypa/readme_renderer/blob/main/CHANGES.rst', + 'requests-toolbelt': 'https://github.com/requests/toolbelt/blob/master/HISTORY.rst', + 'rfc3986': 'https://rfc3986.readthedocs.io/en/latest/release-notes/index.html', + 'tqdm': 'https://tqdm.github.io/releases/', + 'twine': 'https://twine.readthedocs.io/en/stable/changelog.html', + 'webencodings': 'https://github.com/gsnedders/python-webencodings/commits/master', } diff --git a/scripts/dev/run_pylint_on_tests.py b/scripts/dev/run_pylint_on_tests.py index d0385bd17..28c6e32c9 100644 --- a/scripts/dev/run_pylint_on_tests.py +++ b/scripts/dev/run_pylint_on_tests.py @@ -59,8 +59,11 @@ def main(): 'len-as-condition', 'compare-to-empty-string', 'pointless-statement', + 'use-implicit-booleaness-not-comparison', # directories without __init__.py... 'import-error', + # tests/helpers imports + 'wrong-import-order', ] toxinidir = sys.argv[1] diff --git a/scripts/dev/update_3rdparty.py b/scripts/dev/update_3rdparty.py index 88f56b7f3..60b72d110 100755 --- a/scripts/dev/update_3rdparty.py +++ b/scripts/dev/update_3rdparty.py @@ -160,11 +160,11 @@ def test_dicts(): print('Testing dictionary {}... '.format(lang.code), end='') lang_url = urllib.parse.urljoin(dictcli.API_URL, lang.remote_filename) request = urllib.request.Request(lang_url, method='HEAD') - response = urllib.request.urlopen(request) - if response.status == 200: - print('OK') - else: - print('ERROR: {}'.format(response.status)) + with urllib.request.urlopen(request) as response: + if response.status == 200: + print('OK') + else: + print('ERROR: {}'.format(response.status)) def run(nsis=False, ace=False, pdfjs=True, fancy_dmg=False, pdfjs_version=None, diff --git a/scripts/dictcli.py b/scripts/dictcli.py index 8cb93fb8a..a937fd31d 100755 --- a/scripts/dictcli.py +++ b/scripts/dictcli.py @@ -142,11 +142,11 @@ def parse_entry(entry): def language_list_from_api(): """Return a JSON with a list of available languages from Google API.""" listurl = API_URL + '?format=JSON' - response = urllib.request.urlopen(listurl) - # A special 5-byte prefix must be stripped from the response content - # See: https://github.com/google/gitiles/issues/22 - # https://github.com/google/gitiles/issues/82 - json_content = response.read()[5:] + with urllib.request.urlopen(listurl) as response: + # A special 5-byte prefix must be stripped from the response content + # See: https://github.com/google/gitiles/issues/22 + # https://github.com/google/gitiles/issues/82 + json_content = response.read()[5:] entries = json.loads(json_content.decode('utf-8'))['entries'] parsed_entries = [parse_entry(entry) for entry in entries] return [entry for entry in parsed_entries if entry is not None] @@ -176,8 +176,8 @@ def available_languages(): def download_dictionary(url, dest): """Download a decoded dictionary file.""" - response = urllib.request.urlopen(url) - decoded = base64.decodebytes(response.read()) + with urllib.request.urlopen(url) as response: + decoded = base64.decodebytes(response.read()) with open(dest, 'bw') as dict_file: dict_file.write(decoded) diff --git a/scripts/hostblock_blame.py b/scripts/hostblock_blame.py index 38acaa58d..b18c62925 100644 --- a/scripts/hostblock_blame.py +++ b/scripts/hostblock_blame.py @@ -41,8 +41,8 @@ def main(): for url in configdata.DATA['content.blocking.hosts.lists'].default: print("checking {}...".format(url)) - raw_file = urllib.request.urlopen(url) - byte_io = io.BytesIO(raw_file.read()) + with urllib.request.urlopen(url) as raw_file: + byte_io = io.BytesIO(raw_file.read()) f = hostblock.get_fileobj(byte_io) for line in f: line = line.decode('utf-8') 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/data/brave-adblock/generate.py b/tests/end2end/data/brave-adblock/generate.py index 7a23a21ab..393cda4e7 100644 --- a/tests/end2end/data/brave-adblock/generate.py +++ b/tests/end2end/data/brave-adblock/generate.py @@ -53,9 +53,9 @@ def main(): else: request = urllib.request.Request(URL) print(f"Downloading {URL} ...") - response = urllib.request.urlopen(request) - assert response.status == 200 - data_str = response.read().decode("utf-8") + with urllib.request.urlopen(request) as response: + assert response.status == 200 + data_str = response.read().decode("utf-8") print(f"Saving to cache file {CACHE_PATH} ...") CACHE_PATH.write_text(data_str, encoding="utf-8") data = io.StringIO(data_str) diff --git a/tests/end2end/data/hints/html/README.md b/tests/end2end/data/hints/html/README.md index 2a6e97c24..5bbaecb4a 100644 --- a/tests/end2end/data/hints/html/README.md +++ b/tests/end2end/data/hints/html/README.md @@ -3,3 +3,5 @@ Tests in this directory are automatically picked up by `test_hints` in They need to contain a special `<!-- target: foo.html -->` comment which specifies where the hint in it will point to, and will then test that. + +With `<!-- target: null -->`, the page is expected to not generate any hints. diff --git a/tests/end2end/data/hints/invisible.html b/tests/end2end/data/hints/html/invisible.html index b0bfa9dd9..d382c80fa 100644 --- a/tests/end2end/data/hints/invisible.html +++ b/tests/end2end/data/hints/html/invisible.html @@ -1,3 +1,5 @@ +<!-- target: null --> + <!DOCTYPE html> <html> diff --git a/tests/end2end/data/hints/html/tabindex-negative.html b/tests/end2end/data/hints/html/tabindex-negative.html new file mode 100644 index 000000000..03adb32bf --- /dev/null +++ b/tests/end2end/data/hints/html/tabindex-negative.html @@ -0,0 +1,13 @@ +<!-- target: null --> + +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <title>Span with tabindex -1</title> + </head> + <body> + <p>This text should not get a hint:</p> + <span tabindex=-1>test</span> + </body> +</html> diff --git a/tests/end2end/features/hints.feature b/tests/end2end/features/hints.feature index cf35c5356..47153b741 100644 --- a/tests/end2end/features/hints.feature +++ b/tests/end2end/features/hints.feature @@ -241,11 +241,6 @@ Feature: Using hints # The actual check is already done above Then "No elements found." should not be logged - Scenario: Hinting invisible elements - When I open data/hints/invisible.html - And I run :hint - Then the error "No elements found." should be shown - Scenario: Clicking input with existing text When I open data/hints/input.html And I run :click-element id qute-input-existing diff --git a/tests/end2end/features/search.feature b/tests/end2end/features/search.feature index 5fafd19f0..305b45690 100644 --- a/tests/end2end/features/search.feature +++ b/tests/end2end/features/search.feature @@ -71,7 +71,25 @@ Feature: Searching on a page When I run :search foo And I wait for "search found foo" in the log And I run :search foo - Then "Ignoring duplicate search request for foo" should be logged + Then "Ignoring duplicate search request for foo, but resetting flags" should be logged + + Scenario: Reset search direction on duplicate search, forward-to-back + When I run :search baz + And I wait for "search found baz" in the log + And I run :search -r baz + And I wait for "Ignoring duplicate search request for baz, but resetting flags" in the log + And I run :search-next + And I wait for "next_result found baz with flags FindBackward" in the log + Then "BAZ" should be found + + Scenario: Reset search direction on duplicate search, back-to-forward + When I run :search -r baz + And I wait for "search found baz with flags FindBackward" in the log + And I run :search baz + And I wait for "Ignoring duplicate search request for baz, but resetting flags" in the log + And I run :search-next + And I wait for "next_result found baz" in the log + Then "baz" should be found ## search.ignore_case 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/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py index 3cbea01ad..14f34b52c 100644 --- a/tests/end2end/fixtures/quteprocess.py +++ b/tests/end2end/fixtures/quteprocess.py @@ -48,10 +48,7 @@ instance_counter = itertools.count() def is_ignored_qt_message(pytestconfig, message): """Check if the message is listed in qt_log_ignore.""" regexes = pytestconfig.getini('qt_log_ignore') - for regex in regexes: - if re.search(regex, message): - return True - return False + return any(re.search(regex, message) for regex in regexes) def is_ignored_lowlevel_message(message): @@ -609,8 +606,7 @@ class QuteProc(testprocess.Process): self.wait_for(category='webview', message='Scroll position changed to ' + point) - def wait_for(self, timeout=None, # pylint: disable=arguments-differ - **kwargs): + def wait_for(self, timeout=None, **kwargs): """Extend wait_for to add divisor if a test is xfailing.""" __tracebackhide__ = (lambda e: e.errisinstance(testprocess.WaitForTimeout)) @@ -716,7 +712,7 @@ class QuteProc(testprocess.Process): target_arg) self._wait_for_ipc() - def start(self, *args, **kwargs): # pylint: disable=arguments-differ + def start(self, *args, **kwargs): try: super().start(*args, **kwargs) except testprocess.ProcessExited: @@ -913,8 +909,8 @@ class QuteProc(testprocess.Process): """Get a screenshot of the current page. Arguments: - probe: If given, only continue if the pixel at the given position isn't - black (or whatever is specified by probe_color). + probe_pos: If given, only continue if the pixel at the given + position isn't black (or whatever is specified by probe_color). """ for _ in range(5): tmp_path = self.request.getfixturevalue('tmp_path') diff --git a/tests/end2end/fixtures/test_webserver.py b/tests/end2end/fixtures/test_webserver.py index 3c825e5bc..ed0c32ae5 100644 --- a/tests/end2end/fixtures/test_webserver.py +++ b/tests/end2end/fixtures/test_webserver.py @@ -37,7 +37,8 @@ def test_server(server, qtbot, path, content, expected): with qtbot.wait_signal(server.new_request, timeout=100): url = 'http://localhost:{}{}'.format(server.port, path) try: - response = urllib.request.urlopen(url) + with urllib.request.urlopen(url) as response: + data = response.read().decode('utf-8') except urllib.error.HTTPError as e: # "Though being an exception (a subclass of URLError), an HTTPError # can also function as a non-exceptional file-like return value @@ -46,8 +47,6 @@ def test_server(server, qtbot, path, content, expected): print(e.read().decode('utf-8')) raise - data = response.read().decode('utf-8') - assert server.get_requests() == [server.ExpectedRequest('GET', path)] assert (content in data) == expected diff --git a/tests/end2end/test_hints_html.py b/tests/end2end/test_hints_html.py index ebb2a7e33..f1cda97fe 100644 --- a/tests/end2end/test_hints_html.py +++ b/tests/end2end/test_hints_html.py @@ -40,7 +40,7 @@ def collect_tests(): @dataclasses.dataclass class ParsedFile: - target: str + target: Optional[str] qtwebengine_todo: Optional[str] @@ -107,11 +107,18 @@ def test_hints(test_name, zoom_text_only, zoom_level, find_implementation, quteproc.set_setting('zoom.text_only', str(zoom_text_only)) quteproc.set_setting('hints.find_implementation', find_implementation) quteproc.send_cmd(':zoom {}'.format(zoom_level)) + # follow hint quteproc.send_cmd(':hint all normal') - quteproc.wait_for(message='hints: a', category='hints') - quteproc.send_cmd(':hint-follow a') - quteproc.wait_for_load_finished('data/' + parsed.target) + + if parsed.target is None: + msg = quteproc.wait_for(message='No elements found.', category='message') + msg.expected = True + else: + quteproc.wait_for(message='hints: a', category='hints') + quteproc.send_cmd(':hint-follow a') + quteproc.wait_for_load_finished('data/' + parsed.target) + # reset quteproc.send_cmd(':zoom 100') if not request.config.webengine: diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py index cd3778b8a..dd902eb7d 100644 --- a/tests/helpers/fixtures.py +++ b/tests/helpers/fixtures.py @@ -186,7 +186,7 @@ def testdata_scheme(qapp): pass @qutescheme.add_handler('testdata') - def handler(url): # pylint: disable=unused-variable + def handler(url): file_abs = os.path.abspath(os.path.dirname(__file__)) filename = os.path.join(file_abs, os.pardir, 'end2end', url.path().lstrip('/')) diff --git a/tests/helpers/logfail.py b/tests/helpers/logfail.py index ae4ac9bc0..15d5a9253 100644 --- a/tests/helpers/logfail.py +++ b/tests/helpers/logfail.py @@ -39,8 +39,7 @@ class LogFailHandler(logging.Handler): if logger.name == 'messagemock': return - if (logger.level == record.levelno or - root_logger.level == record.levelno): + if record.levelno in (logger.level, root_logger.level): # caplog.at_level(...) was used with the level of this message, # i.e. it was expected. return diff --git a/tests/unit/api/test_cmdutils.py b/tests/unit/api/test_cmdutils.py index 77e648eed..b6650078c 100644 --- a/tests/unit/api/test_cmdutils.py +++ b/tests/unit/api/test_cmdutils.py @@ -17,8 +17,6 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. -# pylint: disable=unused-variable - """Tests for qutebrowser.api.cmdutils.""" import sys @@ -292,7 +290,6 @@ class TestRegister: class Enum(enum.Enum): - # pylint: disable=invalid-name x = enum.auto() y = enum.auto() diff --git a/tests/unit/browser/webkit/test_webkitelem.py b/tests/unit/browser/webkit/test_webkitelem.py index 593896e96..3ccf573ff 100644 --- a/tests/unit/browser/webkit/test_webkitelem.py +++ b/tests/unit/browser/webkit/test_webkitelem.py @@ -181,13 +181,30 @@ class SelectionAndFilterTests: # We can't easily test <frame>/<iframe> as they vanish when setting # them via QWebFrame::setHtml... + ('<img src="bar">', ['all', 'images', 'url']), + ('<summary>bar</summary>', ['all']), + ('<link />', ['all']), + + ('<p contenteditable />', ['all', 'inputs']), + ('<p contenteditable="false" />', []), ('<p onclick="foo" foo="bar"/>', ['all']), ('<p onmousedown="foo" foo="bar"/>', ['all']), ('<p role="option" foo="bar"/>', ['all']), + ('<p role="tab" foo="bar"/>', ['all']), + ('<p role="checkbox" foo="bar"/>', ['all']), + ('<p role="menuitem" foo="bar"/>', ['all']), + ('<p role="menuitemcheckbox" foo="bar"/>', ['all']), + ('<p role="menuitemradio" foo="bar"/>', ['all']), ('<p role="button" foo="bar"/>', ['all']), ('<p role="button" href="bar"/>', ['all', 'url']), - ('<span tabindex=0 />', ['all']), + ('<span tabindex="0" />', ['all']), + ('<span tabindex="-1" />', []), + + ('<span ng-click=""></span>', ['all']), + ('<span ngClick=""></span>', ['all']), + ('<span data-ng-click=""></span>', ['all']), + ('<span x-ng-click=""></span>', ['all']), ] GROUPS = ['all', 'links', 'images', 'url', 'inputs'] diff --git a/tests/unit/completion/test_completionwidget.py b/tests/unit/completion/test_completionwidget.py index 89390cbf1..9a1bc99dd 100644 --- a/tests/unit/completion/test_completionwidget.py +++ b/tests/unit/completion/test_completionwidget.py @@ -163,16 +163,7 @@ def test_completion_item_focus_no_model(which, completionview, model, qtbot): @pytest.mark.skip("Seems to disagree with reality, see #5897") def test_completion_item_focus_fetch(completionview, model, qtbot): - """Test that on_next_prev_item moves the selection properly. - - Args: - which: the direction in which to move the selection. - tree: Each list represents a completion category, with each string - being an item under that category. - expected: expected argument from on_selection_changed for each - successive movement. None implies no signal should be - emitted. - """ + """Test that on_next_prev_item moves the selection properly.""" cat = mock.Mock(spec=[ 'layoutChanged', 'layoutAboutToBeChanged', 'canFetchMore', 'fetchMore', 'rowCount', 'index', 'data']) diff --git a/tests/unit/components/test_hostblock.py b/tests/unit/components/test_hostblock.py index 6a71058ea..5949f92f8 100644 --- a/tests/unit/components/test_hostblock.py +++ b/tests/unit/components/test_hostblock.py @@ -427,7 +427,7 @@ def test_invalid_utf8(config_stub, tmp_path, caplog, host_blocker_factory, locat with caplog.at_level(logging.ERROR): current_download.successful = True current_download.finished.emit() - expected = r"Failed to decode: " r"b'https://www.example.org/\xa0localhost" + expected = r"Failed to decode: b'https://www.example.org/\xa0localhost" assert caplog.messages[-2].startswith(expected) else: current_download.successful = True 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/mainwindow/statusbar/test_textbase.py b/tests/unit/mainwindow/statusbar/test_textbase.py index 631c6ce44..33c4ffd76 100644 --- a/tests/unit/mainwindow/statusbar/test_textbase.py +++ b/tests/unit/mainwindow/statusbar/test_textbase.py @@ -79,11 +79,11 @@ def test_text_elide_none(mocker, qtbot): label = TextBase() qtbot.add_widget(label) label.setText('') - mocker.patch('qutebrowser.mainwindow.statusbar.textbase.TextBase.' - 'fontMetrics') + mock = mocker.patch( + 'qutebrowser.mainwindow.statusbar.textbase.TextBase.fontMetrics') label._update_elided_text(20) - assert not label.fontMetrics.called + assert not mock.called def test_unset_text(qtbot): diff --git a/tests/unit/misc/test_keyhints.py b/tests/unit/misc/test_keyhints.py index 8d9c53c93..922f39331 100644 --- a/tests/unit/misc/test_keyhints.py +++ b/tests/unit/misc/test_keyhints.py @@ -30,8 +30,8 @@ def expected_text(*args): """Helper to format text we expect the KeyHintView to generate. Args: - args: One tuple for each row in the expected output. - Tuples are of the form: (prefix, color, suffix, command). + *args: One tuple for each row in the expected output. + Tuples are of the form: (prefix, color, suffix, command). """ text = '<table>' for group in args: diff --git a/tests/unit/utils/test_log.py b/tests/unit/utils/test_log.py index 7682c1156..7b0f5293e 100644 --- a/tests/unit/utils/test_log.py +++ b/tests/unit/utils/test_log.py @@ -53,8 +53,7 @@ def restore_loggers(): saved_level_to_name = logging._levelToName.copy() logger_states = {} for name in saved_loggers: - logger_states[name] = getattr(saved_loggers[name], 'disabled', - None) + logger_states[name] = getattr(saved_loggers[name], 'disabled', None) finally: logging._releaseLock() @@ -86,9 +85,9 @@ def restore_loggers(): logger_dict = logging.getLogger().manager.loggerDict logger_dict.clear() logger_dict.update(saved_loggers) - for name in logger_states: - if logger_states[name] is not None: - saved_loggers[name].disabled = logger_states[name] + for name, state in logger_states.items(): + if state is not None: + saved_loggers[name].disabled = state finally: logging._releaseLock() diff --git a/tests/unit/utils/test_qtutils.py b/tests/unit/utils/test_qtutils.py index 2d98feece..6309e46b5 100644 --- a/tests/unit/utils/test_qtutils.py +++ b/tests/unit/utils/test_qtutils.py @@ -51,7 +51,6 @@ else: test_file = None -# pylint: disable=bad-continuation @pytest.mark.parametrize(['qversion', 'compiled', 'pyqt', 'version', 'exact', 'expected'], [ # equal versions @@ -75,7 +74,6 @@ else: # dev suffix ('5.15.1', '5.15.1', '5.15.2.dev2009281246', '5.15.0', False, True), ]) -# pylint: enable=bad-continuation def test_version_check(monkeypatch, qversion, compiled, pyqt, version, exact, expected): """Test for version_check(). @@ -559,7 +557,8 @@ if test_file is not None: qiodev.mode = mode # Create empty TESTFN file because the Python tests try to unlink # it.after the test. - open(test_file.TESTFN, 'w', encoding='utf-8').close() + with open(test_file.TESTFN, 'w', encoding='utf-8'): + pass return qiodev class PyAutoFileTests(PyIODeviceTestMixin, test_file.AutoFileTests, 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_urlutils.py b/tests/unit/utils/test_urlutils.py index 97ff268ca..e5773e25e 100644 --- a/tests/unit/utils/test_urlutils.py +++ b/tests/unit/utils/test_urlutils.py @@ -334,7 +334,6 @@ def test_get_search_url_open_base_url(config_stub, url, host): Args: url: The "URL" to enter. host: The expected search machine host. - query: The expected search query. """ config_stub.val.url.open_base_url = True url = urlutils._get_search_url(url) diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py index 6c57cb3d3..7b616d8b7 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.""" @@ -718,7 +721,7 @@ class TestModuleVersions: Args: attribute: The name of the version attribute. - expected: The expected return value. + expected_modules: The expected modules with that attribute. """ import_fake.version_attribute = attribute @@ -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 |