Merge branch 'master' of https://github.com/qutebrowser/qutebrowser into donottrack

This commit is contained in:
Jay Kamat 2019-02-01 23:02:32 -08:00
commit 1c7178c92c
No known key found for this signature in database
GPG Key ID: 5D2E399600F4F7B5
97 changed files with 1735 additions and 532 deletions

View File

@ -46,6 +46,7 @@ ignore =
min-version = 3.4.0
max-complexity = 12
per-file-ignores =
/qutebrowser/api/hook.py : N801
/tests/**/*.py : D100,D101,D401
/tests/unit/browser/test_history.py : N806
/tests/helpers/fixtures.py : N806

1
.gitignore vendored
View File

@ -41,3 +41,4 @@ TODO
/scripts/testbrowser/cpp/webengine/.qmake.stash
/scripts/dev/pylint_checkers/qute_pylint.egg-info
/misc/file_version_info.txt
/doc/extapi/_build

View File

@ -68,16 +68,3 @@ after_success:
after_failure:
- bash scripts/dev/ci/travis_backtrace.sh
notifications:
webhooks:
- https://buildtimetrend.herokuapp.com/travis
irc:
channels:
- "chat.freenode.net#qutebrowser-dev"
on_success: always
on_failure: always
skip_join: true
template:
- "%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}"
- "%{compare_url} - %{build_url}"

View File

@ -13,7 +13,7 @@ include qutebrowser/utils/testfile
include qutebrowser/git-commit-id
include LICENSE doc/* README.asciidoc
include misc/qutebrowser.desktop
include misc/qutebrowser.appdata.xml
include misc/org.qutebrowser.qutebrowser.appdata.xml
include misc/Makefile
include requirements.txt
include tox.ini
@ -40,5 +40,6 @@ exclude .*
exclude misc/qutebrowser.spec
exclude misc/qutebrowser.nsi
exclude misc/qutebrowser.rcc
prune doc/extapi
global-exclude __pycache__ *.pyc *.pyo

View File

@ -25,6 +25,7 @@ Added
opened from a page should stack on each other or not.
- New `completion.open_categories` setting which allows to configure which
categories are shown in the `:open` completion, and how they are ordered.
- New `tabs.pinned.frozen` setting to allow/deny navigating in pinned tabs.
- New config manipulation commands:
* `:config-dict-add` and `:config-list-add` to a new element to a dict/list
setting.
@ -51,6 +52,13 @@ Changed
adblocker can be disabled on a given page.
- Elements with a `tabindex` attribute now also get hints by default.
- Various small performance improvements for hints and the completion.
- The Wayland check for QtWebEngine is now disabled on Qt >= 5.11.2, as those
versions should work without any issues.
- The JavaScript `console` object is now available in PAC files.
- The metainfo file `qutebrowser.appdata.xml` is now renamed to
`org.qutebrowser.qutebrowser.appdata.xml`.
- The `qute-pass` userscript now understands domains in gpg filenames
in addition to directory names.
Fixed
~~~~~
@ -65,9 +73,12 @@ Fixed
`content.cookies.accept = no-3rdparty` from working properly on some pages
like GMail. However, the default for `content.cookies.accept` is still `all`
to be in line with what other browsers do.
- `:navigate` not incrementing in anchors or queries or anchors.
- `:navigate` not incrementing in anchors or queries.
- Crash when trying to use a proxy requiring authentication with QtWebKit.
- Slashes in search terms are now percent-escaped.
- When `scrolling.bar = True` was set in versions before v1.5.0, this now
correctly gets migrated to `always` instead of `when-searching`.
- Completion highlighting now works again on Qt 5.11.3 and 5.12.1.
v1.5.2
------

View File

View File

48
doc/extapi/api.rst Normal file
View File

@ -0,0 +1,48 @@
API modules
===========
cmdutils module
---------------
.. automodule:: qutebrowser.api.cmdutils
:members:
:imported-members:
apitypes module
---------------
.. automodule:: qutebrowser.api.apitypes
:members:
:imported-members:
config module
-------------
.. automodule:: qutebrowser.api.config
:members:
downloads module
----------------
.. automodule:: qutebrowser.api.downloads
:members:
hook module
-----------
.. automodule:: qutebrowser.api.hook
:members:
interceptor module
------------------
.. automodule:: qutebrowser.api.interceptor
:members:
:imported-members:
message module
--------------
.. automodule:: qutebrowser.api.message
:members:
:imported-members:

179
doc/extapi/conf.py Normal file
View File

@ -0,0 +1,179 @@
# -*- coding: utf-8 -*-
#
# Configuration file for the Sphinx documentation builder.
#
# This file does only contain a selection of the most common options. For a
# full list see the documentation:
# http://www.sphinx-doc.org/en/master/config
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
project = 'qutebrowser extensions'
copyright = '2018, Florian Bruhin'
author = 'Florian Bruhin'
# The short X.Y version
version = ''
# The full version, including alpha/beta/rc tags
release = ''
# -- General configuration ---------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.napoleon',
]
autodoc_member_order = 'bysource'
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = None
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# The default sidebars (for documents that don't match any pattern) are
# defined by theme itself. Builtin themes are using these templates by
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
# 'searchbox.html']``.
#
# html_sidebars = {}
# -- Options for HTMLHelp output ---------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'qutebrowserextensionsdoc'
# -- Options for LaTeX output ------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'qutebrowserextensions.tex', 'qutebrowser extensions Documentation',
'Florian Bruhin', 'manual'),
]
# -- Options for manual page output ------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'qutebrowserextensions', 'qutebrowser extensions Documentation',
[author], 1)
]
# -- Options for Texinfo output ----------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'qutebrowserextensions', 'qutebrowser extensions Documentation',
author, 'qutebrowserextensions', 'One line description of project.',
'Miscellaneous'),
]
# -- Options for Epub output -------------------------------------------------
# Bibliographic Dublin Core info.
epub_title = project
# The unique identifier of the text. This can be a ISBN number
# or the project homepage.
#
# epub_identifier = ''
# A unique identification for the text.
#
# epub_uid = ''
# A list of files that should not be packed into the epub file.
epub_exclude_files = ['search.html']
# -- Extension configuration -------------------------------------------------

22
doc/extapi/index.rst Normal file
View File

@ -0,0 +1,22 @@
.. qutebrowser extensions documentation master file, created by
sphinx-quickstart on Tue Dec 11 18:59:44 2018.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to qutebrowser extensions's documentation!
==================================================
.. toctree::
:maxdepth: 2
:caption: Contents:
api
tab
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

44
doc/extapi/tab.rst Normal file
View File

@ -0,0 +1,44 @@
Tab API
=======
.. autoclass:: qutebrowser.browser.browsertab.AbstractTab()
:members:
.. autoclass:: qutebrowser.browser.browsertab.AbstractAction()
:members:
.. autoclass:: qutebrowser.browser.browsertab.AbstractPrinting()
:members:
.. autoclass:: qutebrowser.browser.browsertab.AbstractSearch()
:members:
.. autoclass:: qutebrowser.browser.browsertab.AbstractZoom()
:members:
.. autoclass:: qutebrowser.browser.browsertab.AbstractCaret()
:members:
.. autoclass:: qutebrowser.browser.browsertab.AbstractScroller()
:members:
.. autoclass:: qutebrowser.browser.browsertab.AbstractHistory()
:members:
.. autoclass:: qutebrowser.browser.browsertab.AbstractElements()
:members:
.. autoclass:: qutebrowser.browser.browsertab.AbstractAudio()
:members:
Web element API
===============
.. autoclass:: qutebrowser.browser.webelem.AbstractWebElement
:members:
.. autoclass:: qutebrowser.browser.webelem.Error
:members:
.. autoclass:: qutebrowser.browser.webelem.OrphanedError
:members:

View File

@ -211,9 +211,10 @@ Why does J move to the next (right) tab, and K to the previous (left) one?::
What's the difference between insert and passthrough mode?::
They are quite similar, but insert mode has some bindings (like `Ctrl-e` to
open an editor) while passthrough mode only has escape bound. It might also
be useful to rebind escape to something else in passthrough mode only, to be
able to send an escape keypress to the website.
open an editor) while passthrough mode only has shift+escape bound. This is
because shift+escape is unlikely to be a useful binding to be passed to a
webpage. However, any other keys may be assigned to leaving passthrough mode
instead of shift+escape should this be desired.
Why does it take longer to open a URL in qutebrowser than in chromium?::
When opening a URL in an existing instance, the normal qutebrowser

View File

@ -396,6 +396,7 @@ Pre-built colorschemes
- A collection of https://github.com/chriskempson/base16[base16] color-schemes can be found in https://github.com/theova/base16-qutebrowser[base16-qutebrowser] and used with https://github.com/AuditeMarlow/base16-manager[base16-manager].
- Two implementations of the https://github.com/arcticicestudio/nord[Nord] colorscheme for qutebrowser exist: https://github.com/Linuus/nord-qutebrowser[Linuus], https://github.com/KnownAsDon/QuteBrowser-Nord-Theme[KnownAsDon]
- https://github.com/evannagle/qutebrowser-dracula-theme[Dracula]
- https://github.com/jjzmajic/qutewal[Pywal theme]
Avoiding flake8 errors
^^^^^^^^^^^^^^^^^^^^^^

View File

@ -261,6 +261,7 @@
|<<tabs.new_position.stacking,tabs.new_position.stacking>>|Stack related tabs on top of each other when opened consecutively.
|<<tabs.new_position.unrelated,tabs.new_position.unrelated>>|Position of new tabs which are not opened from another tab.
|<<tabs.padding,tabs.padding>>|Padding (in pixels) around text for tabs.
|<<tabs.pinned.frozen,tabs.pinned.frozen>>|Force pinned tabs to stay at fixed URL.
|<<tabs.pinned.shrink,tabs.pinned.shrink>>|Shrink pinned tabs down to their contents.
|<<tabs.position,tabs.position>>|Position of the tab bar.
|<<tabs.select_on_remove,tabs.select_on_remove>>|Which tab to select when the focused tab is removed.
@ -3307,6 +3308,14 @@ Default:
- +pass:[right]+: +pass:[5]+
- +pass:[top]+: +pass:[0]+
[[tabs.pinned.frozen]]
=== tabs.pinned.frozen
Force pinned tabs to stay at fixed URL.
Type: <<types,Bool>>
Default: +pass:[true]+
[[tabs.pinned.shrink]]
=== tabs.pinned.shrink
Shrink pinned tabs down to their contents.

View File

@ -102,18 +102,12 @@ $ python3 scripts/asciidoc2html.py
On Fedora
---------
NOTE: Fedora's packages used to be outdated for a long time, but are
now (November 2017) maintained and up-to-date again.
qutebrowser is available in the official repositories:
-----
# dnf install qutebrowser
-----
However, note that Fedora 25/26 won't be updated to qutebrowser v1.0, so you
might want to <<tox,install qutebrowser via tox>> instead there.
Additional hints
~~~~~~~~~~~~~~~~

View File

@ -17,8 +17,8 @@ doc/qutebrowser.1.html:
install: doc/qutebrowser.1.html
$(PYTHON) setup.py install --prefix="$(PREFIX)" --optimize=1 $(SETUPTOOLSOPTS)
install -Dm644 misc/qutebrowser.appdata.xml \
"$(DESTDIR)$(DATADIR)/metainfo/qutebrowser.appdata.xml"
install -Dm644 misc/org.qutebrowser.qutebrowser.appdata.xml \
"$(DESTDIR)$(DATADIR)/metainfo/org.qutebrowser.qutebrowser.appdata.xml"
install -Dm644 doc/qutebrowser.1 \
"$(DESTDIR)$(MANDIR)/man1/qutebrowser.1"
install -Dm644 misc/qutebrowser.desktop \

View File

@ -6,6 +6,8 @@ import os
sys.path.insert(0, os.getcwd())
from scripts import setupcommon
from qutebrowser.extensions import loader
block_cipher = None
@ -27,6 +29,13 @@ def get_data_files():
return data_files
def get_hidden_imports():
imports = ['PyQt5.QtOpenGL', 'PyQt5._QOpenGLFunctions_2_0']
for info in loader.walk_components():
imports.append(info.name)
return imports
setupcommon.write_git_file()
@ -42,7 +51,7 @@ a = Analysis(['../qutebrowser/__main__.py'],
pathex=['misc'],
binaries=None,
datas=get_data_files(),
hiddenimports=['PyQt5.QtOpenGL', 'PyQt5._QOpenGLFunctions_2_0'],
hiddenimports=get_hidden_imports(),
hookspath=[],
runtime_hooks=[],
excludes=['tkinter'],

View File

@ -4,6 +4,6 @@ certifi==2018.11.29
chardet==3.0.4
codecov==2.0.15
coverage==4.5.2
idna==2.7
requests==2.20.1
idna==2.8
requests==2.21.0
urllib3==1.24.1

View File

@ -11,7 +11,7 @@ flake8-deprecated==1.3
flake8-docstrings==1.3.0
flake8-future-import==0.4.5
flake8-mock==0.3
flake8-per-file-ignores==0.6
flake8-per-file-ignores==0.7
flake8-polyfill==1.0.2
flake8-string-format==0.2.3
flake8-tidy-imports==1.1.0
@ -22,6 +22,6 @@ pep8-naming==0.7.0
pycodestyle==2.4.0
pydocstyle==3.0.0
pyflakes==2.0.0
six==1.11.0
six==1.12.0
snowballstemmer==1.2.1
typing==3.6.6

View File

@ -1,8 +1,8 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
mypy==0.641
mypy==0.650
mypy-extensions==0.4.1
PyQt5==5.11.3
PyQt5-sip==4.19.13
-e git+https://github.com/qutebrowser/PyQt5-stubs.git@wip#egg=PyQt5_stubs
typed-ast==1.1.0
typed-ast==1.1.1

View File

@ -4,4 +4,4 @@ colorama==0.4.1
cssutils==1.0.2
hunter==2.1.0
Pympler==0.6
six==1.11.0
six==1.12.0

View File

@ -3,6 +3,6 @@
appdirs==1.4.3
packaging==18.0
pyparsing==2.3.0
setuptools==40.6.2
six==1.11.0
setuptools==40.6.3
six==1.12.0
wheel==0.32.3

View File

@ -7,7 +7,7 @@ cffi==1.11.5
chardet==3.0.4
cryptography==2.4.2
github3.py==1.2.0
idna==2.7
idna==2.8
isort==4.3.4
jwcrypto==0.6.0
lazy-object-proxy==1.3.1
@ -16,8 +16,8 @@ pycparser==2.19
pylint==2.2.2
python-dateutil==2.7.5
./scripts/dev/pylint_checkers
requests==2.20.1
six==1.11.0
requests==2.21.0
six==1.12.0
uritemplate==3.0.0
urllib3==1.24.1
wrapt==1.10.11

View File

@ -0,0 +1,21 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
alabaster==0.7.12
Babel==2.6.0
certifi==2018.11.29
chardet==3.0.4
docutils==0.14
idna==2.8
imagesize==1.1.0
Jinja2==2.10
MarkupSafe==1.1.0
packaging==18.0
Pygments==2.3.1
pyparsing==2.3.0
pytz==2018.7
requests==2.21.0
six==1.12.0
snowballstemmer==1.2.1
Sphinx==1.8.3
sphinxcontrib-websupport==1.1.0
urllib3==1.24.1

View File

@ -0,0 +1 @@
sphinx

View File

@ -3,39 +3,39 @@
atomicwrites==1.2.1
attrs==18.2.0
backports.functools-lru-cache==1.5
beautifulsoup4==4.6.3
cheroot==6.5.2
beautifulsoup4==4.7.0
cheroot==6.5.3
Click==7.0
# colorama==0.4.1
coverage==4.5.2
EasyProcess==0.2.3
EasyProcess==0.2.5
Flask==1.0.2
glob2==0.6
hunter==2.1.0
hypothesis==3.82.1
hypothesis==3.85.2
itsdangerous==1.1.0
# Jinja2==2.10
Mako==1.0.7
# MarkupSafe==1.1.0
more-itertools==4.3.0
more-itertools==5.0.0
parse==1.9.0
parse-type==0.4.2
pluggy==0.8.0
py==1.7.0
py-cpuinfo==4.0.0
pytest==4.0.1
pytest-bdd==3.0.0
pytest==4.0.2
pytest-bdd==3.0.1
pytest-benchmark==3.1.1
pytest-cov==2.6.0
pytest-faulthandler==1.5.0
pytest-instafail==0.4.0
pytest-mock==1.10.0
pytest-qt==3.2.1
pytest-qt==3.2.2
pytest-repeat==0.7.0
pytest-rerunfailures==5.0
pytest-travis-fold==1.3.0
pytest-xvfb==1.1.0
PyVirtualDisplay==0.2.1
six==1.11.0
six==1.12.0
vulture==1.0
Werkzeug==0.14.1

View File

@ -3,7 +3,7 @@
filelock==3.0.10
pluggy==0.8.0
py==1.7.0
six==1.11.0
six==1.12.0
toml==0.10.0
tox==3.5.3
tox==3.6.1
virtualenv==16.1.0

View File

@ -64,6 +64,7 @@ die() {
javascript_escape() {
# print the first argument in an escaped way, such that it can safely
# be used within javascripts double quotes
# shellcheck disable=SC2001
sed "s,[\\\\'\"],\\\\&,g" <<< "$1"
}
@ -111,6 +112,7 @@ simplify_url() {
# are found:
no_entries_found() {
while [ 0 -eq "${#files[@]}" ] && [ -n "$simple_url" ]; do
# shellcheck disable=SC2001
shorter_simple_url=$(sed 's,^[^.]*\.,,' <<< "$simple_url")
if [ "$shorter_simple_url" = "$simple_url" ] ; then
# if no dot, then even remove the top level domain

View File

@ -97,13 +97,19 @@ def qute_command(command):
def find_pass_candidates(domain, password_store_path):
candidates = []
for path, directories, file_names in os.walk(password_store_path, followlinks=True):
if directories or domain not in path.split(os.path.sep):
secrets = fnmatch.filter(file_names, '*.gpg')
if not secrets:
continue
# Strip password store path prefix to get the relative pass path
pass_path = path[len(password_store_path) + 1:]
secrets = fnmatch.filter(file_names, '*.gpg')
candidates.extend(os.path.join(pass_path, os.path.splitext(secret)[0]) for secret in secrets)
split_path = pass_path.split(os.path.sep)
for secret in secrets:
secret_base = os.path.splitext(secret)[0]
if domain not in (split_path + [secret_base]):
continue
candidates.append(os.path.join(pass_path, secret_base))
return candidates

View File

@ -37,7 +37,7 @@ get_selection() {
# https://github.com/halfwit/dotfiles/blob/master/.config/dmenu/font
[[ -s $confdir/dmenu/font ]] && read -r font < "$confdir"/dmenu/font
[[ $font ]] && opts+=(-fn "$font")
[[ -n $font ]] && opts+=(-fn "$font")
# shellcheck source=/dev/null
[[ -s $optsfile ]] && source "$optsfile"
@ -46,7 +46,7 @@ url=$(get_selection)
url=${url/*http/http}
# If no selection is made, exit (escape pressed, e.g.)
[[ ! $url ]] && exit 0
[[ -z $url ]] && exit 0
case $1 in
open) printf '%s' "open $url" >> "$QUTE_FIFO" || qutebrowser "$url" ;;

View File

@ -18,10 +18,6 @@ disallow_untyped_decorators = True
# no_implicit_optional = True
# warn_return_any = True
[mypy-faulthandler]
# https://github.com/python/typeshed/pull/2627
ignore_missing_imports = True
[mypy-colorama]
# https://github.com/tartley/colorama/issues/206
ignore_missing_imports = True
@ -73,3 +69,19 @@ disallow_incomplete_defs = True
[mypy-qutebrowser.components.*]
disallow_untyped_defs = True
disallow_incomplete_defs = True
[mypy-qutebrowser.extensions.*]
disallow_untyped_defs = True
disallow_incomplete_defs = True
[mypy-qutebrowser.browser.webelem]
disallow_untyped_defs = True
disallow_incomplete_defs = True
[mypy-qutebrowser.browser.webkit.webkitelem]
disallow_untyped_defs = True
disallow_incomplete_defs = True
[mypy-qutebrowser.browser.webengine.webengineelem]
disallow_untyped_defs = True
disallow_incomplete_defs = True

View File

@ -64,6 +64,7 @@ qt_log_ignore =
^QSettings::value: Empty key passed
^Icon theme ".*" not found
^Error receiving trust for a CA certificate
^QBackingStore::endPaint\(\) called with active painter on backingstore paint device
xfail_strict = true
filterwarnings =
error

View File

@ -24,3 +24,4 @@ from qutebrowser.browser.browsertab import WebTabError, AbstractTab as Tab
from qutebrowser.browser.webelem import (Error as WebElemError,
AbstractWebElement as WebElement)
from qutebrowser.utils.usertypes import ClickTarget, JsWorld
from qutebrowser.extensions.loader import InitContext

View File

@ -17,7 +17,37 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Utilities for command handlers."""
"""qutebrowser has the concept of functions, exposed to the user as commands.
Creating a new command is straightforward::
from qutebrowser.api import cmdutils
@cmdutils.register(...)
def foo():
...
The commands arguments are automatically deduced by inspecting your function.
The types of the function arguments are inferred based on their default values,
e.g., an argument `foo=True` will be converted to a flag `-f`/`--foo` in
qutebrowser's commandline.
The type can be overridden using Python's function annotations::
@cmdutils.register(...)
def foo(bar: int, baz=True):
...
Possible values:
- A callable (``int``, ``float``, etc.): Gets called to validate/convert the
value.
- A python enum type: All members of the enum are possible values.
- A ``typing.Union`` of multiple types above: Any of these types are valid
values, e.g., ``typing.Union[str, int]``.
"""
import inspect
import typing
@ -33,14 +63,16 @@ class CommandError(cmdexc.Error):
"""Raised when a command encounters an error while running.
If your command handler encounters an error and cannot continue, raise this
exception with an appropriate error message:
exception with an appropriate error message::
raise cmdexc.CommandError("Message")
The message will then be shown in the qutebrowser status bar.
Note that you should only raise this exception while a command handler is
run. Raising it at another point causes qutebrowser to crash due to an
.. note::
You should only raise this exception while a command handler is run.
Raising it at another point causes qutebrowser to crash due to an
unhandled exception.
"""
@ -76,13 +108,7 @@ def check_exclusive(flags: typing.Iterable[bool],
class register: # noqa: N801,N806 pylint: disable=invalid-name
"""Decorator to register a new command handler.
Attributes:
_instance: The object from the object registry to be used as "self".
_name: The name (as string) or names (as list) of the command.
_kwargs: The arguments to pass to Command.
"""
"""Decorator to register a new command handler."""
def __init__(self, *,
instance: str = None,
@ -95,8 +121,11 @@ class register: # noqa: N801,N806 pylint: disable=invalid-name
Args:
See class attributes.
"""
# The object from the object registry to be used as "self".
self._instance = instance
# The name (as string) or names (as list) of the command.
self._name = name
# The arguments to pass to Command.
self._kwargs = kwargs
def __call__(self, func: typing.Callable) -> typing.Callable:
@ -127,16 +156,50 @@ class register: # noqa: N801,N806 pylint: disable=invalid-name
class argument: # noqa: N801,N806 pylint: disable=invalid-name
"""Decorator to customize an argument for @cmdutils.register.
"""Decorator to customize an argument.
Attributes:
_argname: The name of the argument to handle.
_kwargs: Keyword arguments, valid ArgInfo members
You can customize how an argument is handled using the
``@cmdutils.argument`` decorator *after* ``@cmdutils.register``. This can,
for example, be used to customize the flag an argument should get::
@cmdutils.register(...)
@cmdutils.argument('bar', flag='c')
def foo(bar):
...
For a ``str`` argument, you can restrict the allowed strings using
``choices``::
@cmdutils.register(...)
@cmdutils.argument('bar', choices=['val1', 'val2'])
def foo(bar: str):
...
For ``typing.Union`` types, the given ``choices`` are only checked if other
types (like ``int``) don't match.
The following arguments are supported for ``@cmdutils.argument``:
- ``flag``: Customize the short flag (``-x``) the argument will get.
- ``value``: Tell qutebrowser to fill the argument with special values:
* ``value=cmdutils.Value.count``: The ``count`` given by the user to the
command.
* ``value=cmdutils.Value.win_id``: The window ID of the current window.
* ``value=cmdutils.Value.cur_tab``: The tab object which is currently
focused.
- ``completion``: A completion function to use when completing arguments
for the given command.
- ``choices``: The allowed string choices for the argument.
The name of an argument will always be the parameter name, with any
trailing underscores stripped and underscores replaced by dashes.
"""
def __init__(self, argname: str, **kwargs: typing.Any) -> None:
self._argname = argname
self._kwargs = kwargs
self._argname = argname # The name of the argument to handle.
self._kwargs = kwargs # Valid ArgInfo members.
def __call__(self, func: typing.Callable) -> typing.Callable:
funcname = func.__name__

View File

@ -21,9 +21,23 @@
import typing
MYPY = False
if MYPY:
# pylint: disable=unused-import,useless-suppression
from qutebrowser.config import config
from PyQt5.QtCore import QUrl
from qutebrowser.config import config
#: Simplified access to config values using attribute acccess.
#: For example, to access the ``content.javascript.enabled`` setting,
#: you can do::
#:
#: if config.val.content.javascript.enabled:
#: ...
#:
#: This also supports setting configuration values::
#:
#: config.val.content.javascript.enabled = False
val = typing.cast('config.ConfigContainer', None)
def get(name: str, url: QUrl = None) -> typing.Any:
"""Get a value from the config based on a string name."""
return config.instance.get(name, url)

View File

@ -0,0 +1,75 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2018 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 <http://www.gnu.org/licenses/>.
"""APIs related to downloading files."""
import io
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, QUrl
from qutebrowser.browser import downloads, qtnetworkdownloads
from qutebrowser.utils import objreg
class TempDownload(QObject):
"""A download of some data into a file object."""
finished = pyqtSignal()
def __init__(self, item: qtnetworkdownloads.DownloadItem) -> None:
super().__init__()
self._item = item
self._item.finished.connect(self._on_download_finished)
self.successful = False
self.fileobj = item.fileobj
@pyqtSlot()
def _on_download_finished(self) -> None:
self.successful = self._item.successful
self.finished.emit()
def download_temp(url: QUrl) -> TempDownload:
"""Download the given URL into a file object.
The download is not saved to disk.
Returns a ``TempDownload`` object, which triggers a ``finished`` signal
when the download has finished::
dl = downloads.download_temp(QUrl("https://www.example.com/"))
dl.finished.connect(functools.partial(on_download_finished, dl))
After the download has finished, its ``successful`` attribute can be
checked to make sure it finished successfully. If so, its contents can be
read by accessing the ``fileobj`` attribute::
def on_download_finished(download: downloads.TempDownload) -> None:
if download.successful:
print(download.fileobj.read())
download.fileobj.close()
"""
fobj = io.BytesIO()
fobj.name = 'temporary: ' + url.host()
target = downloads.FileObjDownloadTarget(fobj)
download_manager = objreg.get('qtnetwork-download-manager')
return download_manager.get(url, target=target, auto_remove=True)

92
qutebrowser/api/hook.py Normal file
View File

@ -0,0 +1,92 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2018 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 <http://www.gnu.org/licenses/>.
# pylint: disable=invalid-name
"""Hooks for extensions."""
import importlib
import typing
from qutebrowser.extensions import loader
def _add_module_info(func: typing.Callable) -> loader.ModuleInfo:
"""Add module info to the given function."""
module = importlib.import_module(func.__module__)
return loader.add_module_info(module)
class init:
"""Decorator to mark a function to run when initializing.
The decorated function gets called with a
:class:`qutebrowser.api.apitypes.InitContext` as argument.
Example::
@hook.init()
def init(_context):
message.info("Extension initialized.")
"""
def __call__(self, func: typing.Callable) -> typing.Callable:
info = _add_module_info(func)
if info.init_hook is not None:
raise ValueError("init hook is already registered!")
info.init_hook = func
return func
class config_changed:
"""Decorator to get notified about changed configs.
By default, the decorated function is called when any change in the config
occurs::
@hook.config_changed()
def on_config_changed():
...
When an option name is passed, it's only called when the given option was
changed::
@hook.config_changed('content.javascript.enabled')
def on_config_changed():
...
Alternatively, a part of an option name can be specified. In the following
snippet, ``on_config_changed`` gets called when either
``bindings.commands`` or ``bindings.key_mappings`` have changed::
@hook.config_changed('bindings')
def on_config_changed():
...
"""
def __init__(self, option_filter: str = None) -> None:
self._filter = option_filter
def __call__(self, func: typing.Callable) -> typing.Callable:
info = _add_module_info(func)
info.config_changed_hooks.append((self._filter, func))
return func

View File

@ -0,0 +1,43 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2018 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 <http://www.gnu.org/licenses/>.
"""APIs related to intercepting/blocking requests."""
from qutebrowser.extensions import interceptors
# pylint: disable=unused-import
from qutebrowser.extensions.interceptors import Request
#: Type annotation for an interceptor function.
InterceptorType = interceptors.InterceptorType
def register(interceptor: InterceptorType) -> None:
"""Register a request interceptor.
Whenever a request happens, the interceptor gets called with a
:class:`Request` object.
Example::
def intercept(request: interceptor.Request) -> None:
if request.request_url.host() == 'badhost.example.com':
request.block()
"""
interceptors.register(interceptor)

View File

@ -63,11 +63,12 @@ from qutebrowser.completion.models import miscmodels
from qutebrowser.commands import runners
from qutebrowser.api import cmdutils
from qutebrowser.config import config, websettings, configfiles, configinit
from qutebrowser.browser import (urlmarks, adblock, history, browsertab,
from qutebrowser.browser import (urlmarks, history, browsertab,
qtnetworkdownloads, downloads, greasemonkey)
from qutebrowser.browser.network import proxy
from qutebrowser.browser.webkit import cookies, cache
from qutebrowser.browser.webkit.network import networkmanager
from qutebrowser.extensions import loader
from qutebrowser.keyinput import macros
from qutebrowser.mainwindow import mainwindow, prompt
from qutebrowser.misc import (readline, ipc, savemanager, sessions,
@ -77,8 +78,6 @@ from qutebrowser.utils import (log, version, message, utils, urlutils, objreg,
usertypes, standarddir, error, qtutils)
# pylint: disable=unused-import
# We import those to run the cmdutils.register decorators.
from qutebrowser.components import (scrollcommands, caretcommands,
zoomcommands, misccommands)
from qutebrowser.mainwindow.statusbar import command
from qutebrowser.misc import utilcmds
# pylint: enable=unused-import
@ -166,6 +165,8 @@ def init(args, crash_handler):
qApp.setQuitOnLastWindowClosed(False)
_init_icon()
loader.init()
loader.load_components()
try:
_init_modules(args, crash_handler)
except (OSError, UnicodeDecodeError, browsertab.WebTabError) as e:
@ -468,11 +469,6 @@ def _init_modules(args, crash_handler):
log.init.debug("Initializing websettings...")
websettings.init(args)
log.init.debug("Initializing adblock...")
host_blocker = adblock.HostBlocker()
host_blocker.read_hosts()
objreg.register('host-blocker', host_blocker)
log.init.debug("Initializing quickmarks...")
quickmark_manager = urlmarks.QuickmarkManager(qApp)
objreg.register('quickmark-manager', quickmark_manager)

View File

@ -141,14 +141,11 @@ class TabData:
class AbstractAction:
"""Attribute of AbstractTab for Qt WebActions.
Class attributes (overridden by subclasses):
action_class: The class actions are defined on (QWeb{Engine,}Page)
action_base: The type of the actions (QWeb{Engine,}Page.WebAction)
"""
"""Attribute ``action`` of AbstractTab for Qt WebActions."""
# The class actions are defined on (QWeb{Engine,}Page)
action_class = None # type: type
# The type of the actions (QWeb{Engine,}Page.WebAction)
action_base = None # type: type
def __init__(self, tab: 'AbstractTab') -> None:
@ -200,7 +197,7 @@ class AbstractAction:
class AbstractPrinting:
"""Attribute of AbstractTab for printing the page."""
"""Attribute ``printing`` of AbstractTab for printing the page."""
def __init__(self, tab: 'AbstractTab') -> None:
self._widget = None
@ -271,7 +268,7 @@ class AbstractPrinting:
class AbstractSearch(QObject):
"""Attribute of AbstractTab for doing searches.
"""Attribute ``search`` of AbstractTab for doing searches.
Attributes:
text: The last thing this view was searched for.
@ -279,15 +276,14 @@ class AbstractSearch(QObject):
this view.
_flags: The flags of the last search (needs to be set by subclasses).
_widget: The underlying WebView widget.
Signals:
finished: Emitted when a search was finished.
arg: True if the text was found, False otherwise.
cleared: Emitted when an existing search was cleared.
"""
#: Signal emitted when a search was finished
#: (True if the text was found, False otherwise)
finished = pyqtSignal(bool)
#: Signal emitted when an existing search was cleared.
cleared = pyqtSignal()
_Callback = typing.Callable[[bool], None]
def __init__(self, tab: 'AbstractTab', parent: QWidget = None):
@ -350,17 +346,13 @@ class AbstractSearch(QObject):
class AbstractZoom(QObject):
"""Attribute of AbstractTab for controlling zoom.
Attributes:
_neighborlist: A NeighborList with the zoom levels.
_default_zoom_changed: Whether the zoom was changed from the default.
"""
"""Attribute ``zoom`` of AbstractTab for controlling zoom."""
def __init__(self, tab: 'AbstractTab', parent: QWidget = None) -> None:
super().__init__(parent)
self._tab = tab
self._widget = None
# Whether zoom was changed from the default.
self._default_zoom_changed = False
self._init_neighborlist()
config.instance.changed.connect(self._on_config_changed)
@ -375,7 +367,9 @@ class AbstractZoom(QObject):
self._init_neighborlist()
def _init_neighborlist(self) -> None:
"""Initialize self._neighborlist."""
"""Initialize self._neighborlist.
It is a NeighborList with the zoom levels."""
levels = config.val.zoom.levels
self._neighborlist = usertypes.NeighborList(
levels, mode=usertypes.NeighborList.Modes.edge)
@ -427,15 +421,12 @@ class AbstractZoom(QObject):
class AbstractCaret(QObject):
"""Attribute of AbstractTab for caret browsing.
Signals:
selection_toggled: Emitted when the selection was toggled.
arg: Whether the selection is now active.
follow_selected_done: Emitted when a follow_selection action is done.
"""
"""Attribute ``caret`` of AbstractTab for caret browsing."""
#: Signal emitted when the selection was toggled.
#: (argument - whether the selection is now active)
selection_toggled = pyqtSignal(bool)
#: Emitted when a ``follow_selection`` action is done.
follow_selected_done = pyqtSignal()
def __init__(self,
@ -522,16 +513,12 @@ class AbstractCaret(QObject):
class AbstractScroller(QObject):
"""Attribute of AbstractTab to manage scroll position.
Signals:
perc_changed: The scroll position changed.
before_jump_requested:
Emitted by other code when the user requested a jump.
Used to set the special ' mark so the user can return.
"""
"""Attribute ``scroller`` of AbstractTab to manage scroll position."""
#: Signal emitted when the scroll position changed (int, int)
perc_changed = pyqtSignal(int, int)
#: Signal emitted before the user requested a jump.
#: Used to set the special ' mark so the user can return.
before_jump_requested = pyqtSignal()
def __init__(self, tab: 'AbstractTab', parent: QWidget = None):
@ -833,42 +820,46 @@ class AbstractTabPrivate:
class AbstractTab(QWidget):
"""An adapter for QWebView/QWebEngineView representing a single tab.
Signals:
See related Qt signals.
new_tab_requested: Emitted when a new tab should be opened with the
given URL.
load_status_changed: The loading status changed
fullscreen_requested: Fullscreen display was requested by the page.
arg: True if fullscreen should be turned on,
False if it should be turned off.
renderer_process_terminated: Emitted when the underlying renderer
process terminated.
arg 0: A TerminationStatus member.
arg 1: The exit code.
before_load_started: Emitted before we tell Qt to open a URL.
"""
"""An adapter for QWebView/QWebEngineView representing a single tab."""
#: Signal emitted when a website requests to close this tab.
window_close_requested = pyqtSignal()
#: Signal emitted when a link is hovered (the hover text)
link_hovered = pyqtSignal(str)
#: Signal emitted when a page started loading
load_started = pyqtSignal()
#: Signal emitted when a page is loading (progress percentage)
load_progress = pyqtSignal(int)
#: Signal emitted when a page finished loading (success as bool)
load_finished = pyqtSignal(bool)
#: Signal emitted when a page's favicon changed (icon as QIcon)
icon_changed = pyqtSignal(QIcon)
#: Signal emitted when a page's title changed (new title as str)
title_changed = pyqtSignal(str)
load_status_changed = pyqtSignal(usertypes.LoadStatus)
#: Signal emitted when a new tab should be opened (url as QUrl)
new_tab_requested = pyqtSignal(QUrl)
#: Signal emitted when a page's URL changed (url as QUrl)
url_changed = pyqtSignal(QUrl)
shutting_down = pyqtSignal()
#: Signal emitted when a tab's content size changed
#: (new size as QSizeF)
contents_size_changed = pyqtSignal(QSizeF)
# url, requested url, title
history_item_triggered = pyqtSignal(QUrl, QUrl, str)
#: Signal emitted when a page requested full-screen (bool)
fullscreen_requested = pyqtSignal(bool)
renderer_process_terminated = pyqtSignal(TerminationStatus, int)
#: Signal emitted before load starts (URL as QUrl)
before_load_started = pyqtSignal(QUrl)
# Signal emitted when a page's load status changed
# (argument: usertypes.LoadStatus)
load_status_changed = pyqtSignal(usertypes.LoadStatus)
# Signal emitted before shutting down
shutting_down = pyqtSignal()
# Signal emitted when a history item should be added
history_item_triggered = pyqtSignal(QUrl, QUrl, str)
# Signal emitted when the underlying renderer process terminated.
# arg 0: A TerminationStatus member.
# arg 1: The exit code.
renderer_process_terminated = pyqtSignal(TerminationStatus, int)
def __init__(self, *, win_id: int, private: bool,
parent: QWidget = None) -> None:
self.is_private = private
@ -952,6 +943,10 @@ class AbstractTab(QWidget):
evt.posted = True
QApplication.postEvent(recipient, evt)
def navigation_blocked(self) -> bool:
"""Test if navigation is allowed on the current tab."""
return self.data.pinned and config.val.tabs.pinned.frozen
@pyqtSlot(QUrl)
def _on_before_load_started(self, url: QUrl) -> None:
"""Adjust the title if we are going to visit a URL soon."""

View File

@ -34,7 +34,7 @@ from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate,
webelem, downloads)
from qutebrowser.keyinput import modeman, keyutils
from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
objreg, utils, standarddir)
objreg, utils, standarddir, debug)
from qutebrowser.utils.usertypes import KeyMode
from qutebrowser.misc import editor, guiprocess, objects
from qutebrowser.completion.models import urlmodel, miscmodels
@ -316,7 +316,7 @@ class CommandDispatcher:
else:
# Explicit count with a tab that doesn't exist.
return
elif curtab.data.pinned:
elif curtab.navigation_blocked():
message.info("Tab is pinned!")
else:
curtab.load_url(cur_url)
@ -1721,4 +1721,10 @@ class CommandDispatcher:
return
window = self._tabbed_browser.widget.window()
if not window.isFullScreen():
window.state_before_fullscreen = window.windowState()
window.setWindowState(window.windowState() ^ Qt.WindowFullScreen)
log.misc.debug('state before fullscreen: {}'.format(
debug.qflags_key(Qt, window.state_before_fullscreen)))

View File

@ -75,6 +75,7 @@ class DownloadView(QListView):
def __init__(self, win_id, parent=None):
super().__init__(parent)
if not utils.is_mac:
self.setStyle(QStyleFactory.create('Fusion'))
config.set_register_stylesheet(self)
self.setResizeMode(QListView.Adjust)

View File

@ -180,6 +180,8 @@ class PACResolver:
"""
self._engine = QJSEngine()
self._engine.installExtensions(QJSEngine.ConsoleExtension)
self._ctx = _PACContext(self._engine)
self._engine.globalObject().setProperty(
"PAC", self._engine.newQObject(self._ctx))

View File

@ -19,15 +19,23 @@
"""Generic web element related code."""
import typing
import collections.abc
from PyQt5.QtCore import QUrl, Qt, QEvent, QTimer
from PyQt5.QtCore import QUrl, Qt, QEvent, QTimer, QRect, QPoint
from PyQt5.QtGui import QMouseEvent
from qutebrowser.config import config
from qutebrowser.keyinput import modeman
from qutebrowser.mainwindow import mainwindow
from qutebrowser.utils import log, usertypes, utils, qtutils, objreg
MYPY = False
if MYPY:
# pylint: disable=unused-import,useless-suppression
from qutebrowser.browser import browsertab
JsValueType = typing.Union[int, float, str, None]
class Error(Exception):
@ -40,7 +48,7 @@ class OrphanedError(Error):
"""Raised when a webelement's parent has vanished."""
def css_selector(group, url):
def css_selector(group: str, url: QUrl) -> str:
"""Get a CSS selector for the given group/URL."""
selectors = config.instance.get('hints.selectors', url)
if group not in selectors:
@ -54,76 +62,74 @@ def css_selector(group, url):
class AbstractWebElement(collections.abc.MutableMapping):
"""A wrapper around QtWebKit/QtWebEngine web element.
"""A wrapper around QtWebKit/QtWebEngine web element."""
Attributes:
tab: The tab associated with this element.
"""
def __init__(self, tab):
def __init__(self, tab: 'browsertab.AbstractTab') -> None:
self._tab = tab
def __eq__(self, other):
def __eq__(self, other: object) -> bool:
raise NotImplementedError
def __str__(self):
def __str__(self) -> str:
raise NotImplementedError
def __getitem__(self, key):
def __getitem__(self, key: str) -> str:
raise NotImplementedError
def __setitem__(self, key, val):
def __setitem__(self, key: str, val: str) -> None:
raise NotImplementedError
def __delitem__(self, key):
def __delitem__(self, key: str) -> None:
raise NotImplementedError
def __iter__(self):
def __iter__(self) -> typing.Iterator[str]:
raise NotImplementedError
def __len__(self):
def __len__(self) -> int:
raise NotImplementedError
def __repr__(self):
def __repr__(self) -> str:
try:
html = utils.compact_text(self.outer_xml(), 500)
except Error:
html = None
return utils.get_repr(self, html=html)
def has_frame(self):
def has_frame(self) -> bool:
"""Check if this element has a valid frame attached."""
raise NotImplementedError
def geometry(self):
def geometry(self) -> QRect:
"""Get the geometry for this element."""
raise NotImplementedError
def classes(self):
def classes(self) -> typing.List[str]:
"""Get a list of classes assigned to this element."""
raise NotImplementedError
def tag_name(self):
def tag_name(self) -> str:
"""Get the tag name of this element.
The returned name will always be lower-case.
"""
raise NotImplementedError
def outer_xml(self):
def outer_xml(self) -> str:
"""Get the full HTML representation of this element."""
raise NotImplementedError
def value(self):
def value(self) -> JsValueType:
"""Get the value attribute for this element, or None."""
raise NotImplementedError
def set_value(self, value):
def set_value(self, value: JsValueType) -> None:
"""Set the element value."""
raise NotImplementedError
def dispatch_event(self, event, bubbles=False,
cancelable=False, composed=False):
def dispatch_event(self, event: str,
bubbles: bool = False,
cancelable: bool = False,
composed: bool = False) -> None:
"""Dispatch an event to the element.
Args:
@ -134,35 +140,25 @@ class AbstractWebElement(collections.abc.MutableMapping):
"""
raise NotImplementedError
def insert_text(self, text):
def insert_text(self, text: str) -> None:
"""Insert the given text into the element."""
raise NotImplementedError
def rect_on_view(self, *, elem_geometry=None, no_js=False):
def rect_on_view(self, *, elem_geometry: QRect = None,
no_js: bool = False) -> QRect:
"""Get the geometry of the element relative to the webview.
Uses the getClientRects() JavaScript method to obtain the collection of
rectangles containing the element and returns the first rectangle which
is large enough (larger than 1px times 1px). If all rectangles returned
by getClientRects() are too small, falls back to elem.rect_on_view().
Skipping of small rectangles is due to <a> elements containing other
elements with "display:block" style, see
https://github.com/qutebrowser/qutebrowser/issues/1298
Args:
elem_geometry: The geometry of the element, or None.
Calling QWebElement::geometry is rather expensive so
we want to avoid doing it twice.
no_js: Fall back to the Python implementation
no_js: Fall back to the Python implementation.
"""
raise NotImplementedError
def is_writable(self):
def is_writable(self) -> bool:
"""Check whether an element is writable."""
return not ('disabled' in self or 'readonly' in self)
def is_content_editable(self):
def is_content_editable(self) -> bool:
"""Check if an element has a contenteditable attribute.
Args:
@ -177,7 +173,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
except KeyError:
return False
def _is_editable_object(self):
def _is_editable_object(self) -> bool:
"""Check if an object-element is editable."""
if 'type' not in self:
log.webelem.debug("<object> without type clicked...")
@ -193,7 +189,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
# Image/Audio/...
return False
def _is_editable_input(self):
def _is_editable_input(self) -> bool:
"""Check if an input-element is editable.
Return:
@ -210,7 +206,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
else:
return False
def _is_editable_classes(self):
def _is_editable_classes(self) -> bool:
"""Check if an element is editable based on its classes.
Return:
@ -229,7 +225,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
return True
return False
def is_editable(self, strict=False):
def is_editable(self, strict: bool = False) -> bool:
"""Check whether we should switch to insert mode for this element.
Args:
@ -260,17 +256,17 @@ class AbstractWebElement(collections.abc.MutableMapping):
return self._is_editable_classes() and not strict
return False
def is_text_input(self):
def is_text_input(self) -> bool:
"""Check if this element is some kind of text box."""
roles = ('combobox', 'textbox')
tag = self.tag_name()
return self.get('role', None) in roles or tag in ['input', 'textarea']
def remove_blank_target(self):
def remove_blank_target(self) -> None:
"""Remove target from link."""
raise NotImplementedError
def resolve_url(self, baseurl):
def resolve_url(self, baseurl: QUrl) -> typing.Optional[QUrl]:
"""Resolve the URL in the element's src/href attribute.
Args:
@ -297,16 +293,16 @@ class AbstractWebElement(collections.abc.MutableMapping):
qtutils.ensure_valid(url)
return url
def is_link(self):
def is_link(self) -> bool:
"""Return True if this AbstractWebElement is a link."""
href_tags = ['a', 'area', 'link']
return self.tag_name() in href_tags and 'href' in self
def _requires_user_interaction(self):
def _requires_user_interaction(self) -> bool:
"""Return True if clicking this element needs user interaction."""
raise NotImplementedError
def _mouse_pos(self):
def _mouse_pos(self) -> QPoint:
"""Get the position to click/hover."""
# Click the center of the largest square fitting into the top/left
# corner of the rectangle, this will help if part of the <a> element
@ -322,35 +318,38 @@ class AbstractWebElement(collections.abc.MutableMapping):
raise Error("Element position is out of view!")
return pos
def _move_text_cursor(self):
def _move_text_cursor(self) -> None:
"""Move cursor to end after clicking."""
raise NotImplementedError
def _click_fake_event(self, click_target):
def _click_fake_event(self, click_target: usertypes.ClickTarget) -> None:
"""Send a fake click event to the element."""
pos = self._mouse_pos()
log.webelem.debug("Sending fake click to {!r} at position {} with "
"target {}".format(self, pos, click_target))
modifiers = {
target_modifiers = {
usertypes.ClickTarget.normal: Qt.NoModifier,
usertypes.ClickTarget.window: Qt.AltModifier | Qt.ShiftModifier,
usertypes.ClickTarget.tab: Qt.ControlModifier,
usertypes.ClickTarget.tab_bg: Qt.ControlModifier,
}
if config.val.tabs.background:
modifiers[usertypes.ClickTarget.tab] |= Qt.ShiftModifier
target_modifiers[usertypes.ClickTarget.tab] |= Qt.ShiftModifier
else:
modifiers[usertypes.ClickTarget.tab_bg] |= Qt.ShiftModifier
target_modifiers[usertypes.ClickTarget.tab_bg] |= Qt.ShiftModifier
modifiers = typing.cast(Qt.KeyboardModifiers,
target_modifiers[click_target])
events = [
QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
Qt.NoModifier),
QMouseEvent(QEvent.MouseButtonPress, pos, Qt.LeftButton,
Qt.LeftButton, modifiers[click_target]),
Qt.LeftButton, modifiers),
QMouseEvent(QEvent.MouseButtonRelease, pos, Qt.LeftButton,
Qt.NoButton, modifiers[click_target]),
Qt.NoButton, modifiers),
]
for evt in events:
@ -358,15 +357,15 @@ class AbstractWebElement(collections.abc.MutableMapping):
QTimer.singleShot(0, self._move_text_cursor)
def _click_editable(self, click_target):
def _click_editable(self, click_target: usertypes.ClickTarget) -> None:
"""Fake a click on an editable input field."""
raise NotImplementedError
def _click_js(self, click_target):
def _click_js(self, click_target: usertypes.ClickTarget) -> None:
"""Fake a click by using the JS .click() method."""
raise NotImplementedError
def _click_href(self, click_target):
def _click_href(self, click_target: usertypes.ClickTarget) -> None:
"""Fake a click on an element with a href by opening the link."""
baseurl = self._tab.url()
url = self.resolve_url(baseurl)
@ -388,7 +387,8 @@ class AbstractWebElement(collections.abc.MutableMapping):
else:
raise ValueError("Unknown ClickTarget {}".format(click_target))
def click(self, click_target, *, force_event=False):
def click(self, click_target: usertypes.ClickTarget, *,
force_event: bool = False) -> None:
"""Simulate a click on the element.
Args:
@ -425,7 +425,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
else:
raise ValueError("Unknown ClickTarget {}".format(click_target))
def hover(self):
def hover(self) -> None:
"""Simulate a mouse hover over the element."""
pos = self._mouse_pos()
event = QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,

View File

@ -26,15 +26,15 @@ from PyQt5.QtWebEngineCore import (QWebEngineUrlRequestInterceptor,
from qutebrowser.config import config
from qutebrowser.browser import shared
from qutebrowser.utils import utils, log, debug
from qutebrowser.extensions import interceptors
class RequestInterceptor(QWebEngineUrlRequestInterceptor):
"""Handle ad blocking and custom headers."""
def __init__(self, host_blocker, args, parent=None):
def __init__(self, args, parent=None):
super().__init__(parent)
self._host_blocker = host_blocker
self._args = args
def install(self, profile):
@ -84,9 +84,10 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor):
return
# FIXME:qtwebengine only block ads for NavigationTypeOther?
if self._host_blocker.is_blocked(url, first_party):
log.webview.info("Request to {} blocked by host blocker.".format(
url.host()))
request = interceptors.Request(first_party_url=first_party,
request_url=url)
interceptors.run(request)
if request.is_blocked:
info.block(True)
for header, value in shared.custom_headers(url=url):

View File

@ -22,20 +22,27 @@
"""QtWebEngine specific part of the web element API."""
import typing
from PyQt5.QtCore import QRect, Qt, QPoint, QEventLoop
from PyQt5.QtGui import QMouseEvent
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebEngineWidgets import QWebEngineSettings
from qutebrowser.utils import log, javascript, urlutils
from qutebrowser.utils import log, javascript, urlutils, usertypes
from qutebrowser.browser import webelem
MYPY = False
if MYPY:
# pylint: disable=unused-import,useless-suppression
from qutebrowser.browser.webengine import webenginetab
class WebEngineElement(webelem.AbstractWebElement):
"""A web element for QtWebEngine, using JS under the hood."""
def __init__(self, js_dict, tab):
def __init__(self, js_dict: typing.Dict[str, typing.Any],
tab: 'webenginetab.WebEngineTab') -> None:
super().__init__(tab)
# Do some sanity checks on the data we get from JS
js_dict_types = {
@ -48,7 +55,7 @@ class WebEngineElement(webelem.AbstractWebElement):
'rects': list,
'attributes': dict,
'caret_position': (int, type(None)),
}
} # type: typing.Dict[str, typing.Union[type, typing.Tuple[type,...]]]
assert set(js_dict.keys()).issubset(js_dict_types.keys())
for name, typ in js_dict_types.items():
if name in js_dict and not isinstance(js_dict[name], typ):
@ -73,50 +80,51 @@ class WebEngineElement(webelem.AbstractWebElement):
self._id = js_dict['id']
self._js_dict = js_dict
def __str__(self):
def __str__(self) -> str:
return self._js_dict.get('text', '')
def __eq__(self, other):
def __eq__(self, other: object) -> bool:
if not isinstance(other, WebEngineElement):
return NotImplemented
return self._id == other._id # pylint: disable=protected-access
def __getitem__(self, key):
def __getitem__(self, key: str) -> str:
attrs = self._js_dict['attributes']
return attrs[key]
def __setitem__(self, key, val):
def __setitem__(self, key: str, val: str) -> None:
self._js_dict['attributes'][key] = val
self._js_call('set_attribute', key, val)
def __delitem__(self, key):
def __delitem__(self, key: str) -> None:
log.stub()
def __iter__(self):
def __iter__(self) -> typing.Iterator[str]:
return iter(self._js_dict['attributes'])
def __len__(self):
def __len__(self) -> int:
return len(self._js_dict['attributes'])
def _js_call(self, name, *args, callback=None):
def _js_call(self, name: str, *args: webelem.JsValueType,
callback: typing.Callable[[typing.Any], None] = None) -> None:
"""Wrapper to run stuff from webelem.js."""
if self._tab.is_deleted():
raise webelem.OrphanedError("Tab containing element vanished")
js_code = javascript.assemble('webelem', name, self._id, *args)
self._tab.run_js_async(js_code, callback=callback)
def has_frame(self):
def has_frame(self) -> bool:
return True
def geometry(self):
def geometry(self) -> QRect:
log.stub()
return QRect()
def classes(self):
def classes(self) -> typing.List[str]:
"""Get a list of classes assigned to this element."""
return self._js_dict['class_name'].split()
def tag_name(self):
def tag_name(self) -> str:
"""Get the tag name of this element.
The returned name will always be lower-case.
@ -125,34 +133,37 @@ class WebEngineElement(webelem.AbstractWebElement):
assert isinstance(tag, str), tag
return tag.lower()
def outer_xml(self):
def outer_xml(self) -> str:
"""Get the full HTML representation of this element."""
return self._js_dict['outer_xml']
def value(self):
def value(self) -> webelem.JsValueType:
return self._js_dict.get('value', None)
def set_value(self, value):
def set_value(self, value: webelem.JsValueType) -> None:
self._js_call('set_value', value)
def dispatch_event(self, event, bubbles=False,
cancelable=False, composed=False):
def dispatch_event(self, event: str,
bubbles: bool = False,
cancelable: bool = False,
composed: bool = False) -> None:
self._js_call('dispatch_event', event, bubbles, cancelable, composed)
def caret_position(self):
def caret_position(self) -> typing.Optional[int]:
"""Get the text caret position for the current element.
If the element is not a text element, None is returned.
"""
return self._js_dict.get('caret_position', None)
def insert_text(self, text):
def insert_text(self, text: str) -> None:
if not self.is_editable(strict=True):
raise webelem.Error("Element is not editable!")
log.webelem.debug("Inserting text into element {!r}".format(self))
self._js_call('insert_text', text)
def rect_on_view(self, *, elem_geometry=None, no_js=False):
def rect_on_view(self, *, elem_geometry: QRect = None,
no_js: bool = False) -> QRect:
"""Get the geometry of the element relative to the webview.
Skipping of small rectangles is due to <a> elements containing other
@ -193,16 +204,16 @@ class WebEngineElement(webelem.AbstractWebElement):
self, rects))
return QRect()
def remove_blank_target(self):
def remove_blank_target(self) -> None:
if self._js_dict['attributes'].get('target') == '_blank':
self._js_dict['attributes']['target'] = '_top'
self._js_call('remove_blank_target')
def _move_text_cursor(self):
def _move_text_cursor(self) -> None:
if self.is_text_input() and self.is_editable():
self._js_call('move_cursor_to_end')
def _requires_user_interaction(self):
def _requires_user_interaction(self) -> bool:
baseurl = self._tab.url()
url = self.resolve_url(baseurl)
if url is None:
@ -211,7 +222,7 @@ class WebEngineElement(webelem.AbstractWebElement):
return False
return url.scheme() not in urlutils.WEBENGINE_SCHEMES
def _click_editable(self, click_target):
def _click_editable(self, click_target: usertypes.ClickTarget) -> None:
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58515
ev = QMouseEvent(QMouseEvent.MouseButtonPress, QPoint(0, 0),
QPoint(0, 0), QPoint(0, 0), Qt.NoButton, Qt.NoButton,
@ -221,10 +232,11 @@ class WebEngineElement(webelem.AbstractWebElement):
self._js_call('focus')
self._move_text_cursor()
def _click_js(self, _click_target):
def _click_js(self, _click_target: usertypes.ClickTarget) -> None:
# FIXME:qtwebengine Have a proper API for this
# pylint: disable=protected-access
view = self._tab._widget
assert view is not None
# pylint: enable=protected-access
attribute = QWebEngineSettings.JavascriptCanOpenWindows
could_open_windows = view.settings().testAttribute(attribute)
@ -238,8 +250,9 @@ class WebEngineElement(webelem.AbstractWebElement):
qapp.processEvents(QEventLoop.ExcludeSocketNotifiers |
QEventLoop.ExcludeUserInputEvents)
def reset_setting(_arg):
def reset_setting(_arg: typing.Any) -> None:
"""Set the JavascriptCanOpenWindows setting to its old value."""
assert view is not None
try:
view.settings().setAttribute(attribute, could_open_windows)
except RuntimeError:

View File

@ -22,6 +22,11 @@
from PyQt5.QtCore import QBuffer, QIODevice, QUrl
from PyQt5.QtWebEngineCore import (QWebEngineUrlSchemeHandler,
QWebEngineUrlRequestJob)
try:
from PyQt5.QtWebEngineCore import QWebEngineUrlScheme # type: ignore
except ImportError:
# Added in Qt 5.12
QWebEngineUrlScheme = None
from qutebrowser.browser import qutescheme
from qutebrowser.utils import log, qtutils
@ -33,8 +38,12 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
def install(self, profile):
"""Install the handler for qute:// URLs on the given profile."""
if QWebEngineUrlScheme is not None:
assert QWebEngineUrlScheme.schemeByName(b'qute') is not None
profile.installUrlSchemeHandler(b'qute', self)
if qtutils.version_check('5.11', compiled=False):
if (qtutils.version_check('5.11', compiled=False) and
not qtutils.version_check('5.12', compiled=False)):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-63378
profile.installUrlSchemeHandler(b'chrome-error', self)
profile.installUrlSchemeHandler(b'chrome-extension', self)
@ -130,3 +139,16 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
buf.seek(0)
buf.close()
job.reply(mimetype.encode('ascii'), buf)
def init():
"""Register the qute:// scheme.
Note this needs to be called early, before constructing any QtWebEngine
classes.
"""
if QWebEngineUrlScheme is not None:
scheme = QWebEngineUrlScheme(b'qute')
scheme.setFlags(QWebEngineUrlScheme.LocalScheme |
QWebEngineUrlScheme.LocalAccessAllowed)
QWebEngineUrlScheme.registerScheme(scheme)

View File

@ -30,7 +30,7 @@ from PyQt5.QtGui import QFont
from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile,
QWebEnginePage)
from qutebrowser.browser.webengine import spell
from qutebrowser.browser.webengine import spell, webenginequtescheme
from qutebrowser.config import config, websettings
from qutebrowser.config.websettings import AttributeInfo as Attr
from qutebrowser.utils import utils, standarddir, qtutils, message, log
@ -298,6 +298,7 @@ def init(args):
not hasattr(QWebEnginePage, 'setInspectedPage')): # only Qt < 5.11
os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = str(utils.random_port())
webenginequtescheme.init()
spell.init()
_init_profiles()

View File

@ -60,10 +60,8 @@ def init():
_qute_scheme_handler.install(webenginesettings.private_profile)
log.init.debug("Initializing request interceptor...")
host_blocker = objreg.get('host-blocker')
args = objreg.get('args')
req_interceptor = interceptor.RequestInterceptor(
host_blocker, args=args, parent=app)
req_interceptor = interceptor.RequestInterceptor(args=args, parent=app)
req_interceptor.install(webenginesettings.default_profile)
req_interceptor.install(webenginesettings.private_profile)

View File

@ -39,6 +39,7 @@ from PyQt5.QtCore import QUrl
from qutebrowser.browser import downloads
from qutebrowser.browser.webkit import webkitelem
from qutebrowser.utils import log, objreg, message, usertypes, utils, urlutils
from qutebrowser.extensions import interceptors
@attr.s
@ -354,8 +355,9 @@ class _Downloader:
# qute, see the comments/discussion on
# https://github.com/qutebrowser/qutebrowser/pull/962#discussion_r40256987
# and https://github.com/qutebrowser/qutebrowser/issues/1053
host_blocker = objreg.get('host-blocker')
if host_blocker.is_blocked(url):
request = interceptors.Request(first_party_url=None, request_url=url)
interceptors.run(request)
if request.is_blocked:
log.downloads.debug("Skipping {}, host-blocked".format(url))
# We still need an empty file in the output, QWebView can be pretty
# picky about displaying a file correctly when not all assets are

View File

@ -38,6 +38,7 @@ if MYPY:
from qutebrowser.utils import (message, log, usertypes, utils, objreg,
urlutils, debug)
from qutebrowser.browser import shared
from qutebrowser.extensions import interceptors
from qutebrowser.browser.webkit import certificateerror
from qutebrowser.browser.webkit.network import (webkitqutescheme, networkreply,
filescheme)
@ -405,10 +406,10 @@ class NetworkManager(QNetworkAccessManager):
# the webpage shutdown here.
current_url = QUrl()
host_blocker = objreg.get('host-blocker')
if host_blocker.is_blocked(req.url(), current_url):
log.webview.info("Request to {} blocked by host blocker.".format(
req.url().host()))
request = interceptors.Request(first_party_url=current_url,
request_url=req.url())
interceptors.run(request)
if request.is_blocked:
return networkreply.ErrorNetworkReply(
req, HOSTBLOCK_ERROR_STRING, QNetworkReply.ContentAccessDenied,
self)

View File

@ -19,12 +19,19 @@
"""QtWebKit specific part of the web element API."""
import typing
from PyQt5.QtCore import QRect
from PyQt5.QtWebKit import QWebElement, QWebSettings
from PyQt5.QtWebKitWidgets import QWebFrame
from qutebrowser.config import config
from qutebrowser.utils import log, utils, javascript
from qutebrowser.utils import log, utils, javascript, usertypes
from qutebrowser.browser import webelem
MYPY = False
if MYPY:
# pylint: disable=unused-import,useless-suppression
from qutebrowser.browser.webkit import webkittab
class IsNullError(webelem.Error):
@ -36,7 +43,7 @@ class WebKitElement(webelem.AbstractWebElement):
"""A wrapper around a QWebElement."""
def __init__(self, elem, tab):
def __init__(self, elem: QWebElement, tab: 'webkittab.WebKitTab') -> None:
super().__init__(tab)
if isinstance(elem, self.__class__):
raise TypeError("Trying to wrap a wrapper!")
@ -44,90 +51,94 @@ class WebKitElement(webelem.AbstractWebElement):
raise IsNullError('{} is a null element!'.format(elem))
self._elem = elem
def __str__(self):
def __str__(self) -> str:
self._check_vanished()
return self._elem.toPlainText()
def __eq__(self, other):
def __eq__(self, other: object) -> bool:
if not isinstance(other, WebKitElement):
return NotImplemented
return self._elem == other._elem # pylint: disable=protected-access
def __getitem__(self, key):
def __getitem__(self, key: str) -> str:
self._check_vanished()
if key not in self:
raise KeyError(key)
return self._elem.attribute(key)
def __setitem__(self, key, val):
def __setitem__(self, key: str, val: str) -> None:
self._check_vanished()
self._elem.setAttribute(key, val)
def __delitem__(self, key):
def __delitem__(self, key: str) -> None:
self._check_vanished()
if key not in self:
raise KeyError(key)
self._elem.removeAttribute(key)
def __contains__(self, key):
def __contains__(self, key: object) -> bool:
assert isinstance(key, str)
self._check_vanished()
return self._elem.hasAttribute(key)
def __iter__(self):
def __iter__(self) -> typing.Iterator[str]:
self._check_vanished()
yield from self._elem.attributeNames()
def __len__(self):
def __len__(self) -> int:
self._check_vanished()
return len(self._elem.attributeNames())
def _check_vanished(self):
def _check_vanished(self) -> None:
"""Raise an exception if the element vanished (is null)."""
if self._elem.isNull():
raise IsNullError('Element {} vanished!'.format(self._elem))
def has_frame(self):
def has_frame(self) -> bool:
self._check_vanished()
return self._elem.webFrame() is not None
def geometry(self):
def geometry(self) -> QRect:
self._check_vanished()
return self._elem.geometry()
def classes(self):
def classes(self) -> typing.List[str]:
self._check_vanished()
return self._elem.classes()
def tag_name(self):
def tag_name(self) -> str:
"""Get the tag name for the current element."""
self._check_vanished()
return self._elem.tagName().lower()
def outer_xml(self):
def outer_xml(self) -> str:
"""Get the full HTML representation of this element."""
self._check_vanished()
return self._elem.toOuterXml()
def value(self):
def value(self) -> webelem.JsValueType:
self._check_vanished()
val = self._elem.evaluateJavaScript('this.value')
assert isinstance(val, (int, float, str, type(None))), val
return val
def set_value(self, value):
def set_value(self, value: webelem.JsValueType) -> None:
self._check_vanished()
if self._tab.is_deleted():
raise webelem.OrphanedError("Tab containing element vanished")
if self.is_content_editable():
log.webelem.debug("Filling {!r} via set_text.".format(self))
assert isinstance(value, str)
self._elem.setPlainText(value)
else:
log.webelem.debug("Filling {!r} via javascript.".format(self))
value = javascript.to_js(value)
self._elem.evaluateJavaScript("this.value={}".format(value))
def dispatch_event(self, event, bubbles=False,
cancelable=False, composed=False):
def dispatch_event(self, event: str,
bubbles: bool = False,
cancelable: bool = False,
composed: bool = False) -> None:
self._check_vanished()
log.webelem.debug("Firing event on {!r} via javascript.".format(self))
self._elem.evaluateJavaScript(
@ -138,7 +149,7 @@ class WebKitElement(webelem.AbstractWebElement):
javascript.to_js(cancelable),
javascript.to_js(composed)))
def caret_position(self):
def caret_position(self) -> int:
"""Get the text caret position for the current element."""
self._check_vanished()
pos = self._elem.evaluateJavaScript('this.selectionStart')
@ -146,7 +157,7 @@ class WebKitElement(webelem.AbstractWebElement):
return 0
return int(pos)
def insert_text(self, text):
def insert_text(self, text: str) -> None:
self._check_vanished()
if not self.is_editable(strict=True):
raise webelem.Error("Element is not editable!")
@ -158,7 +169,7 @@ class WebKitElement(webelem.AbstractWebElement):
this.dispatchEvent(event);
""".format(javascript.to_js(text)))
def _parent(self):
def _parent(self) -> typing.Optional['WebKitElement']:
"""Get the parent element of this element."""
self._check_vanished()
elem = self._elem.parent()
@ -166,7 +177,7 @@ class WebKitElement(webelem.AbstractWebElement):
return None
return WebKitElement(elem, tab=self._tab)
def _rect_on_view_js(self):
def _rect_on_view_js(self) -> typing.Optional[QRect]:
"""Javascript implementation for rect_on_view."""
# FIXME:qtwebengine maybe we can reuse this?
rects = self._elem.evaluateJavaScript("this.getClientRects()")
@ -178,8 +189,8 @@ class WebKitElement(webelem.AbstractWebElement):
return None
text = utils.compact_text(self._elem.toOuterXml(), 500)
log.webelem.vdebug("Client rectangles of element '{}': {}".format(
text, rects))
log.webelem.vdebug( # type: ignore
"Client rectangles of element '{}': {}".format(text, rects))
for i in range(int(rects.get("length", 0))):
rect = rects[str(i)]
@ -204,7 +215,8 @@ class WebKitElement(webelem.AbstractWebElement):
return None
def _rect_on_view_python(self, elem_geometry):
def _rect_on_view_python(self,
elem_geometry: typing.Optional[QRect]) -> QRect:
"""Python implementation for rect_on_view."""
if elem_geometry is None:
geometry = self._elem.geometry()
@ -218,7 +230,8 @@ class WebKitElement(webelem.AbstractWebElement):
frame = frame.parentFrame()
return rect
def rect_on_view(self, *, elem_geometry=None, no_js=False):
def rect_on_view(self, *, elem_geometry: QRect = None,
no_js: bool = False) -> QRect:
"""Get the geometry of the element relative to the webview.
Uses the getClientRects() JavaScript method to obtain the collection of
@ -248,7 +261,7 @@ class WebKitElement(webelem.AbstractWebElement):
# No suitable rects found via JS, try via the QWebElement API
return self._rect_on_view_python(elem_geometry)
def _is_visible(self, mainframe):
def _is_visible(self, mainframe: QWebFrame) -> bool:
"""Check if the given element is visible in the given frame.
This is not public API because it can't be implemented easily here with
@ -300,8 +313,8 @@ class WebKitElement(webelem.AbstractWebElement):
visible_in_frame = visible_on_screen
return all([visible_on_screen, visible_in_frame])
def remove_blank_target(self):
elem = self
def remove_blank_target(self) -> None:
elem = self # type: typing.Optional[WebKitElement]
for _ in range(5):
if elem is None:
break
@ -311,14 +324,14 @@ class WebKitElement(webelem.AbstractWebElement):
break
elem = elem._parent() # pylint: disable=protected-access
def _move_text_cursor(self):
def _move_text_cursor(self) -> None:
if self.is_text_input() and self.is_editable():
self._tab.caret.move_to_end_of_document()
def _requires_user_interaction(self):
def _requires_user_interaction(self) -> bool:
return False
def _click_editable(self, click_target):
def _click_editable(self, click_target: usertypes.ClickTarget) -> None:
ok = self._elem.evaluateJavaScript('this.focus(); true;')
if ok:
self._move_text_cursor()
@ -326,7 +339,7 @@ class WebKitElement(webelem.AbstractWebElement):
log.webelem.debug("Failed to focus via JS, falling back to event")
self._click_fake_event(click_target)
def _click_js(self, click_target):
def _click_js(self, click_target: usertypes.ClickTarget) -> None:
settings = QWebSettings.globalSettings()
attribute = QWebSettings.JavascriptCanOpenWindows
could_open_windows = settings.testAttribute(attribute)
@ -337,12 +350,12 @@ class WebKitElement(webelem.AbstractWebElement):
log.webelem.debug("Failed to click via JS, falling back to event")
self._click_fake_event(click_target)
def _click_fake_event(self, click_target):
def _click_fake_event(self, click_target: usertypes.ClickTarget) -> None:
self._tab.data.override_target = click_target
super()._click_fake_event(click_target)
def get_child_frames(startframe):
def get_child_frames(startframe: QWebFrame) -> typing.List[QWebFrame]:
"""Get all children recursively of a given QWebFrame.
Loosely based on http://blog.nextgenetics.net/?e=64
@ -356,7 +369,7 @@ def get_child_frames(startframe):
results = []
frames = [startframe]
while frames:
new_frames = []
new_frames = [] # type: typing.List[QWebFrame]
for frame in frames:
results.append(frame)
new_frames += frame.childFrames()

View File

@ -212,11 +212,11 @@ class CompletionItemDelegate(QStyledItemDelegate):
view = self.parent()
pattern = view.pattern
columns_to_filter = index.model().columns_to_filter(index)
self._doc.setPlainText(self._opt.text)
if index.column() in columns_to_filter and pattern:
pat = re.escape(pattern).replace(r'\ ', r'|')
_Highlighter(self._doc, pat,
config.val.colors.completion.match.fg)
self._doc.setPlainText(self._opt.text)
else:
self._doc.setHtml(
'<span style="font: {};">{}</span>'.format(

View File

@ -17,4 +17,4 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""qutebrowser "extensions" which only use the qutebrowser.API API."""
"""qutebrowser "extensions" which only use the qutebrowser.api API."""

View File

@ -24,19 +24,22 @@ import os.path
import functools
import posixpath
import zipfile
import logging
import typing
import pathlib
from qutebrowser.browser import downloads
from qutebrowser.config import config
from qutebrowser.utils import objreg, standarddir, log, message
from qutebrowser.api import cmdutils
from PyQt5.QtCore import QUrl
from qutebrowser.api import (cmdutils, hook, config, message, downloads,
interceptor, apitypes)
def _guess_zip_filename(zf):
"""Guess which file to use inside a zip file.
logger = logging.getLogger('misc')
_host_blocker = typing.cast('HostBlocker', None)
Args:
zf: A ZipFile instance.
"""
def _guess_zip_filename(zf: zipfile.ZipFile) -> str:
"""Guess which file to use inside a zip file."""
files = zf.namelist()
if len(files) == 1:
return files[0]
@ -47,7 +50,7 @@ def _guess_zip_filename(zf):
raise FileNotFoundError("No hosts file found in zip")
def get_fileobj(byte_io):
def get_fileobj(byte_io: typing.IO[bytes]) -> typing.IO[bytes]:
"""Get a usable file object to read the hosts file from."""
byte_io.seek(0) # rewind downloaded file
if zipfile.is_zipfile(byte_io):
@ -60,24 +63,20 @@ def get_fileobj(byte_io):
return byte_io
def _is_whitelisted_url(url):
"""Check if the given URL is on the adblock whitelist.
Args:
url: The URL to check as QUrl.
"""
def _is_whitelisted_url(url: QUrl) -> bool:
"""Check if the given URL is on the adblock whitelist."""
for pattern in config.val.content.host_blocking.whitelist:
if pattern.matches(url):
return True
return False
class _FakeDownload:
class _FakeDownload(downloads.TempDownload):
"""A download stub to use on_download_finished with local files."""
def __init__(self, fileobj):
self.basename = os.path.basename(fileobj.name)
def __init__(self, # pylint: disable=super-init-not-called
fileobj: typing.IO[bytes]) -> None:
self.fileobj = fileobj
self.successful = True
@ -93,37 +92,46 @@ class HostBlocker:
_done_count: How many files have been read successfully.
_local_hosts_file: The path to the blocked-hosts file.
_config_hosts_file: The path to a blocked-hosts in ~/.config
_has_basedir: Whether a custom --basedir is set.
"""
def __init__(self):
self._blocked_hosts = set()
self._config_blocked_hosts = set()
self._in_progress = []
def __init__(self, *, data_dir: pathlib.Path, config_dir: pathlib.Path,
has_basedir: bool = False) -> None:
self._has_basedir = has_basedir
self._blocked_hosts = set() # type: typing.Set[str]
self._config_blocked_hosts = set() # type: typing.Set[str]
self._in_progress = [] # type: typing.List[downloads.TempDownload]
self._done_count = 0
data_dir = standarddir.data()
self._local_hosts_file = os.path.join(data_dir, 'blocked-hosts')
self._update_files()
self._local_hosts_file = str(data_dir / 'blocked-hosts')
self.update_files()
config_dir = standarddir.config()
self._config_hosts_file = os.path.join(config_dir, 'blocked-hosts')
self._config_hosts_file = str(config_dir / 'blocked-hosts')
config.instance.changed.connect(self._update_files)
def is_blocked(self, url, first_party_url=None):
"""Check if the given URL (as QUrl) is blocked."""
def _is_blocked(self, request_url: QUrl,
first_party_url: QUrl = None) -> bool:
"""Check whether the given request is blocked."""
if first_party_url is not None and not first_party_url.isValid():
first_party_url = None
if not config.instance.get('content.host_blocking.enabled',
if not config.get('content.host_blocking.enabled',
url=first_party_url):
return False
host = url.host()
host = request_url.host()
return ((host in self._blocked_hosts or
host in self._config_blocked_hosts) and
not _is_whitelisted_url(url))
not _is_whitelisted_url(request_url))
def _read_hosts_file(self, filename, target):
def filter_request(self, info: interceptor.Request) -> None:
"""Block the given request if necessary."""
if self._is_blocked(request_url=info.request_url,
first_party_url=info.first_party_url):
logger.info("Request to {} blocked by host blocker."
.format(info.request_url.host()))
info.block()
def _read_hosts_file(self, filename: str, target: typing.Set[str]) -> bool:
"""Read hosts from the given filename.
Args:
@ -141,11 +149,11 @@ class HostBlocker:
for line in f:
target.add(line.strip())
except (OSError, UnicodeDecodeError):
log.misc.exception("Failed to read host blocklist!")
logger.exception("Failed to read host blocklist!")
return True
def read_hosts(self):
def read_hosts(self) -> None:
"""Read hosts from the existing blocked-hosts file."""
self._blocked_hosts = set()
@ -156,24 +164,17 @@ class HostBlocker:
self._blocked_hosts)
if not found:
args = objreg.get('args')
if (config.val.content.host_blocking.lists and
args.basedir is None and
not self._has_basedir and
config.val.content.host_blocking.enabled):
message.info("Run :adblock-update to get adblock lists.")
@cmdutils.register(instance='host-blocker')
def adblock_update(self):
"""Update the adblock block lists.
This updates `~/.local/share/qutebrowser/blocked-hosts` with downloaded
host lists and re-reads `~/.config/qutebrowser/blocked-hosts`.
"""
def adblock_update(self) -> None:
"""Update the adblock block lists."""
self._read_hosts_file(self._config_hosts_file,
self._config_blocked_hosts)
self._blocked_hosts = set()
self._done_count = 0
download_manager = objreg.get('qtnetwork-download-manager')
for url in config.val.content.host_blocking.lists:
if url.scheme() == 'file':
filename = url.toLocalFile()
@ -184,16 +185,12 @@ class HostBlocker:
else:
self._import_local(filename)
else:
fobj = io.BytesIO()
fobj.name = 'adblock: ' + url.host()
target = downloads.FileObjDownloadTarget(fobj)
download = download_manager.get(url, target=target,
auto_remove=True)
download = downloads.download_temp(url)
self._in_progress.append(download)
download.finished.connect(
functools.partial(self._on_download_finished, download))
def _import_local(self, filename):
def _import_local(self, filename: str) -> None:
"""Adds the contents of a file to the blocklist.
Args:
@ -209,24 +206,24 @@ class HostBlocker:
self._in_progress.append(download)
self._on_download_finished(download)
def _parse_line(self, line):
def _parse_line(self, raw_line: bytes) -> bool:
"""Parse a line from a host file.
Args:
line: The bytes object to parse.
raw_line: The bytes object to parse.
Returns:
True if parsing succeeded, False otherwise.
"""
if line.startswith(b'#'):
if raw_line.startswith(b'#'):
# Ignoring comments early so we don't have to care about
# encoding errors in them.
return True
try:
line = line.decode('utf-8')
line = raw_line.decode('utf-8')
except UnicodeDecodeError:
log.misc.error("Failed to decode: {!r}".format(line))
logger.error("Failed to decode: {!r}".format(raw_line))
return False
# Remove comments
@ -257,14 +254,11 @@ class HostBlocker:
return True
def _merge_file(self, byte_io):
def _merge_file(self, byte_io: io.BytesIO) -> None:
"""Read and merge host files.
Args:
byte_io: The BytesIO object of the completed download.
Return:
A set of the merged hosts.
"""
error_count = 0
line_count = 0
@ -282,12 +276,12 @@ class HostBlocker:
if not ok:
error_count += 1
log.misc.debug("{}: read {} lines".format(byte_io.name, line_count))
logger.debug("{}: read {} lines".format(byte_io.name, line_count))
if error_count > 0:
message.error("adblock: {} read errors for {}".format(
error_count, byte_io.name))
def _on_lists_downloaded(self):
def _on_lists_downloaded(self) -> None:
"""Install block lists after files have been downloaded."""
with open(self._local_hosts_file, 'w', encoding='utf-8') as f:
for host in sorted(self._blocked_hosts):
@ -295,8 +289,7 @@ class HostBlocker:
message.info("adblock: Read {} hosts from {} sources.".format(
len(self._blocked_hosts), self._done_count))
@config.change_filter('content.host_blocking.lists')
def _update_files(self):
def update_files(self) -> None:
"""Update files when the config changed."""
if not config.val.content.host_blocking.lists:
try:
@ -304,13 +297,13 @@ class HostBlocker:
except FileNotFoundError:
pass
except OSError as e:
log.misc.exception("Failed to delete hosts file: {}".format(e))
logger.exception("Failed to delete hosts file: {}".format(e))
def _on_download_finished(self, download):
def _on_download_finished(self, download: downloads.TempDownload) -> None:
"""Check if all downloads are finished and if so, trigger reading.
Arguments:
download: The finished DownloadItem.
download: The finished download.
"""
self._in_progress.remove(download)
if download.successful:
@ -323,4 +316,32 @@ class HostBlocker:
try:
self._on_lists_downloaded()
except OSError:
log.misc.exception("Failed to write host block list!")
logger.exception("Failed to write host block list!")
@cmdutils.register()
def adblock_update() -> None:
"""Update the adblock block lists.
This updates `~/.local/share/qutebrowser/blocked-hosts` with downloaded
host lists and re-reads `~/.config/qutebrowser/blocked-hosts`.
"""
# FIXME: As soon as we can register instances again, we should move this
# back to the class.
_host_blocker.adblock_update()
@hook.config_changed('content.host_blocking.lists')
def on_config_changed() -> None:
_host_blocker.update_files()
@hook.init()
def init(context: apitypes.InitContext) -> None:
"""Initialize the host blocker."""
global _host_blocker
_host_blocker = HostBlocker(data_dir=context.data_dir,
config_dir=context.config_dir,
has_basedir=context.args.basedir is not None)
_host_blocker.read_hosts()
interceptor.register(_host_blocker.filter_request)

View File

@ -118,7 +118,7 @@ def printpage(tab: apitypes.Tab,
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
def home(tab: apitypes.Tab) -> None:
"""Open main startpage in current tab."""
if tab.data.pinned:
if tab.navigation_blocked():
message.info("Tab is pinned!")
else:
tab.load_url(config.val.url.start_pages[0])
@ -238,7 +238,7 @@ def tab_mute(tab: apitypes.Tab) -> None:
if tab is None:
return
try:
tab.audio.set_muted(tab.audio.is_muted(), override=True)
tab.audio.set_muted(not tab.audio.is_muted(), override=True)
except apitypes.WebTabError as e:
raise cmdutils.CommandError(e)

View File

@ -86,7 +86,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name
not configdata.is_valid_prefix(self._option)):
raise configexc.NoOptionError(self._option)
def _check_match(self, option: typing.Optional[str]) -> bool:
def check_match(self, option: typing.Optional[str]) -> bool:
"""Check if the given option matches the filter."""
if option is None:
# Called directly, not from a config change event.
@ -119,7 +119,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name
@functools.wraps(func)
def func_wrapper(option: str = None) -> typing.Any:
"""Call the underlying function."""
if self._check_match(option):
if self.check_match(option):
return func()
return None
return func_wrapper
@ -128,7 +128,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name
def meth_wrapper(wrapper_self: typing.Any,
option: str = None) -> typing.Any:
"""Call the underlying function."""
if self._check_match(option):
if self.check_match(option):
return func(wrapper_self)
return None
return meth_wrapper

View File

@ -46,7 +46,9 @@ class ConfigCache:
self._cache[attr] = config.instance.get(attr)
def __getitem__(self, attr: str) -> typing.Any:
if attr not in self._cache:
try:
return self._cache[attr]
except KeyError:
assert not config.instance.get_opt(attr).supports_pattern
self._cache[attr] = config.instance.get(attr)
return self._cache[attr]

View File

@ -25,7 +25,7 @@ DATA: A dict of Option objects after init() has been called.
"""
import typing
from typing import Optional # pylint: disable=unused-import
from typing import Optional # pylint: disable=unused-import,useless-suppression
import functools
import attr

View File

@ -1768,6 +1768,11 @@ tabs.pinned.shrink:
type: Bool
desc: Shrink pinned tabs down to their contents.
tabs.pinned.frozen:
type: Bool
default: True
desc: Force pinned tabs to stay at fixed URL.
tabs.wrap:
default: true
type: Bool

View File

@ -308,7 +308,7 @@ class YamlConfig(QObject):
self._migrate_bool(settings, 'tabs.favicons.show', 'always', 'never')
self._migrate_bool(settings, 'scrolling.bar',
'when-searching', 'never')
'always', 'when-searching')
self._migrate_bool(settings, 'qt.force_software_rendering',
'software-opengl', 'none')

View File

View File

@ -0,0 +1,63 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2018 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 <http://www.gnu.org/licenses/>.
"""Infrastructure for intercepting requests."""
import typing
import attr
MYPY = False
if MYPY:
# pylint: disable=unused-import,useless-suppression
from PyQt5.QtCore import QUrl
@attr.s
class Request:
"""A request which can be intercepted/blocked."""
#: The URL of the page being shown.
first_party_url = attr.ib() # type: QUrl
#: The URL of the file being requested.
request_url = attr.ib() # type: QUrl
is_blocked = attr.ib(False) # type: bool
def block(self) -> None:
"""Block this request."""
self.is_blocked = True
#: Type annotation for an interceptor function.
InterceptorType = typing.Callable[[Request], None]
_interceptors = [] # type: typing.List[InterceptorType]
def register(interceptor: InterceptorType) -> None:
_interceptors.append(interceptor)
def run(info: Request) -> None:
for interceptor in _interceptors:
interceptor(info)

View File

@ -0,0 +1,187 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2018 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 <http://www.gnu.org/licenses/>.
"""Loader for qutebrowser extensions."""
import importlib.abc
import pkgutil
import types
import typing
import sys
import pathlib
import attr
from PyQt5.QtCore import pyqtSlot
from qutebrowser import components
from qutebrowser.config import config
from qutebrowser.utils import log, standarddir, objreg
MYPY = False
if MYPY:
# pylint: disable=unused-import,useless-suppression
import argparse
# ModuleInfo objects for all loaded plugins
_module_infos = []
@attr.s
class InitContext:
"""Context an extension gets in its init hook."""
data_dir = attr.ib() # type: pathlib.Path
config_dir = attr.ib() # type: pathlib.Path
args = attr.ib() # type: argparse.Namespace
@attr.s
class ModuleInfo:
"""Information attached to an extension module.
This gets used by qutebrowser.api.hook.
"""
_ConfigChangedHooksType = typing.List[typing.Tuple[typing.Optional[str],
typing.Callable]]
skip_hooks = attr.ib(False) # type: bool
init_hook = attr.ib(None) # type: typing.Optional[typing.Callable]
config_changed_hooks = attr.ib(
attr.Factory(list)) # type: _ConfigChangedHooksType
@attr.s
class ExtensionInfo:
"""Information about a qutebrowser extension."""
name = attr.ib() # type: str
def add_module_info(module: types.ModuleType) -> ModuleInfo:
"""Add ModuleInfo to a module (if not added yet)."""
# pylint: disable=protected-access
if not hasattr(module, '__qute_module_info'):
module.__qute_module_info = ModuleInfo() # type: ignore
return module.__qute_module_info # type: ignore
def load_components(*, skip_hooks: bool = False) -> None:
"""Load everything from qutebrowser.components."""
for info in walk_components():
_load_component(info, skip_hooks=skip_hooks)
def walk_components() -> typing.Iterator[ExtensionInfo]:
"""Yield ExtensionInfo objects for all modules."""
if hasattr(sys, 'frozen'):
yield from _walk_pyinstaller()
else:
yield from _walk_normal()
def _on_walk_error(name: str) -> None:
raise ImportError("Failed to import {}".format(name))
def _walk_normal() -> typing.Iterator[ExtensionInfo]:
"""Walk extensions when not using PyInstaller."""
for _finder, name, ispkg in pkgutil.walk_packages(
# Only packages have a __path__ attribute,
# but we're sure this is one.
path=components.__path__, # type: ignore
prefix=components.__name__ + '.',
onerror=_on_walk_error):
if ispkg:
continue
yield ExtensionInfo(name=name)
def _walk_pyinstaller() -> typing.Iterator[ExtensionInfo]:
"""Walk extensions when using PyInstaller.
See https://github.com/pyinstaller/pyinstaller/issues/1905
Inspired by:
https://github.com/webcomics/dosage/blob/master/dosagelib/loader.py
"""
toc = set() # type: typing.Set[str]
for importer in pkgutil.iter_importers('qutebrowser'):
if hasattr(importer, 'toc'):
toc |= importer.toc
for name in toc:
if name.startswith(components.__name__ + '.'):
yield ExtensionInfo(name=name)
def _get_init_context() -> InitContext:
"""Get an InitContext object."""
return InitContext(data_dir=pathlib.Path(standarddir.data()),
config_dir=pathlib.Path(standarddir.config()),
args=objreg.get('args'))
def _load_component(info: ExtensionInfo, *,
skip_hooks: bool = False) -> types.ModuleType:
"""Load the given extension and run its init hook (if any).
Args:
skip_hooks: Whether to skip all hooks for this module.
This is used to only run @cmdutils.register decorators.
"""
log.extensions.debug("Importing {}".format(info.name))
mod = importlib.import_module(info.name)
mod_info = add_module_info(mod)
if skip_hooks:
mod_info.skip_hooks = True
if mod_info.init_hook is not None and not skip_hooks:
log.extensions.debug("Running init hook {!r}"
.format(mod_info.init_hook.__name__))
mod_info.init_hook(_get_init_context())
_module_infos.append(mod_info)
return mod
@pyqtSlot(str)
def _on_config_changed(changed_name: str) -> None:
"""Call config_changed hooks if the config changed."""
for mod_info in _module_infos:
if mod_info.skip_hooks:
continue
for option, hook in mod_info.config_changed_hooks:
if option is None:
hook()
else:
cfilter = config.change_filter(option)
cfilter.validate()
if cfilter.check_match(changed_name):
hook()
def init() -> None:
config.instance.changed.connect(_on_config_changed)

View File

@ -1,5 +1,5 @@
/* eslint-disable max-len, max-statements, complexity,
default-case, valid-jsdoc */
default-case */
// Copyright 2014 The Chromium Authors. All rights reserved.
//

View File

@ -64,7 +64,7 @@ class NotInModeError(Exception):
def init(win_id, parent):
"""Initialize the mode manager and the keyparsers for the given win_id."""
KM = usertypes.KeyMode # noqa: N801,N806 pylint: disable=invalid-name
KM = usertypes.KeyMode # noqa: N806
modeman = ModeManager(win_id, parent)
objreg.register('mode-manager', modeman, scope='window', window=win_id)
keyparsers = {

View File

@ -32,7 +32,7 @@ from qutebrowser.commands import runners
from qutebrowser.api import cmdutils
from qutebrowser.config import config, configfiles
from qutebrowser.utils import (message, log, usertypes, qtutils, objreg, utils,
jinja)
jinja, debug)
from qutebrowser.mainwindow import messageview, prompt
from qutebrowser.completion import completionwidget, completer
from qutebrowser.keyinput import modeman
@ -137,6 +137,7 @@ class MainWindow(QWidget):
Attributes:
status: The StatusBar widget.
tabbed_browser: The TabbedBrowser widget.
state_before_fullscreen: window state before activation of fullscreen.
_downloadview: The DownloadView widget.
_vbox: The main QVBoxLayout.
_commandrunner: The main CommandRunner instance.
@ -238,6 +239,8 @@ class MainWindow(QWidget):
objreg.get("app").new_window.emit(self)
self._set_decoration(config.val.window.hide_decoration)
self.state_before_fullscreen = self.windowState()
def _init_geometry(self, geometry):
"""Initialize the window geometry or load it from disk."""
if geometry is not None:
@ -517,9 +520,13 @@ class MainWindow(QWidget):
def _on_fullscreen_requested(self, on):
if not config.val.content.windowed_fullscreen:
if on:
self.setWindowState(self.windowState() | Qt.WindowFullScreen)
self.state_before_fullscreen = self.windowState()
self.setWindowState(
Qt.WindowFullScreen | self.state_before_fullscreen)
elif self.isFullScreen():
self.setWindowState(self.windowState() & ~Qt.WindowFullScreen)
self.setWindowState(self.state_before_fullscreen)
log.misc.debug('on: {}, state before fullscreen: {}'.format(
on, debug.qflags_key(Qt, self.state_before_fullscreen)))
@cmdutils.register(instance='main-window', scope='window')
@pyqtSlot()

View File

@ -238,6 +238,9 @@ def _handle_wayland():
if has_qt511 and config.val.qt.force_software_rendering == 'chromium':
return
if qtutils.version_check('5.11.2', compiled=False):
return
buttons = []
text = "<p>You can work around this in one of the following ways:</p>"

View File

@ -137,6 +137,7 @@ prompt = logging.getLogger('prompt')
network = logging.getLogger('network')
sql = logging.getLogger('sql')
greasemonkey = logging.getLogger('greasemonkey')
extensions = logging.getLogger('extensions')
LOGGER_NAMES = [
'statusbar', 'completion', 'init', 'url',
@ -146,7 +147,7 @@ LOGGER_NAMES = [
'js', 'qt', 'rfc6266', 'ipc', 'shlexer',
'save', 'message', 'config', 'sessions',
'webelem', 'prompt', 'network', 'sql',
'greasemonkey'
'greasemonkey', 'extensions',
]

View File

@ -42,12 +42,12 @@ def _log_stack(typ: str, stack: str) -> None:
def error(message: str, *, stack: str = None, replace: bool = False) -> None:
"""Convenience function to display an error message in the statusbar.
"""Display an error message.
Args:
message: The message to show
stack: The stack trace to show.
replace: Replace existing messages with replace=True
message: The message to show.
stack: The stack trace to show (if any).
replace: Replace existing messages which are still being shown.
"""
if stack is None:
stack = ''.join(traceback.format_stack())
@ -60,11 +60,11 @@ def error(message: str, *, stack: str = None, replace: bool = False) -> None:
def warning(message: str, *, replace: bool = False) -> None:
"""Convenience function to display a warning message in the statusbar.
"""Display a warning message.
Args:
message: The message to show
replace: Replace existing messages with replace=True
message: The message to show.
replace: Replace existing messages which are still being shown.
"""
_log_stack('warning', ''.join(traceback.format_stack()))
log.message.warning(message)
@ -72,11 +72,11 @@ def warning(message: str, *, replace: bool = False) -> None:
def info(message: str, *, replace: bool = False) -> None:
"""Convenience function to display an info message in the statusbar.
"""Display an info message.
Args:
message: The message to show
replace: Replace existing messages with replace=True
message: The message to show.
replace: Replace existing messages which are still being shown.
"""
log.message.info(message)
global_bridge.show(usertypes.MessageLevel.info, message, replace)

View File

@ -210,15 +210,33 @@ PromptMode = enum.Enum('PromptMode', ['yesno', 'text', 'user_pwd', 'alert',
'download'])
# Where to open a clicked link.
ClickTarget = enum.Enum('ClickTarget', ['normal', 'tab', 'tab_bg', 'window',
'hover'])
class ClickTarget(enum.Enum):
"""How to open a clicked link."""
normal = 0 #: Open the link in the current tab
tab = 1 #: Open the link in a new foreground tab
tab_bg = 2 #: Open the link in a new background tab
window = 3 #: Open the link in a new window
hover = 4 #: Only hover over the link
# Key input modes
KeyMode = enum.Enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt',
'insert', 'passthrough', 'caret', 'set_mark',
'jump_mark', 'record_macro', 'run_macro'])
class KeyMode(enum.Enum):
"""Key input modes."""
normal = 1 #: Normal mode (no mode was entered)
hint = 2 #: Hint mode (showing labels for links)
command = 3 #: Command mode (after pressing the colon key)
yesno = 4 #: Yes/No prompts
prompt = 5 #: Text prompts
insert = 6 #: Insert mode (passing through most keys)
passthrough = 7 #: Passthrough mode (passing through all keys)
caret = 8 #: Caret mode (moving cursor with keys)
set_mark = 9
jump_mark = 10
record_macro = 11
run_macro = 12
class Exit(enum.IntEnum):
@ -241,8 +259,14 @@ LoadStatus = enum.Enum('LoadStatus', ['none', 'success', 'success_https',
Backend = enum.Enum('Backend', ['QtWebKit', 'QtWebEngine'])
# JS world for QtWebEngine
JsWorld = enum.Enum('JsWorld', ['main', 'application', 'user', 'jseval'])
class JsWorld(enum.Enum):
"""World/context to run JavaScript code in."""
main = 1 #: Same world as the web page's JavaScript.
application = 2 #: Application world, used by qutebrowser internally.
user = 3 #: User world, currently not used.
jseval = 4 #: World used for the jseval-command.
# Log level of a JS message. This needs to match up with the keys allowed for

View File

@ -45,7 +45,8 @@ try:
CSafeDumper as YamlDumper)
YAML_C_EXT = True
except ImportError: # pragma: no cover
from yaml import SafeLoader as YamlLoader, SafeDumper as YamlDumper
from yaml import (SafeLoader as YamlLoader, # type: ignore
SafeDumper as YamlDumper)
YAML_C_EXT = False
import qutebrowser

View File

@ -5,6 +5,6 @@ colorama==0.4.1
cssutils==1.0.2
Jinja2==2.10
MarkupSafe==1.1.0
Pygments==2.3.0
Pygments==2.3.1
pyPEG2==2.15.2
PyYAML==3.13

View File

@ -204,6 +204,7 @@ WHITELISTED_FILES = [
'browser/webkit/webkitinspector.py',
'keyinput/macros.py',
'browser/webkit/webkitelem.py',
'api/interceptor.py',
]

View File

@ -71,7 +71,7 @@ EOF
set -e
if [[ $DOCKER ]]; then
if [[ -n $DOCKER ]]; then
exit 0
elif [[ $TRAVIS_OS_NAME == osx ]]; then
# Disable App Nap

View File

@ -1,6 +1,6 @@
#!/bin/bash
if [[ $DOCKER ]]; then
if [[ -n $DOCKER ]]; then
docker run \
--privileged \
-v "$PWD:/outside" \

View File

@ -30,6 +30,7 @@ import argparse
import vulture
import qutebrowser.app # pylint: disable=unused-import
from qutebrowser.extensions import loader
from qutebrowser.misc import objects
from qutebrowser.utils import utils
from qutebrowser.browser.webkit import rfc6266
@ -43,6 +44,8 @@ from qutebrowser.config import configtypes
def whitelist_generator(): # noqa
"""Generator which yields lines to add to a vulture whitelist."""
loader.load_components(skip_hooks=True)
# qutebrowser commands
for cmd in objects.commands.values():
yield utils.qualname(cmd.handler)
@ -127,6 +130,9 @@ def whitelist_generator(): # noqa
yield 'scripts.get_coredumpctl_traces.Line.gid'
yield 'scripts.importer.import_moz_places.places.row_factory'
# component hooks
yield 'qutebrowser.components.adblock.on_config_changed'
def filter_func(item):
"""Check if a missing function should be filtered or not.

View File

@ -35,6 +35,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
# We import qutebrowser.app so all @cmdutils-register decorators are run.
import qutebrowser.app
from qutebrowser import qutebrowser, commands
from qutebrowser.extensions import loader
from qutebrowser.commands import argparser
from qutebrowser.config import configdata, configtypes
from qutebrowser.utils import docutils, usertypes
@ -549,6 +550,7 @@ def regenerate_cheatsheet():
def main():
"""Regenerate all documentation."""
utils.change_cwd()
loader.load_components(skip_hooks=True)
print("Generating manpage...")
regenerate_manpage('doc/qutebrowser.1.asciidoc')
print("Generating settings help...")

View File

@ -27,7 +27,7 @@ import os.path
import urllib.request
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from qutebrowser.browser import adblock
from qutebrowser.components import adblock
from qutebrowser.config import configdata

View File

@ -93,7 +93,7 @@ Feature: Downloading things from a website.
Then no crash should happen
# https://github.com/qutebrowser/qutebrowser/issues/4240
@qt!=5.11.2
@qt<5.11.2
Scenario: Downloading with SSL errors (issue 1413)
When SSL is supported
And I clear SSL errors

View File

@ -124,9 +124,9 @@ Feature: Javascript stuff
# https://github.com/qutebrowser/qutebrowser/issues/1190
# https://github.com/qutebrowser/qutebrowser/issues/2495
# Currently broken on Windows:
# Currently broken on Windows and on Qt 5.12
# https://github.com/qutebrowser/qutebrowser/issues/4230
@posix
@posix @qt<5.12
Scenario: Checking visible/invisible window size
When I run :tab-only
And I open data/javascript/windowsize.html in a new background tab
@ -134,7 +134,7 @@ Feature: Javascript stuff
And I run :tab-next
Then the window sizes should be the same
@flaky
@flaky @qt<5.12
Scenario: Checking visible/invisible window size with vertical tabbar
When I run :tab-only
And I set tabs.position to left

View File

@ -1289,6 +1289,14 @@ Feature: Tab management
And the following tabs should be open:
- data/numbers/1.txt (active) (pinned)
Scenario: :tab-pin open url with tabs.pinned.frozen = false
When I set tabs.pinned.frozen to false
And I open data/numbers/1.txt
And I run :tab-pin
And I open data/numbers/2.txt
Then the following tabs should be open:
- data/numbers/2.txt (active) (pinned)
Scenario: :home on a pinned tab
When I open data/numbers/1.txt
And I run :tab-pin
@ -1297,6 +1305,16 @@ Feature: Tab management
And the following tabs should be open:
- data/numbers/1.txt (active) (pinned)
Scenario: :home on a pinned tab with tabs.pinned.frozen = false
When I set url.start_pages to ["http://localhost:(port)/data/numbers/2.txt"]
And I set tabs.pinned.frozen to false
And I open data/numbers/1.txt
And I run :tab-pin
And I run :home
Then data/numbers/2.txt should be loaded
And the following tabs should be open:
- data/numbers/2.txt (active) (pinned)
Scenario: Cloning a pinned tab
When I open data/numbers/1.txt
And I run :tab-pin

View File

@ -356,7 +356,10 @@ class QuteProc(testprocess.Process):
self._focus_ready = True
else:
raise ValueError("Invalid value {!r} for 'what'.".format(what))
if self._load_ready and self._focus_ready:
is_qt_5_12 = qtutils.version_check('5.12', compiled=False)
if ((self._load_ready and self._focus_ready) or
(self._load_ready and is_qt_5_12)):
self._load_ready = False
self._focus_ready = False
self.ready.emit()

View File

@ -61,8 +61,8 @@ def normalize_line(line):
return line
def normalize_whole(s):
if qtutils.version_check('5.12', compiled=False):
def normalize_whole(s, webengine):
if qtutils.version_check('5.12', compiled=False) and webengine:
s = s.replace('\n\n-----=_qute-UUID', '\n-----=_qute-UUID')
return s
@ -71,8 +71,9 @@ class DownloadDir:
"""Abstraction over a download directory."""
def __init__(self, tmpdir):
def __init__(self, tmpdir, config):
self._tmpdir = tmpdir
self._config = config
self.location = str(tmpdir)
def read_file(self):
@ -92,14 +93,15 @@ class DownloadDir:
if normalize_line(line) is not None)
actual_data = '\n'.join(normalize_line(line)
for line in self.read_file())
actual_data = normalize_whole(actual_data)
actual_data = normalize_whole(actual_data,
webengine=self._config.webengine)
assert actual_data == expected_data
@pytest.fixture
def download_dir(tmpdir):
return DownloadDir(tmpdir)
def download_dir(tmpdir, pytestconfig):
return DownloadDir(tmpdir, pytestconfig)
def _test_mhtml_requests(test_dir, test_path, server):

View File

@ -44,6 +44,7 @@ from PyQt5.QtNetwork import QNetworkCookieJar
import helpers.stubs as stubsmod
from qutebrowser.config import (config, configdata, configtypes, configexc,
configfiles, configcache)
from qutebrowser.api import config as configapi
from qutebrowser.utils import objreg, standarddir, utils, usertypes
from qutebrowser.browser import greasemonkey, history, qutescheme
from qutebrowser.browser.webkit import cookies
@ -190,8 +191,8 @@ def testdata_scheme(qapp):
@pytest.fixture
def web_tab_setup(qtbot, tab_registry, session_manager_stub,
greasemonkey_manager, fake_args, host_blocker_stub,
config_stub, testdata_scheme):
greasemonkey_manager, fake_args, config_stub,
testdata_scheme):
"""Shared setup for webkit_tab/webengine_tab."""
# Make sure error logging via JS fails tests
config_stub.val.content.javascript.log = {
@ -306,6 +307,7 @@ def config_stub(stubs, monkeypatch, configdata_init, yaml_config_stub):
container = config.ConfigContainer(conf)
monkeypatch.setattr(config, 'val', container)
monkeypatch.setattr(configapi, 'val', container)
cache = configcache.ConfigCache()
monkeypatch.setattr(config, 'cache', cache)
@ -328,15 +330,6 @@ def key_config_stub(config_stub, monkeypatch):
return keyconf
@pytest.fixture
def host_blocker_stub(stubs):
"""Fixture which provides a fake host blocker object."""
stub = stubs.HostBlockerStub()
objreg.register('host-blocker', stub)
yield stub
objreg.delete('host-blocker')
@pytest.fixture
def quickmark_manager_stub(stubs):
"""Fixture which provides a fake quickmark manager object."""

View File

@ -459,17 +459,6 @@ class QuickmarkManagerStub(UrlMarkManagerStub):
self.delete(key)
class HostBlockerStub:
"""Stub for the host-blocker object."""
def __init__(self):
self.blocked_hosts = set()
def is_blocked(self, url, first_party_url=None):
return url in self.blocked_hosts
class SessionManagerStub:
"""Stub for the session-manager object."""

View File

@ -205,6 +205,20 @@ def test_secret_url(url, has_secret, from_file):
res.resolve(QNetworkProxyQuery(QUrl(url)), from_file=from_file)
def test_logging(qtlog):
"""Make sure console.log() works for PAC files."""
test_str = """
function FindProxyForURL(domain, host) {
console.log("logging test");
return "DIRECT";
}
"""
res = pac.PACResolver(test_str)
res.resolve(QNetworkProxyQuery(QUrl("https://example.com/test")))
assert len(qtlog.records) == 1
assert qtlog.records[0].message == 'logging test'
def fetcher_test(test_str):
class PACHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):

View File

@ -247,7 +247,7 @@ class TestWebKitElement:
pytest.param(lambda e: e[None], id='getitem'),
pytest.param(lambda e: operator.setitem(e, None, None), id='setitem'),
pytest.param(lambda e: operator.delitem(e, None), id='delitem'),
pytest.param(lambda e: None in e, id='contains'),
pytest.param(lambda e: '' in e, id='contains'),
pytest.param(list, id='iter'),
pytest.param(len, id='len'),
pytest.param(lambda e: e.has_frame(), id='has_frame'),

View File

@ -20,7 +20,8 @@ from unittest import mock
import pytest
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QTextDocument
from PyQt5.QtGui import QTextDocument, QColor
from PyQt5.QtWidgets import QTextEdit
from qutebrowser.completion import completiondelegate
@ -50,3 +51,24 @@ def test_highlight(pat, txt, segments):
highlighter.setFormat.assert_has_calls([
mock.call(s[0], s[1], mock.ANY) for s in segments
])
def test_highlighted(qtbot):
"""Make sure highlighting works.
Note that with Qt 5.11.3 and > 5.12.1 we need to call setPlainText *after*
creating the highlighter for highlighting to work. Ideally, we'd test
whether CompletionItemDelegate._get_textdoc() works properly, but testing
that is kind of hard, so we just test it in isolation here.
"""
doc = QTextDocument()
completiondelegate._Highlighter(doc, 'Hello', Qt.red)
doc.setPlainText('Hello World')
# Needed so the highlighting actually works.
edit = QTextEdit()
qtbot.addWidget(edit)
edit.setDocument(doc)
colors = [f.foreground().color() for f in doc.allFormats()]
assert QColor('red') in colors

View File

@ -28,12 +28,12 @@ import pytest
from PyQt5.QtCore import QUrl
from qutebrowser.browser import adblock
from qutebrowser.components import adblock
from qutebrowser.utils import urlmatch
from tests.helpers import utils
pytestmark = pytest.mark.usefixtures('qapp', 'config_tmpdir')
pytestmark = pytest.mark.usefixtures('qapp')
# TODO See ../utils/test_standarddirutils for OSError and caplog assertion
@ -58,18 +58,13 @@ URLS_TO_CHECK = ('http://localhost',
'http://veryverygoodhost.edu')
class BaseDirStub:
"""Mock for objreg.get('args') called in adblock.HostBlocker.read_hosts."""
def __init__(self):
self.basedir = None
@pytest.fixture
def basedir(fake_args):
"""Register a Fake basedir."""
fake_args.basedir = None
def host_blocker_factory(config_tmpdir, data_tmpdir, download_stub,
config_stub):
def factory():
return adblock.HostBlocker(config_dir=config_tmpdir,
data_dir=data_tmpdir)
return factory
def create_zipfile(directory, files, zipname='test'):
@ -133,9 +128,9 @@ def assert_urls(host_blocker, blocked=BLOCKLIST_HOSTS,
url = QUrl(str_url)
host = url.host()
if host in blocked and host not in whitelisted:
assert host_blocker.is_blocked(url)
assert host_blocker._is_blocked(url)
else:
assert not host_blocker.is_blocked(url)
assert not host_blocker._is_blocked(url)
def blocklist_to_url(filename):
@ -202,13 +197,13 @@ def generic_blocklists(directory):
blocklist5.toString()]
def test_disabled_blocking_update(basedir, config_stub, download_stub,
data_tmpdir, tmpdir, win_registry, caplog):
def test_disabled_blocking_update(config_stub, tmpdir, caplog,
host_blocker_factory):
"""Ensure no URL is blocked when host blocking is disabled."""
config_stub.val.content.host_blocking.lists = generic_blocklists(tmpdir)
config_stub.val.content.host_blocking.enabled = False
host_blocker = adblock.HostBlocker()
host_blocker = host_blocker_factory()
host_blocker.adblock_update()
while host_blocker._in_progress:
current_download = host_blocker._in_progress[0]
@ -217,10 +212,10 @@ def test_disabled_blocking_update(basedir, config_stub, download_stub,
current_download.finished.emit()
host_blocker.read_hosts()
for str_url in URLS_TO_CHECK:
assert not host_blocker.is_blocked(QUrl(str_url))
assert not host_blocker._is_blocked(QUrl(str_url))
def test_disabled_blocking_per_url(config_stub, data_tmpdir):
def test_disabled_blocking_per_url(config_stub, host_blocker_factory):
example_com = 'https://www.example.com/'
config_stub.val.content.host_blocking.lists = []
@ -230,36 +225,34 @@ def test_disabled_blocking_per_url(config_stub, data_tmpdir):
url = QUrl('blocked.example.com')
host_blocker = adblock.HostBlocker()
host_blocker = host_blocker_factory()
host_blocker._blocked_hosts.add(url.host())
assert host_blocker.is_blocked(url)
assert not host_blocker.is_blocked(url, first_party_url=QUrl(example_com))
assert host_blocker._is_blocked(url)
assert not host_blocker._is_blocked(url, first_party_url=QUrl(example_com))
def test_no_blocklist_update(config_stub, download_stub,
data_tmpdir, basedir, tmpdir, win_registry):
def test_no_blocklist_update(config_stub, download_stub, host_blocker_factory):
"""Ensure no URL is blocked when no block list exists."""
config_stub.val.content.host_blocking.lists = None
config_stub.val.content.host_blocking.enabled = True
host_blocker = adblock.HostBlocker()
host_blocker = host_blocker_factory()
host_blocker.adblock_update()
host_blocker.read_hosts()
for dl in download_stub.downloads:
dl.successful = True
for str_url in URLS_TO_CHECK:
assert not host_blocker.is_blocked(QUrl(str_url))
assert not host_blocker._is_blocked(QUrl(str_url))
def test_successful_update(config_stub, basedir, download_stub,
data_tmpdir, tmpdir, win_registry, caplog):
def test_successful_update(config_stub, tmpdir, caplog, host_blocker_factory):
"""Ensure hosts from host_blocking.lists are blocked after an update."""
config_stub.val.content.host_blocking.lists = generic_blocklists(tmpdir)
config_stub.val.content.host_blocking.enabled = True
config_stub.val.content.host_blocking.whitelist = None
host_blocker = adblock.HostBlocker()
host_blocker = host_blocker_factory()
host_blocker.adblock_update()
# Simulate download is finished
while host_blocker._in_progress:
@ -271,11 +264,9 @@ def test_successful_update(config_stub, basedir, download_stub,
assert_urls(host_blocker, whitelisted=[])
def test_parsing_multiple_hosts_on_line(config_stub, basedir, download_stub,
data_tmpdir, tmpdir, win_registry,
caplog):
def test_parsing_multiple_hosts_on_line(host_blocker_factory):
"""Ensure multiple hosts on a line get parsed correctly."""
host_blocker = adblock.HostBlocker()
host_blocker = host_blocker_factory()
bytes_host_line = ' '.join(BLOCKLIST_HOSTS).encode('utf-8')
host_blocker._parse_line(bytes_host_line)
assert_urls(host_blocker, whitelisted=[])
@ -299,17 +290,15 @@ def test_parsing_multiple_hosts_on_line(config_stub, basedir, download_stub,
('127.0.1.1', 'myhostname'),
('127.0.0.53', 'myhostname'),
])
def test_whitelisted_lines(config_stub, basedir, download_stub, data_tmpdir,
tmpdir, win_registry, caplog, ip, host):
def test_whitelisted_lines(host_blocker_factory, ip, host):
"""Make sure we don't block hosts we don't want to."""
host_blocker = adblock.HostBlocker()
host_blocker = host_blocker_factory()
line = ('{} {}'.format(ip, host)).encode('ascii')
host_blocker._parse_line(line)
assert host not in host_blocker._blocked_hosts
def test_failed_dl_update(config_stub, basedir, download_stub,
data_tmpdir, tmpdir, win_registry, caplog):
def test_failed_dl_update(config_stub, tmpdir, caplog, host_blocker_factory):
"""One blocklist fails to download.
Ensure hosts from this list are not blocked.
@ -323,7 +312,7 @@ def test_failed_dl_update(config_stub, basedir, download_stub,
config_stub.val.content.host_blocking.enabled = True
config_stub.val.content.host_blocking.whitelist = None
host_blocker = adblock.HostBlocker()
host_blocker = host_blocker_factory()
host_blocker.adblock_update()
while host_blocker._in_progress:
current_download = host_blocker._in_progress[0]
@ -339,8 +328,8 @@ def test_failed_dl_update(config_stub, basedir, download_stub,
@pytest.mark.parametrize('location', ['content', 'comment'])
def test_invalid_utf8(config_stub, download_stub, tmpdir, data_tmpdir,
caplog, location):
def test_invalid_utf8(config_stub, tmpdir, caplog, host_blocker_factory,
location):
"""Make sure invalid UTF-8 is handled correctly.
See https://github.com/qutebrowser/qutebrowser/issues/2301
@ -359,7 +348,7 @@ def test_invalid_utf8(config_stub, download_stub, tmpdir, data_tmpdir,
config_stub.val.content.host_blocking.enabled = True
config_stub.val.content.host_blocking.whitelist = None
host_blocker = adblock.HostBlocker()
host_blocker = host_blocker_factory()
host_blocker.adblock_update()
current_download = host_blocker._in_progress[0]
@ -379,26 +368,25 @@ def test_invalid_utf8(config_stub, download_stub, tmpdir, data_tmpdir,
def test_invalid_utf8_compiled(config_stub, config_tmpdir, data_tmpdir,
monkeypatch, caplog):
monkeypatch, caplog, host_blocker_factory):
"""Make sure invalid UTF-8 in the compiled file is handled."""
config_stub.val.content.host_blocking.lists = []
# Make sure the HostBlocker doesn't delete blocked-hosts in __init__
monkeypatch.setattr(adblock.HostBlocker, '_update_files',
monkeypatch.setattr(adblock.HostBlocker, 'update_files',
lambda _self: None)
(config_tmpdir / 'blocked-hosts').write_binary(
b'https://www.example.org/\xa0')
(data_tmpdir / 'blocked-hosts').ensure()
host_blocker = adblock.HostBlocker()
host_blocker = host_blocker_factory()
with caplog.at_level(logging.ERROR):
host_blocker.read_hosts()
assert caplog.messages[-1] == "Failed to read host blocklist!"
def test_blocking_with_whitelist(config_stub, basedir, download_stub,
data_tmpdir, tmpdir):
def test_blocking_with_whitelist(config_stub, data_tmpdir, host_blocker_factory):
"""Ensure hosts in content.host_blocking.whitelist are never blocked."""
# Simulate adblock_update has already been run
# by creating a file named blocked-hosts,
@ -412,13 +400,12 @@ def test_blocking_with_whitelist(config_stub, basedir, download_stub,
config_stub.val.content.host_blocking.enabled = True
config_stub.val.content.host_blocking.whitelist = list(WHITELISTED_HOSTS)
host_blocker = adblock.HostBlocker()
host_blocker = host_blocker_factory()
host_blocker.read_hosts()
assert_urls(host_blocker)
def test_config_change_initial(config_stub, basedir, download_stub,
data_tmpdir, tmpdir):
def test_config_change_initial(config_stub, tmpdir, host_blocker_factory):
"""Test emptying host_blocking.lists with existing blocked_hosts.
- A blocklist is present in host_blocking.lists and blocked_hosts is
@ -432,14 +419,13 @@ def test_config_change_initial(config_stub, basedir, download_stub,
config_stub.val.content.host_blocking.enabled = True
config_stub.val.content.host_blocking.whitelist = None
host_blocker = adblock.HostBlocker()
host_blocker = host_blocker_factory()
host_blocker.read_hosts()
for str_url in URLS_TO_CHECK:
assert not host_blocker.is_blocked(QUrl(str_url))
assert not host_blocker._is_blocked(QUrl(str_url))
def test_config_change(config_stub, basedir, download_stub,
data_tmpdir, tmpdir):
def test_config_change(config_stub, tmpdir, host_blocker_factory):
"""Ensure blocked-hosts resets if host-block-list is changed to None."""
filtered_blocked_hosts = BLOCKLIST_HOSTS[1:] # Exclude localhost
blocklist = blocklist_to_url(create_blocklist(
@ -449,16 +435,15 @@ def test_config_change(config_stub, basedir, download_stub,
config_stub.val.content.host_blocking.enabled = True
config_stub.val.content.host_blocking.whitelist = None
host_blocker = adblock.HostBlocker()
host_blocker = host_blocker_factory()
host_blocker.read_hosts()
config_stub.val.content.host_blocking.lists = None
host_blocker.read_hosts()
for str_url in URLS_TO_CHECK:
assert not host_blocker.is_blocked(QUrl(str_url))
assert not host_blocker._is_blocked(QUrl(str_url))
def test_add_directory(config_stub, basedir, download_stub,
data_tmpdir, tmpdir):
def test_add_directory(config_stub, tmpdir, host_blocker_factory):
"""Ensure adblocker can import all files in a directory."""
blocklist_hosts2 = []
for i in BLOCKLIST_HOSTS[1:]:
@ -471,18 +456,18 @@ def test_add_directory(config_stub, basedir, download_stub,
config_stub.val.content.host_blocking.lists = [tmpdir.strpath]
config_stub.val.content.host_blocking.enabled = True
host_blocker = adblock.HostBlocker()
host_blocker = host_blocker_factory()
host_blocker.adblock_update()
assert len(host_blocker._blocked_hosts) == len(blocklist_hosts2) * 2
def test_adblock_benchmark(config_stub, data_tmpdir, basedir, benchmark):
def test_adblock_benchmark(data_tmpdir, benchmark, host_blocker_factory):
blocked_hosts = os.path.join(utils.abs_datapath(), 'blocked-hosts')
shutil.copy(blocked_hosts, str(data_tmpdir))
url = QUrl('https://www.example.org/')
blocker = adblock.HostBlocker()
blocker = host_blocker_factory()
blocker.read_hosts()
assert blocker._blocked_hosts
benchmark(lambda: blocker.is_blocked(url))
benchmark(lambda: blocker._is_blocked(url))

View File

@ -50,3 +50,17 @@ def test_configcache_get_after_set(config_stub):
assert not config.cache['auto_save.session']
config_stub.val.auto_save.session = True
assert config.cache['auto_save.session']
def test_configcache_naive_benchmark(config_stub, benchmark):
def _run_bench():
for _i in range(10000):
# pylint: disable=pointless-statement
config.cache['tabs.padding']
config.cache['tabs.indicator.width']
config.cache['tabs.indicator.padding']
config.cache['tabs.min_width']
config.cache['tabs.max_width']
config.cache['tabs.pinned.shrink']
# pylint: enable=pointless-statement
benchmark(_run_bench)

View File

@ -250,36 +250,28 @@ class TestYaml:
data = autoconfig.read()
assert data['content.webrtc_ip_handling_policy']['global'] == expected
@pytest.mark.parametrize('show, expected', [
(True, 'always'),
(False, 'never'),
('always', 'always'),
('never', 'never'),
('pinned', 'pinned'),
@pytest.mark.parametrize('setting, old, new', [
('tabs.favicons.show', True, 'always'),
('tabs.favicons.show', False, 'never'),
('tabs.favicons.show', 'always', 'always'),
('scrolling.bar', True, 'always'),
('scrolling.bar', False, 'when-searching'),
('scrolling.bar', 'always', 'always'),
('qt.force_software_rendering', True, 'software-opengl'),
('qt.force_software_rendering', False, 'none'),
('qt.force_software_rendering', 'chromium', 'chromium'),
])
def test_tabs_favicons_show(self, yaml, autoconfig, show, expected):
"""Tests for migration of tabs.favicons.show."""
autoconfig.write({'tabs.favicons.show': {'global': show}})
def test_bool_migrations(self, yaml, autoconfig, setting, old, new):
"""Tests for migration of former boolean settings."""
autoconfig.write({setting: {'global': old}})
yaml.load()
yaml._save()
data = autoconfig.read()
assert data['tabs.favicons.show']['global'] == expected
@pytest.mark.parametrize('force, expected', [
(True, 'software-opengl'),
(False, 'none'),
('chromium', 'chromium'),
])
def test_force_software_rendering(self, yaml, autoconfig, force, expected):
autoconfig.write({'qt.force_software_rendering': {'global': force}})
yaml.load()
yaml._save()
data = autoconfig.read()
assert data['qt.force_software_rendering']['global'] == expected
assert data[setting]['global'] == new
def test_renamed_key_unknown_target(self, monkeypatch, yaml,
autoconfig):

View File

@ -0,0 +1,143 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2018 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 <http://www.gnu.org/licenses/>.
import types
import pytest
from qutebrowser.extensions import loader
from qutebrowser.misc import objects
pytestmark = pytest.mark.usefixtures('data_tmpdir', 'config_tmpdir',
'fake_args')
def test_on_walk_error():
with pytest.raises(ImportError, match='Failed to import foo'):
loader._on_walk_error('foo')
def test_walk_normal():
names = [info.name for info in loader._walk_normal()]
assert 'qutebrowser.components.scrollcommands' in names
def test_walk_pyinstaller():
# We can't test whether we get something back without being frozen by
# PyInstaller, but at least we can test that we don't crash.
list(loader._walk_pyinstaller())
def test_load_component(monkeypatch):
monkeypatch.setattr(objects, 'commands', {})
info = loader.ExtensionInfo(name='qutebrowser.components.scrollcommands')
mod = loader._load_component(info, skip_hooks=True)
assert hasattr(mod, 'scroll_to_perc')
assert 'scroll-to-perc' in objects.commands
@pytest.fixture
def module(monkeypatch, request):
mod = types.ModuleType('testmodule')
monkeypatch.setattr(loader, '_module_infos', [])
monkeypatch.setattr(loader.importlib, 'import_module',
lambda _name: mod)
mod.info = loader.add_module_info(mod)
return mod
def test_get_init_context(data_tmpdir, config_tmpdir, fake_args):
ctx = loader._get_init_context()
assert str(ctx.data_dir) == data_tmpdir
assert str(ctx.config_dir) == config_tmpdir
assert ctx.args == fake_args
def test_add_module_info():
# pylint: disable=no-member
mod = types.ModuleType('testmodule')
info1 = loader.add_module_info(mod)
assert mod.__qute_module_info is info1
info2 = loader.add_module_info(mod)
assert mod.__qute_module_info is info1
assert info2 is info1
class _Hook:
"""Hook to use in tests."""
__name__ = '_Hook'
def __init__(self):
self.called = False
self.raising = False
def __call__(self, *args):
if self.raising:
raise Exception("Should not be called!")
self.called = True
@pytest.fixture
def hook():
return _Hook()
def test_skip_hooks(hook, module):
hook.raising = True
module.info.init_hook = hook
module.info.config_changed_hooks = [(None, hook)]
info = loader.ExtensionInfo(name='testmodule')
loader._load_component(info, skip_hooks=True)
loader._on_config_changed('test')
assert not hook.called
@pytest.mark.parametrize('option_filter, option, called', [
(None, 'content.javascript.enabled', True),
('content.javascript', 'content.javascript.enabled', True),
('content.javascript.enabled', 'content.javascript.enabled', True),
('content.javascript.log', 'content.javascript.enabled', False),
])
def test_on_config_changed(configdata_init, hook, module,
option_filter, option, called):
module.info.config_changed_hooks = [(option_filter, hook)]
info = loader.ExtensionInfo(name='testmodule')
loader._load_component(info)
loader._on_config_changed(option)
assert hook.called == called
def test_init_hook(hook, module):
module.info.init_hook = hook
info = loader.ExtensionInfo(name='testmodule')
loader._load_component(info)
assert hook.called

View File

@ -25,7 +25,7 @@ import pytest
QtWebEngineWidgets = pytest.importorskip("PyQt5.QtWebEngineWidgets")
QWebEngineProfile = QtWebEngineWidgets.QWebEngineProfile
from qutebrowser.utils import javascript
from qutebrowser.utils import javascript, qtutils
DEFAULT_BODY_BG = "rgba(0, 0, 0, 0)"
@ -128,6 +128,8 @@ def test_set_error(stylesheet_tester, config_stub):
stylesheet_tester.check_set(GREEN_BODY_BG)
@pytest.mark.skip(qtutils.version_check('5.12', compiled=False),
reason='Broken with Qt 5.12')
def test_appendchild(stylesheet_tester):
stylesheet_tester.js.load('stylesheet/simple.html')
stylesheet_tester.init_stylesheet()

View File

@ -34,7 +34,7 @@ from PyQt5.QtTest import QSignalSpy
import qutebrowser
from qutebrowser.misc import ipc
from qutebrowser.utils import standarddir, utils
from qutebrowser.utils import standarddir, utils, qtutils
from helpers import stubs
@ -630,6 +630,8 @@ class TestSendOrListen:
assert ret_client is None
@pytest.mark.posix(reason="Unneeded on Windows")
@pytest.mark.xfail(qtutils.version_check('5.12', compiled=False) and
utils.is_mac, reason="Broken, see #4471")
def test_correct_socket_name(self, args):
server = ipc.send_or_listen(args)
expected_dir = ipc._get_socketname(args.basedir)

11
tox.ini
View File

@ -199,3 +199,14 @@ deps =
-r{toxinidir}/misc/requirements/requirements-mypy.txt
commands =
{envpython} -m mypy qutebrowser {posargs}
[testenv:sphinx]
basepython = {env:PYTHON:python3}
passenv =
usedevelop = true
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/misc/requirements/requirements-pyqt.txt
-r{toxinidir}/misc/requirements/requirements-sphinx.txt
commands =
{envpython} -m sphinx -jauto -W --color {posargs} {toxinidir}/doc/extapi/ {toxinidir}/doc/extapi/_build/