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 min-version = 3.4.0
max-complexity = 12 max-complexity = 12
per-file-ignores = per-file-ignores =
/qutebrowser/api/hook.py : N801
/tests/**/*.py : D100,D101,D401 /tests/**/*.py : D100,D101,D401
/tests/unit/browser/test_history.py : N806 /tests/unit/browser/test_history.py : N806
/tests/helpers/fixtures.py : N806 /tests/helpers/fixtures.py : N806

1
.gitignore vendored
View File

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

View File

@ -68,16 +68,3 @@ after_success:
after_failure: after_failure:
- bash scripts/dev/ci/travis_backtrace.sh - 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 qutebrowser/git-commit-id
include LICENSE doc/* README.asciidoc include LICENSE doc/* README.asciidoc
include misc/qutebrowser.desktop include misc/qutebrowser.desktop
include misc/qutebrowser.appdata.xml include misc/org.qutebrowser.qutebrowser.appdata.xml
include misc/Makefile include misc/Makefile
include requirements.txt include requirements.txt
include tox.ini include tox.ini
@ -40,5 +40,6 @@ exclude .*
exclude misc/qutebrowser.spec exclude misc/qutebrowser.spec
exclude misc/qutebrowser.nsi exclude misc/qutebrowser.nsi
exclude misc/qutebrowser.rcc exclude misc/qutebrowser.rcc
prune doc/extapi
global-exclude __pycache__ *.pyc *.pyo global-exclude __pycache__ *.pyc *.pyo

View File

@ -25,6 +25,7 @@ Added
opened from a page should stack on each other or not. opened from a page should stack on each other or not.
- New `completion.open_categories` setting which allows to configure which - New `completion.open_categories` setting which allows to configure which
categories are shown in the `:open` completion, and how they are ordered. 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: - New config manipulation commands:
* `:config-dict-add` and `:config-list-add` to a new element to a dict/list * `:config-dict-add` and `:config-list-add` to a new element to a dict/list
setting. setting.
@ -51,6 +52,13 @@ Changed
adblocker can be disabled on a given page. adblocker can be disabled on a given page.
- Elements with a `tabindex` attribute now also get hints by default. - Elements with a `tabindex` attribute now also get hints by default.
- Various small performance improvements for hints and the completion. - 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 Fixed
~~~~~ ~~~~~
@ -65,9 +73,12 @@ Fixed
`content.cookies.accept = no-3rdparty` from working properly on some pages `content.cookies.accept = no-3rdparty` from working properly on some pages
like GMail. However, the default for `content.cookies.accept` is still `all` like GMail. However, the default for `content.cookies.accept` is still `all`
to be in line with what other browsers do. 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. - Crash when trying to use a proxy requiring authentication with QtWebKit.
- Slashes in search terms are now percent-escaped. - 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 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?:: What's the difference between insert and passthrough mode?::
They are quite similar, but insert mode has some bindings (like `Ctrl-e` to 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 open an editor) while passthrough mode only has shift+escape bound. This is
be useful to rebind escape to something else in passthrough mode only, to be because shift+escape is unlikely to be a useful binding to be passed to a
able to send an escape keypress to the website. 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?:: 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 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]. - 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] - 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/evannagle/qutebrowser-dracula-theme[Dracula]
- https://github.com/jjzmajic/qutewal[Pywal theme]
Avoiding flake8 errors 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.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.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.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.pinned.shrink,tabs.pinned.shrink>>|Shrink pinned tabs down to their contents.
|<<tabs.position,tabs.position>>|Position of the tab bar. |<<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. |<<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:[right]+: +pass:[5]+
- +pass:[top]+: +pass:[0]+ - +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]]
=== tabs.pinned.shrink === tabs.pinned.shrink
Shrink pinned tabs down to their contents. Shrink pinned tabs down to their contents.

View File

@ -102,18 +102,12 @@ $ python3 scripts/asciidoc2html.py
On Fedora 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: qutebrowser is available in the official repositories:
----- -----
# dnf install qutebrowser # 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 Additional hints
~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
mypy==0.641 mypy==0.650
mypy-extensions==0.4.1 mypy-extensions==0.4.1
PyQt5==5.11.3 PyQt5==5.11.3
PyQt5-sip==4.19.13 PyQt5-sip==4.19.13
-e git+https://github.com/qutebrowser/PyQt5-stubs.git@wip#egg=PyQt5_stubs -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 cssutils==1.0.2
hunter==2.1.0 hunter==2.1.0
Pympler==0.6 Pympler==0.6
six==1.11.0 six==1.12.0

View File

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

View File

@ -7,7 +7,7 @@ cffi==1.11.5
chardet==3.0.4 chardet==3.0.4
cryptography==2.4.2 cryptography==2.4.2
github3.py==1.2.0 github3.py==1.2.0
idna==2.7 idna==2.8
isort==4.3.4 isort==4.3.4
jwcrypto==0.6.0 jwcrypto==0.6.0
lazy-object-proxy==1.3.1 lazy-object-proxy==1.3.1
@ -16,8 +16,8 @@ pycparser==2.19
pylint==2.2.2 pylint==2.2.2
python-dateutil==2.7.5 python-dateutil==2.7.5
./scripts/dev/pylint_checkers ./scripts/dev/pylint_checkers
requests==2.20.1 requests==2.21.0
six==1.11.0 six==1.12.0
uritemplate==3.0.0 uritemplate==3.0.0
urllib3==1.24.1 urllib3==1.24.1
wrapt==1.10.11 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 atomicwrites==1.2.1
attrs==18.2.0 attrs==18.2.0
backports.functools-lru-cache==1.5 backports.functools-lru-cache==1.5
beautifulsoup4==4.6.3 beautifulsoup4==4.7.0
cheroot==6.5.2 cheroot==6.5.3
Click==7.0 Click==7.0
# colorama==0.4.1 # colorama==0.4.1
coverage==4.5.2 coverage==4.5.2
EasyProcess==0.2.3 EasyProcess==0.2.5
Flask==1.0.2 Flask==1.0.2
glob2==0.6 glob2==0.6
hunter==2.1.0 hunter==2.1.0
hypothesis==3.82.1 hypothesis==3.85.2
itsdangerous==1.1.0 itsdangerous==1.1.0
# Jinja2==2.10 # Jinja2==2.10
Mako==1.0.7 Mako==1.0.7
# MarkupSafe==1.1.0 # MarkupSafe==1.1.0
more-itertools==4.3.0 more-itertools==5.0.0
parse==1.9.0 parse==1.9.0
parse-type==0.4.2 parse-type==0.4.2
pluggy==0.8.0 pluggy==0.8.0
py==1.7.0 py==1.7.0
py-cpuinfo==4.0.0 py-cpuinfo==4.0.0
pytest==4.0.1 pytest==4.0.2
pytest-bdd==3.0.0 pytest-bdd==3.0.1
pytest-benchmark==3.1.1 pytest-benchmark==3.1.1
pytest-cov==2.6.0 pytest-cov==2.6.0
pytest-faulthandler==1.5.0 pytest-faulthandler==1.5.0
pytest-instafail==0.4.0 pytest-instafail==0.4.0
pytest-mock==1.10.0 pytest-mock==1.10.0
pytest-qt==3.2.1 pytest-qt==3.2.2
pytest-repeat==0.7.0 pytest-repeat==0.7.0
pytest-rerunfailures==5.0 pytest-rerunfailures==5.0
pytest-travis-fold==1.3.0 pytest-travis-fold==1.3.0
pytest-xvfb==1.1.0 pytest-xvfb==1.1.0
PyVirtualDisplay==0.2.1 PyVirtualDisplay==0.2.1
six==1.11.0 six==1.12.0
vulture==1.0 vulture==1.0
Werkzeug==0.14.1 Werkzeug==0.14.1

View File

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

View File

@ -64,6 +64,7 @@ die() {
javascript_escape() { javascript_escape() {
# print the first argument in an escaped way, such that it can safely # print the first argument in an escaped way, such that it can safely
# be used within javascripts double quotes # be used within javascripts double quotes
# shellcheck disable=SC2001
sed "s,[\\\\'\"],\\\\&,g" <<< "$1" sed "s,[\\\\'\"],\\\\&,g" <<< "$1"
} }
@ -111,6 +112,7 @@ simplify_url() {
# are found: # are found:
no_entries_found() { no_entries_found() {
while [ 0 -eq "${#files[@]}" ] && [ -n "$simple_url" ]; do while [ 0 -eq "${#files[@]}" ] && [ -n "$simple_url" ]; do
# shellcheck disable=SC2001
shorter_simple_url=$(sed 's,^[^.]*\.,,' <<< "$simple_url") shorter_simple_url=$(sed 's,^[^.]*\.,,' <<< "$simple_url")
if [ "$shorter_simple_url" = "$simple_url" ] ; then if [ "$shorter_simple_url" = "$simple_url" ] ; then
# if no dot, then even remove the top level domain # 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): def find_pass_candidates(domain, password_store_path):
candidates = [] candidates = []
for path, directories, file_names in os.walk(password_store_path, followlinks=True): 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 continue
# Strip password store path prefix to get the relative pass path # Strip password store path prefix to get the relative pass path
pass_path = path[len(password_store_path) + 1:] pass_path = path[len(password_store_path) + 1:]
secrets = fnmatch.filter(file_names, '*.gpg') split_path = pass_path.split(os.path.sep)
candidates.extend(os.path.join(pass_path, os.path.splitext(secret)[0]) for secret in secrets) 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 return candidates

View File

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

View File

@ -18,10 +18,6 @@ disallow_untyped_decorators = True
# no_implicit_optional = True # no_implicit_optional = True
# warn_return_any = True # warn_return_any = True
[mypy-faulthandler]
# https://github.com/python/typeshed/pull/2627
ignore_missing_imports = True
[mypy-colorama] [mypy-colorama]
# https://github.com/tartley/colorama/issues/206 # https://github.com/tartley/colorama/issues/206
ignore_missing_imports = True ignore_missing_imports = True
@ -73,3 +69,19 @@ disallow_incomplete_defs = True
[mypy-qutebrowser.components.*] [mypy-qutebrowser.components.*]
disallow_untyped_defs = True disallow_untyped_defs = True
disallow_incomplete_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 ^QSettings::value: Empty key passed
^Icon theme ".*" not found ^Icon theme ".*" not found
^Error receiving trust for a CA certificate ^Error receiving trust for a CA certificate
^QBackingStore::endPaint\(\) called with active painter on backingstore paint device
xfail_strict = true xfail_strict = true
filterwarnings = filterwarnings =
error error

View File

@ -24,3 +24,4 @@ from qutebrowser.browser.browsertab import WebTabError, AbstractTab as Tab
from qutebrowser.browser.webelem import (Error as WebElemError, from qutebrowser.browser.webelem import (Error as WebElemError,
AbstractWebElement as WebElement) AbstractWebElement as WebElement)
from qutebrowser.utils.usertypes import ClickTarget, JsWorld 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 # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # 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 inspect
import typing import typing
@ -33,14 +63,16 @@ class CommandError(cmdexc.Error):
"""Raised when a command encounters an error while running. """Raised when a command encounters an error while running.
If your command handler encounters an error and cannot continue, raise this 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") raise cmdexc.CommandError("Message")
The message will then be shown in the qutebrowser status bar. The message will then be shown in the qutebrowser status bar.
Note that you should only raise this exception while a command handler is .. note::
run. Raising it at another point causes qutebrowser to crash due to an
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. unhandled exception.
""" """
@ -76,13 +108,7 @@ def check_exclusive(flags: typing.Iterable[bool],
class register: # noqa: N801,N806 pylint: disable=invalid-name class register: # noqa: N801,N806 pylint: disable=invalid-name
"""Decorator to register a new command handler. """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.
"""
def __init__(self, *, def __init__(self, *,
instance: str = None, instance: str = None,
@ -95,8 +121,11 @@ class register: # noqa: N801,N806 pylint: disable=invalid-name
Args: Args:
See class attributes. See class attributes.
""" """
# The object from the object registry to be used as "self".
self._instance = instance self._instance = instance
# The name (as string) or names (as list) of the command.
self._name = name self._name = name
# The arguments to pass to Command.
self._kwargs = kwargs self._kwargs = kwargs
def __call__(self, func: typing.Callable) -> typing.Callable: 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 class argument: # noqa: N801,N806 pylint: disable=invalid-name
"""Decorator to customize an argument for @cmdutils.register. """Decorator to customize an argument.
Attributes: You can customize how an argument is handled using the
_argname: The name of the argument to handle. ``@cmdutils.argument`` decorator *after* ``@cmdutils.register``. This can,
_kwargs: Keyword arguments, valid ArgInfo members 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: def __init__(self, argname: str, **kwargs: typing.Any) -> None:
self._argname = argname self._argname = argname # The name of the argument to handle.
self._kwargs = kwargs self._kwargs = kwargs # Valid ArgInfo members.
def __call__(self, func: typing.Callable) -> typing.Callable: def __call__(self, func: typing.Callable) -> typing.Callable:
funcname = func.__name__ funcname = func.__name__

View File

@ -21,9 +21,23 @@
import typing import typing
MYPY = False from PyQt5.QtCore import QUrl
if MYPY:
# pylint: disable=unused-import,useless-suppression
from qutebrowser.config import config 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) 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.commands import runners
from qutebrowser.api import cmdutils from qutebrowser.api import cmdutils
from qutebrowser.config import config, websettings, configfiles, configinit 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) qtnetworkdownloads, downloads, greasemonkey)
from qutebrowser.browser.network import proxy from qutebrowser.browser.network import proxy
from qutebrowser.browser.webkit import cookies, cache from qutebrowser.browser.webkit import cookies, cache
from qutebrowser.browser.webkit.network import networkmanager from qutebrowser.browser.webkit.network import networkmanager
from qutebrowser.extensions import loader
from qutebrowser.keyinput import macros from qutebrowser.keyinput import macros
from qutebrowser.mainwindow import mainwindow, prompt from qutebrowser.mainwindow import mainwindow, prompt
from qutebrowser.misc import (readline, ipc, savemanager, sessions, 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) usertypes, standarddir, error, qtutils)
# pylint: disable=unused-import # pylint: disable=unused-import
# We import those to run the cmdutils.register decorators. # 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.mainwindow.statusbar import command
from qutebrowser.misc import utilcmds from qutebrowser.misc import utilcmds
# pylint: enable=unused-import # pylint: enable=unused-import
@ -166,6 +165,8 @@ def init(args, crash_handler):
qApp.setQuitOnLastWindowClosed(False) qApp.setQuitOnLastWindowClosed(False)
_init_icon() _init_icon()
loader.init()
loader.load_components()
try: try:
_init_modules(args, crash_handler) _init_modules(args, crash_handler)
except (OSError, UnicodeDecodeError, browsertab.WebTabError) as e: except (OSError, UnicodeDecodeError, browsertab.WebTabError) as e:
@ -468,11 +469,6 @@ def _init_modules(args, crash_handler):
log.init.debug("Initializing websettings...") log.init.debug("Initializing websettings...")
websettings.init(args) 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...") log.init.debug("Initializing quickmarks...")
quickmark_manager = urlmarks.QuickmarkManager(qApp) quickmark_manager = urlmarks.QuickmarkManager(qApp)
objreg.register('quickmark-manager', quickmark_manager) objreg.register('quickmark-manager', quickmark_manager)

View File

@ -141,14 +141,11 @@ class TabData:
class AbstractAction: class AbstractAction:
"""Attribute of AbstractTab for Qt WebActions. """Attribute ``action`` 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)
"""
# The class actions are defined on (QWeb{Engine,}Page)
action_class = None # type: type action_class = None # type: type
# The type of the actions (QWeb{Engine,}Page.WebAction)
action_base = None # type: type action_base = None # type: type
def __init__(self, tab: 'AbstractTab') -> None: def __init__(self, tab: 'AbstractTab') -> None:
@ -200,7 +197,7 @@ class AbstractAction:
class AbstractPrinting: class AbstractPrinting:
"""Attribute of AbstractTab for printing the page.""" """Attribute ``printing`` of AbstractTab for printing the page."""
def __init__(self, tab: 'AbstractTab') -> None: def __init__(self, tab: 'AbstractTab') -> None:
self._widget = None self._widget = None
@ -271,7 +268,7 @@ class AbstractPrinting:
class AbstractSearch(QObject): class AbstractSearch(QObject):
"""Attribute of AbstractTab for doing searches. """Attribute ``search`` of AbstractTab for doing searches.
Attributes: Attributes:
text: The last thing this view was searched for. text: The last thing this view was searched for.
@ -279,15 +276,14 @@ class AbstractSearch(QObject):
this view. this view.
_flags: The flags of the last search (needs to be set by subclasses). _flags: The flags of the last search (needs to be set by subclasses).
_widget: The underlying WebView widget. _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) finished = pyqtSignal(bool)
#: Signal emitted when an existing search was cleared.
cleared = pyqtSignal() cleared = pyqtSignal()
_Callback = typing.Callable[[bool], None] _Callback = typing.Callable[[bool], None]
def __init__(self, tab: 'AbstractTab', parent: QWidget = None): def __init__(self, tab: 'AbstractTab', parent: QWidget = None):
@ -350,17 +346,13 @@ class AbstractSearch(QObject):
class AbstractZoom(QObject): class AbstractZoom(QObject):
"""Attribute of AbstractTab for controlling zoom. """Attribute ``zoom`` of AbstractTab for controlling zoom."""
Attributes:
_neighborlist: A NeighborList with the zoom levels.
_default_zoom_changed: Whether the zoom was changed from the default.
"""
def __init__(self, tab: 'AbstractTab', parent: QWidget = None) -> None: def __init__(self, tab: 'AbstractTab', parent: QWidget = None) -> None:
super().__init__(parent) super().__init__(parent)
self._tab = tab self._tab = tab
self._widget = None self._widget = None
# Whether zoom was changed from the default.
self._default_zoom_changed = False self._default_zoom_changed = False
self._init_neighborlist() self._init_neighborlist()
config.instance.changed.connect(self._on_config_changed) config.instance.changed.connect(self._on_config_changed)
@ -375,7 +367,9 @@ class AbstractZoom(QObject):
self._init_neighborlist() self._init_neighborlist()
def _init_neighborlist(self) -> None: def _init_neighborlist(self) -> None:
"""Initialize self._neighborlist.""" """Initialize self._neighborlist.
It is a NeighborList with the zoom levels."""
levels = config.val.zoom.levels levels = config.val.zoom.levels
self._neighborlist = usertypes.NeighborList( self._neighborlist = usertypes.NeighborList(
levels, mode=usertypes.NeighborList.Modes.edge) levels, mode=usertypes.NeighborList.Modes.edge)
@ -427,15 +421,12 @@ class AbstractZoom(QObject):
class AbstractCaret(QObject): class AbstractCaret(QObject):
"""Attribute of AbstractTab for caret browsing. """Attribute ``caret`` 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.
"""
#: Signal emitted when the selection was toggled.
#: (argument - whether the selection is now active)
selection_toggled = pyqtSignal(bool) selection_toggled = pyqtSignal(bool)
#: Emitted when a ``follow_selection`` action is done.
follow_selected_done = pyqtSignal() follow_selected_done = pyqtSignal()
def __init__(self, def __init__(self,
@ -522,16 +513,12 @@ class AbstractCaret(QObject):
class AbstractScroller(QObject): class AbstractScroller(QObject):
"""Attribute of AbstractTab to manage scroll position. """Attribute ``scroller`` 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.
"""
#: Signal emitted when the scroll position changed (int, int)
perc_changed = pyqtSignal(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() before_jump_requested = pyqtSignal()
def __init__(self, tab: 'AbstractTab', parent: QWidget = None): def __init__(self, tab: 'AbstractTab', parent: QWidget = None):
@ -833,42 +820,46 @@ class AbstractTabPrivate:
class AbstractTab(QWidget): class AbstractTab(QWidget):
"""An adapter for QWebView/QWebEngineView representing a single tab. """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.
"""
#: Signal emitted when a website requests to close this tab.
window_close_requested = pyqtSignal() window_close_requested = pyqtSignal()
#: Signal emitted when a link is hovered (the hover text)
link_hovered = pyqtSignal(str) link_hovered = pyqtSignal(str)
#: Signal emitted when a page started loading
load_started = pyqtSignal() load_started = pyqtSignal()
#: Signal emitted when a page is loading (progress percentage)
load_progress = pyqtSignal(int) load_progress = pyqtSignal(int)
#: Signal emitted when a page finished loading (success as bool)
load_finished = pyqtSignal(bool) load_finished = pyqtSignal(bool)
#: Signal emitted when a page's favicon changed (icon as QIcon)
icon_changed = pyqtSignal(QIcon) icon_changed = pyqtSignal(QIcon)
#: Signal emitted when a page's title changed (new title as str)
title_changed = pyqtSignal(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) new_tab_requested = pyqtSignal(QUrl)
#: Signal emitted when a page's URL changed (url as QUrl)
url_changed = pyqtSignal(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) contents_size_changed = pyqtSignal(QSizeF)
# url, requested url, title #: Signal emitted when a page requested full-screen (bool)
history_item_triggered = pyqtSignal(QUrl, QUrl, str)
fullscreen_requested = pyqtSignal(bool) fullscreen_requested = pyqtSignal(bool)
renderer_process_terminated = pyqtSignal(TerminationStatus, int) #: Signal emitted before load starts (URL as QUrl)
before_load_started = pyqtSignal(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, def __init__(self, *, win_id: int, private: bool,
parent: QWidget = None) -> None: parent: QWidget = None) -> None:
self.is_private = private self.is_private = private
@ -952,6 +943,10 @@ class AbstractTab(QWidget):
evt.posted = True evt.posted = True
QApplication.postEvent(recipient, evt) 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) @pyqtSlot(QUrl)
def _on_before_load_started(self, url: QUrl) -> None: def _on_before_load_started(self, url: QUrl) -> None:
"""Adjust the title if we are going to visit a URL soon.""" """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) webelem, downloads)
from qutebrowser.keyinput import modeman, keyutils from qutebrowser.keyinput import modeman, keyutils
from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils, from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
objreg, utils, standarddir) objreg, utils, standarddir, debug)
from qutebrowser.utils.usertypes import KeyMode from qutebrowser.utils.usertypes import KeyMode
from qutebrowser.misc import editor, guiprocess, objects from qutebrowser.misc import editor, guiprocess, objects
from qutebrowser.completion.models import urlmodel, miscmodels from qutebrowser.completion.models import urlmodel, miscmodels
@ -316,7 +316,7 @@ class CommandDispatcher:
else: else:
# Explicit count with a tab that doesn't exist. # Explicit count with a tab that doesn't exist.
return return
elif curtab.data.pinned: elif curtab.navigation_blocked():
message.info("Tab is pinned!") message.info("Tab is pinned!")
else: else:
curtab.load_url(cur_url) curtab.load_url(cur_url)
@ -1721,4 +1721,10 @@ class CommandDispatcher:
return return
window = self._tabbed_browser.widget.window() window = self._tabbed_browser.widget.window()
if not window.isFullScreen():
window.state_before_fullscreen = window.windowState()
window.setWindowState(window.windowState() ^ Qt.WindowFullScreen) 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): def __init__(self, win_id, parent=None):
super().__init__(parent) super().__init__(parent)
if not utils.is_mac:
self.setStyle(QStyleFactory.create('Fusion')) self.setStyle(QStyleFactory.create('Fusion'))
config.set_register_stylesheet(self) config.set_register_stylesheet(self)
self.setResizeMode(QListView.Adjust) self.setResizeMode(QListView.Adjust)

View File

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

View File

@ -19,15 +19,23 @@
"""Generic web element related code.""" """Generic web element related code."""
import typing
import collections.abc 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 PyQt5.QtGui import QMouseEvent
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.keyinput import modeman from qutebrowser.keyinput import modeman
from qutebrowser.mainwindow import mainwindow from qutebrowser.mainwindow import mainwindow
from qutebrowser.utils import log, usertypes, utils, qtutils, objreg 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): class Error(Exception):
@ -40,7 +48,7 @@ class OrphanedError(Error):
"""Raised when a webelement's parent has vanished.""" """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.""" """Get a CSS selector for the given group/URL."""
selectors = config.instance.get('hints.selectors', url) selectors = config.instance.get('hints.selectors', url)
if group not in selectors: if group not in selectors:
@ -54,76 +62,74 @@ def css_selector(group, url):
class AbstractWebElement(collections.abc.MutableMapping): class AbstractWebElement(collections.abc.MutableMapping):
"""A wrapper around QtWebKit/QtWebEngine web element. """A wrapper around QtWebKit/QtWebEngine web element."""
Attributes: def __init__(self, tab: 'browsertab.AbstractTab') -> None:
tab: The tab associated with this element.
"""
def __init__(self, tab):
self._tab = tab self._tab = tab
def __eq__(self, other): def __eq__(self, other: object) -> bool:
raise NotImplementedError raise NotImplementedError
def __str__(self): def __str__(self) -> str:
raise NotImplementedError raise NotImplementedError
def __getitem__(self, key): def __getitem__(self, key: str) -> str:
raise NotImplementedError raise NotImplementedError
def __setitem__(self, key, val): def __setitem__(self, key: str, val: str) -> None:
raise NotImplementedError raise NotImplementedError
def __delitem__(self, key): def __delitem__(self, key: str) -> None:
raise NotImplementedError raise NotImplementedError
def __iter__(self): def __iter__(self) -> typing.Iterator[str]:
raise NotImplementedError raise NotImplementedError
def __len__(self): def __len__(self) -> int:
raise NotImplementedError raise NotImplementedError
def __repr__(self): def __repr__(self) -> str:
try: try:
html = utils.compact_text(self.outer_xml(), 500) html = utils.compact_text(self.outer_xml(), 500)
except Error: except Error:
html = None html = None
return utils.get_repr(self, html=html) 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.""" """Check if this element has a valid frame attached."""
raise NotImplementedError raise NotImplementedError
def geometry(self): def geometry(self) -> QRect:
"""Get the geometry for this element.""" """Get the geometry for this element."""
raise NotImplementedError raise NotImplementedError
def classes(self): def classes(self) -> typing.List[str]:
"""Get a list of classes assigned to this element.""" """Get a list of classes assigned to this element."""
raise NotImplementedError raise NotImplementedError
def tag_name(self): def tag_name(self) -> str:
"""Get the tag name of this element. """Get the tag name of this element.
The returned name will always be lower-case. The returned name will always be lower-case.
""" """
raise NotImplementedError raise NotImplementedError
def outer_xml(self): def outer_xml(self) -> str:
"""Get the full HTML representation of this element.""" """Get the full HTML representation of this element."""
raise NotImplementedError raise NotImplementedError
def value(self): def value(self) -> JsValueType:
"""Get the value attribute for this element, or None.""" """Get the value attribute for this element, or None."""
raise NotImplementedError raise NotImplementedError
def set_value(self, value): def set_value(self, value: JsValueType) -> None:
"""Set the element value.""" """Set the element value."""
raise NotImplementedError raise NotImplementedError
def dispatch_event(self, event, bubbles=False, def dispatch_event(self, event: str,
cancelable=False, composed=False): bubbles: bool = False,
cancelable: bool = False,
composed: bool = False) -> None:
"""Dispatch an event to the element. """Dispatch an event to the element.
Args: Args:
@ -134,35 +140,25 @@ class AbstractWebElement(collections.abc.MutableMapping):
""" """
raise NotImplementedError raise NotImplementedError
def insert_text(self, text): def insert_text(self, text: str) -> None:
"""Insert the given text into the element.""" """Insert the given text into the element."""
raise NotImplementedError 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. """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: Args:
elem_geometry: The geometry of the element, or None. elem_geometry: The geometry of the element, or None.
Calling QWebElement::geometry is rather expensive so no_js: Fall back to the Python implementation.
we want to avoid doing it twice.
no_js: Fall back to the Python implementation
""" """
raise NotImplementedError raise NotImplementedError
def is_writable(self): def is_writable(self) -> bool:
"""Check whether an element is writable.""" """Check whether an element is writable."""
return not ('disabled' in self or 'readonly' in self) 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. """Check if an element has a contenteditable attribute.
Args: Args:
@ -177,7 +173,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
except KeyError: except KeyError:
return False return False
def _is_editable_object(self): def _is_editable_object(self) -> bool:
"""Check if an object-element is editable.""" """Check if an object-element is editable."""
if 'type' not in self: if 'type' not in self:
log.webelem.debug("<object> without type clicked...") log.webelem.debug("<object> without type clicked...")
@ -193,7 +189,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
# Image/Audio/... # Image/Audio/...
return False return False
def _is_editable_input(self): def _is_editable_input(self) -> bool:
"""Check if an input-element is editable. """Check if an input-element is editable.
Return: Return:
@ -210,7 +206,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
else: else:
return False return False
def _is_editable_classes(self): def _is_editable_classes(self) -> bool:
"""Check if an element is editable based on its classes. """Check if an element is editable based on its classes.
Return: Return:
@ -229,7 +225,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
return True return True
return False 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. """Check whether we should switch to insert mode for this element.
Args: Args:
@ -260,17 +256,17 @@ class AbstractWebElement(collections.abc.MutableMapping):
return self._is_editable_classes() and not strict return self._is_editable_classes() and not strict
return False return False
def is_text_input(self): def is_text_input(self) -> bool:
"""Check if this element is some kind of text box.""" """Check if this element is some kind of text box."""
roles = ('combobox', 'textbox') roles = ('combobox', 'textbox')
tag = self.tag_name() tag = self.tag_name()
return self.get('role', None) in roles or tag in ['input', 'textarea'] 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.""" """Remove target from link."""
raise NotImplementedError 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. """Resolve the URL in the element's src/href attribute.
Args: Args:
@ -297,16 +293,16 @@ class AbstractWebElement(collections.abc.MutableMapping):
qtutils.ensure_valid(url) qtutils.ensure_valid(url)
return url return url
def is_link(self): def is_link(self) -> bool:
"""Return True if this AbstractWebElement is a link.""" """Return True if this AbstractWebElement is a link."""
href_tags = ['a', 'area', 'link'] href_tags = ['a', 'area', 'link']
return self.tag_name() in href_tags and 'href' in self 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.""" """Return True if clicking this element needs user interaction."""
raise NotImplementedError raise NotImplementedError
def _mouse_pos(self): def _mouse_pos(self) -> QPoint:
"""Get the position to click/hover.""" """Get the position to click/hover."""
# Click the center of the largest square fitting into the top/left # 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 # 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!") raise Error("Element position is out of view!")
return pos return pos
def _move_text_cursor(self): def _move_text_cursor(self) -> None:
"""Move cursor to end after clicking.""" """Move cursor to end after clicking."""
raise NotImplementedError 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.""" """Send a fake click event to the element."""
pos = self._mouse_pos() pos = self._mouse_pos()
log.webelem.debug("Sending fake click to {!r} at position {} with " log.webelem.debug("Sending fake click to {!r} at position {} with "
"target {}".format(self, pos, click_target)) "target {}".format(self, pos, click_target))
modifiers = { target_modifiers = {
usertypes.ClickTarget.normal: Qt.NoModifier, usertypes.ClickTarget.normal: Qt.NoModifier,
usertypes.ClickTarget.window: Qt.AltModifier | Qt.ShiftModifier, usertypes.ClickTarget.window: Qt.AltModifier | Qt.ShiftModifier,
usertypes.ClickTarget.tab: Qt.ControlModifier, usertypes.ClickTarget.tab: Qt.ControlModifier,
usertypes.ClickTarget.tab_bg: Qt.ControlModifier, usertypes.ClickTarget.tab_bg: Qt.ControlModifier,
} }
if config.val.tabs.background: if config.val.tabs.background:
modifiers[usertypes.ClickTarget.tab] |= Qt.ShiftModifier target_modifiers[usertypes.ClickTarget.tab] |= Qt.ShiftModifier
else: 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 = [ events = [
QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton, QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
Qt.NoModifier), Qt.NoModifier),
QMouseEvent(QEvent.MouseButtonPress, pos, Qt.LeftButton, QMouseEvent(QEvent.MouseButtonPress, pos, Qt.LeftButton,
Qt.LeftButton, modifiers[click_target]), Qt.LeftButton, modifiers),
QMouseEvent(QEvent.MouseButtonRelease, pos, Qt.LeftButton, QMouseEvent(QEvent.MouseButtonRelease, pos, Qt.LeftButton,
Qt.NoButton, modifiers[click_target]), Qt.NoButton, modifiers),
] ]
for evt in events: for evt in events:
@ -358,15 +357,15 @@ class AbstractWebElement(collections.abc.MutableMapping):
QTimer.singleShot(0, self._move_text_cursor) 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.""" """Fake a click on an editable input field."""
raise NotImplementedError 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.""" """Fake a click by using the JS .click() method."""
raise NotImplementedError 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.""" """Fake a click on an element with a href by opening the link."""
baseurl = self._tab.url() baseurl = self._tab.url()
url = self.resolve_url(baseurl) url = self.resolve_url(baseurl)
@ -388,7 +387,8 @@ class AbstractWebElement(collections.abc.MutableMapping):
else: else:
raise ValueError("Unknown ClickTarget {}".format(click_target)) 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. """Simulate a click on the element.
Args: Args:
@ -425,7 +425,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
else: else:
raise ValueError("Unknown ClickTarget {}".format(click_target)) raise ValueError("Unknown ClickTarget {}".format(click_target))
def hover(self): def hover(self) -> None:
"""Simulate a mouse hover over the element.""" """Simulate a mouse hover over the element."""
pos = self._mouse_pos() pos = self._mouse_pos()
event = QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton, 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.config import config
from qutebrowser.browser import shared from qutebrowser.browser import shared
from qutebrowser.utils import utils, log, debug from qutebrowser.utils import utils, log, debug
from qutebrowser.extensions import interceptors
class RequestInterceptor(QWebEngineUrlRequestInterceptor): class RequestInterceptor(QWebEngineUrlRequestInterceptor):
"""Handle ad blocking and custom headers.""" """Handle ad blocking and custom headers."""
def __init__(self, host_blocker, args, parent=None): def __init__(self, args, parent=None):
super().__init__(parent) super().__init__(parent)
self._host_blocker = host_blocker
self._args = args self._args = args
def install(self, profile): def install(self, profile):
@ -84,9 +84,10 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor):
return return
# FIXME:qtwebengine only block ads for NavigationTypeOther? # FIXME:qtwebengine only block ads for NavigationTypeOther?
if self._host_blocker.is_blocked(url, first_party): request = interceptors.Request(first_party_url=first_party,
log.webview.info("Request to {} blocked by host blocker.".format( request_url=url)
url.host())) interceptors.run(request)
if request.is_blocked:
info.block(True) info.block(True)
for header, value in shared.custom_headers(url=url): for header, value in shared.custom_headers(url=url):

View File

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

View File

@ -22,6 +22,11 @@
from PyQt5.QtCore import QBuffer, QIODevice, QUrl from PyQt5.QtCore import QBuffer, QIODevice, QUrl
from PyQt5.QtWebEngineCore import (QWebEngineUrlSchemeHandler, from PyQt5.QtWebEngineCore import (QWebEngineUrlSchemeHandler,
QWebEngineUrlRequestJob) 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.browser import qutescheme
from qutebrowser.utils import log, qtutils from qutebrowser.utils import log, qtutils
@ -33,8 +38,12 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
def install(self, profile): def install(self, profile):
"""Install the handler for qute:// URLs on the given 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) 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 # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-63378
profile.installUrlSchemeHandler(b'chrome-error', self) profile.installUrlSchemeHandler(b'chrome-error', self)
profile.installUrlSchemeHandler(b'chrome-extension', self) profile.installUrlSchemeHandler(b'chrome-extension', self)
@ -130,3 +139,16 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
buf.seek(0) buf.seek(0)
buf.close() buf.close()
job.reply(mimetype.encode('ascii'), buf) 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, from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile,
QWebEnginePage) QWebEnginePage)
from qutebrowser.browser.webengine import spell from qutebrowser.browser.webengine import spell, webenginequtescheme
from qutebrowser.config import config, websettings from qutebrowser.config import config, websettings
from qutebrowser.config.websettings import AttributeInfo as Attr from qutebrowser.config.websettings import AttributeInfo as Attr
from qutebrowser.utils import utils, standarddir, qtutils, message, log from qutebrowser.utils import utils, standarddir, qtutils, message, log
@ -298,6 +298,7 @@ def init(args):
not hasattr(QWebEnginePage, 'setInspectedPage')): # only Qt < 5.11 not hasattr(QWebEnginePage, 'setInspectedPage')): # only Qt < 5.11
os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = str(utils.random_port()) os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = str(utils.random_port())
webenginequtescheme.init()
spell.init() spell.init()
_init_profiles() _init_profiles()

View File

@ -60,10 +60,8 @@ def init():
_qute_scheme_handler.install(webenginesettings.private_profile) _qute_scheme_handler.install(webenginesettings.private_profile)
log.init.debug("Initializing request interceptor...") log.init.debug("Initializing request interceptor...")
host_blocker = objreg.get('host-blocker')
args = objreg.get('args') args = objreg.get('args')
req_interceptor = interceptor.RequestInterceptor( req_interceptor = interceptor.RequestInterceptor(args=args, parent=app)
host_blocker, args=args, parent=app)
req_interceptor.install(webenginesettings.default_profile) req_interceptor.install(webenginesettings.default_profile)
req_interceptor.install(webenginesettings.private_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 import downloads
from qutebrowser.browser.webkit import webkitelem from qutebrowser.browser.webkit import webkitelem
from qutebrowser.utils import log, objreg, message, usertypes, utils, urlutils from qutebrowser.utils import log, objreg, message, usertypes, utils, urlutils
from qutebrowser.extensions import interceptors
@attr.s @attr.s
@ -354,8 +355,9 @@ class _Downloader:
# qute, see the comments/discussion on # qute, see the comments/discussion on
# https://github.com/qutebrowser/qutebrowser/pull/962#discussion_r40256987 # https://github.com/qutebrowser/qutebrowser/pull/962#discussion_r40256987
# and https://github.com/qutebrowser/qutebrowser/issues/1053 # and https://github.com/qutebrowser/qutebrowser/issues/1053
host_blocker = objreg.get('host-blocker') request = interceptors.Request(first_party_url=None, request_url=url)
if host_blocker.is_blocked(url): interceptors.run(request)
if request.is_blocked:
log.downloads.debug("Skipping {}, host-blocked".format(url)) log.downloads.debug("Skipping {}, host-blocked".format(url))
# We still need an empty file in the output, QWebView can be pretty # We still need an empty file in the output, QWebView can be pretty
# picky about displaying a file correctly when not all assets are # 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, from qutebrowser.utils import (message, log, usertypes, utils, objreg,
urlutils, debug) urlutils, debug)
from qutebrowser.browser import shared from qutebrowser.browser import shared
from qutebrowser.extensions import interceptors
from qutebrowser.browser.webkit import certificateerror from qutebrowser.browser.webkit import certificateerror
from qutebrowser.browser.webkit.network import (webkitqutescheme, networkreply, from qutebrowser.browser.webkit.network import (webkitqutescheme, networkreply,
filescheme) filescheme)
@ -405,10 +406,10 @@ class NetworkManager(QNetworkAccessManager):
# the webpage shutdown here. # the webpage shutdown here.
current_url = QUrl() current_url = QUrl()
host_blocker = objreg.get('host-blocker') request = interceptors.Request(first_party_url=current_url,
if host_blocker.is_blocked(req.url(), current_url): request_url=req.url())
log.webview.info("Request to {} blocked by host blocker.".format( interceptors.run(request)
req.url().host())) if request.is_blocked:
return networkreply.ErrorNetworkReply( return networkreply.ErrorNetworkReply(
req, HOSTBLOCK_ERROR_STRING, QNetworkReply.ContentAccessDenied, req, HOSTBLOCK_ERROR_STRING, QNetworkReply.ContentAccessDenied,
self) self)

View File

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

View File

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

View File

@ -17,4 +17,4 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # 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 functools
import posixpath import posixpath
import zipfile import zipfile
import logging
import typing
import pathlib
from qutebrowser.browser import downloads from PyQt5.QtCore import QUrl
from qutebrowser.config import config
from qutebrowser.utils import objreg, standarddir, log, message from qutebrowser.api import (cmdutils, hook, config, message, downloads,
from qutebrowser.api import cmdutils interceptor, apitypes)
def _guess_zip_filename(zf): logger = logging.getLogger('misc')
"""Guess which file to use inside a zip file. _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() files = zf.namelist()
if len(files) == 1: if len(files) == 1:
return files[0] return files[0]
@ -47,7 +50,7 @@ def _guess_zip_filename(zf):
raise FileNotFoundError("No hosts file found in zip") 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.""" """Get a usable file object to read the hosts file from."""
byte_io.seek(0) # rewind downloaded file byte_io.seek(0) # rewind downloaded file
if zipfile.is_zipfile(byte_io): if zipfile.is_zipfile(byte_io):
@ -60,24 +63,20 @@ def get_fileobj(byte_io):
return byte_io return byte_io
def _is_whitelisted_url(url): def _is_whitelisted_url(url: QUrl) -> bool:
"""Check if the given URL is on the adblock whitelist. """Check if the given URL is on the adblock whitelist."""
Args:
url: The URL to check as QUrl.
"""
for pattern in config.val.content.host_blocking.whitelist: for pattern in config.val.content.host_blocking.whitelist:
if pattern.matches(url): if pattern.matches(url):
return True return True
return False return False
class _FakeDownload: class _FakeDownload(downloads.TempDownload):
"""A download stub to use on_download_finished with local files.""" """A download stub to use on_download_finished with local files."""
def __init__(self, fileobj): def __init__(self, # pylint: disable=super-init-not-called
self.basename = os.path.basename(fileobj.name) fileobj: typing.IO[bytes]) -> None:
self.fileobj = fileobj self.fileobj = fileobj
self.successful = True self.successful = True
@ -93,37 +92,46 @@ class HostBlocker:
_done_count: How many files have been read successfully. _done_count: How many files have been read successfully.
_local_hosts_file: The path to the blocked-hosts file. _local_hosts_file: The path to the blocked-hosts file.
_config_hosts_file: The path to a blocked-hosts in ~/.config _config_hosts_file: The path to a blocked-hosts in ~/.config
_has_basedir: Whether a custom --basedir is set.
""" """
def __init__(self): def __init__(self, *, data_dir: pathlib.Path, config_dir: pathlib.Path,
self._blocked_hosts = set() has_basedir: bool = False) -> None:
self._config_blocked_hosts = set() self._has_basedir = has_basedir
self._in_progress = [] 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 self._done_count = 0
data_dir = standarddir.data() self._local_hosts_file = str(data_dir / 'blocked-hosts')
self._local_hosts_file = os.path.join(data_dir, 'blocked-hosts') self.update_files()
self._update_files()
config_dir = standarddir.config() self._config_hosts_file = str(config_dir / 'blocked-hosts')
self._config_hosts_file = os.path.join(config_dir, 'blocked-hosts')
config.instance.changed.connect(self._update_files) def _is_blocked(self, request_url: QUrl,
first_party_url: QUrl = None) -> bool:
def is_blocked(self, url, first_party_url=None): """Check whether the given request is blocked."""
"""Check if the given URL (as QUrl) is blocked."""
if first_party_url is not None and not first_party_url.isValid(): if first_party_url is not None and not first_party_url.isValid():
first_party_url = None 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): url=first_party_url):
return False return False
host = url.host() host = request_url.host()
return ((host in self._blocked_hosts or return ((host in self._blocked_hosts or
host in self._config_blocked_hosts) and 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. """Read hosts from the given filename.
Args: Args:
@ -141,11 +149,11 @@ class HostBlocker:
for line in f: for line in f:
target.add(line.strip()) target.add(line.strip())
except (OSError, UnicodeDecodeError): except (OSError, UnicodeDecodeError):
log.misc.exception("Failed to read host blocklist!") logger.exception("Failed to read host blocklist!")
return True return True
def read_hosts(self): def read_hosts(self) -> None:
"""Read hosts from the existing blocked-hosts file.""" """Read hosts from the existing blocked-hosts file."""
self._blocked_hosts = set() self._blocked_hosts = set()
@ -156,24 +164,17 @@ class HostBlocker:
self._blocked_hosts) self._blocked_hosts)
if not found: if not found:
args = objreg.get('args')
if (config.val.content.host_blocking.lists and if (config.val.content.host_blocking.lists and
args.basedir is None and not self._has_basedir and
config.val.content.host_blocking.enabled): config.val.content.host_blocking.enabled):
message.info("Run :adblock-update to get adblock lists.") message.info("Run :adblock-update to get adblock lists.")
@cmdutils.register(instance='host-blocker') def adblock_update(self) -> None:
def adblock_update(self): """Update the adblock block lists."""
"""Update the adblock block lists.
This updates `~/.local/share/qutebrowser/blocked-hosts` with downloaded
host lists and re-reads `~/.config/qutebrowser/blocked-hosts`.
"""
self._read_hosts_file(self._config_hosts_file, self._read_hosts_file(self._config_hosts_file,
self._config_blocked_hosts) self._config_blocked_hosts)
self._blocked_hosts = set() self._blocked_hosts = set()
self._done_count = 0 self._done_count = 0
download_manager = objreg.get('qtnetwork-download-manager')
for url in config.val.content.host_blocking.lists: for url in config.val.content.host_blocking.lists:
if url.scheme() == 'file': if url.scheme() == 'file':
filename = url.toLocalFile() filename = url.toLocalFile()
@ -184,16 +185,12 @@ class HostBlocker:
else: else:
self._import_local(filename) self._import_local(filename)
else: else:
fobj = io.BytesIO() download = downloads.download_temp(url)
fobj.name = 'adblock: ' + url.host()
target = downloads.FileObjDownloadTarget(fobj)
download = download_manager.get(url, target=target,
auto_remove=True)
self._in_progress.append(download) self._in_progress.append(download)
download.finished.connect( download.finished.connect(
functools.partial(self._on_download_finished, download)) 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. """Adds the contents of a file to the blocklist.
Args: Args:
@ -209,24 +206,24 @@ class HostBlocker:
self._in_progress.append(download) self._in_progress.append(download)
self._on_download_finished(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. """Parse a line from a host file.
Args: Args:
line: The bytes object to parse. raw_line: The bytes object to parse.
Returns: Returns:
True if parsing succeeded, False otherwise. 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 # Ignoring comments early so we don't have to care about
# encoding errors in them. # encoding errors in them.
return True return True
try: try:
line = line.decode('utf-8') line = raw_line.decode('utf-8')
except UnicodeDecodeError: except UnicodeDecodeError:
log.misc.error("Failed to decode: {!r}".format(line)) logger.error("Failed to decode: {!r}".format(raw_line))
return False return False
# Remove comments # Remove comments
@ -257,14 +254,11 @@ class HostBlocker:
return True return True
def _merge_file(self, byte_io): def _merge_file(self, byte_io: io.BytesIO) -> None:
"""Read and merge host files. """Read and merge host files.
Args: Args:
byte_io: The BytesIO object of the completed download. byte_io: The BytesIO object of the completed download.
Return:
A set of the merged hosts.
""" """
error_count = 0 error_count = 0
line_count = 0 line_count = 0
@ -282,12 +276,12 @@ class HostBlocker:
if not ok: if not ok:
error_count += 1 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: if error_count > 0:
message.error("adblock: {} read errors for {}".format( message.error("adblock: {} read errors for {}".format(
error_count, byte_io.name)) error_count, byte_io.name))
def _on_lists_downloaded(self): def _on_lists_downloaded(self) -> None:
"""Install block lists after files have been downloaded.""" """Install block lists after files have been downloaded."""
with open(self._local_hosts_file, 'w', encoding='utf-8') as f: with open(self._local_hosts_file, 'w', encoding='utf-8') as f:
for host in sorted(self._blocked_hosts): for host in sorted(self._blocked_hosts):
@ -295,8 +289,7 @@ class HostBlocker:
message.info("adblock: Read {} hosts from {} sources.".format( message.info("adblock: Read {} hosts from {} sources.".format(
len(self._blocked_hosts), self._done_count)) len(self._blocked_hosts), self._done_count))
@config.change_filter('content.host_blocking.lists') def update_files(self) -> None:
def _update_files(self):
"""Update files when the config changed.""" """Update files when the config changed."""
if not config.val.content.host_blocking.lists: if not config.val.content.host_blocking.lists:
try: try:
@ -304,13 +297,13 @@ class HostBlocker:
except FileNotFoundError: except FileNotFoundError:
pass pass
except OSError as e: 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. """Check if all downloads are finished and if so, trigger reading.
Arguments: Arguments:
download: The finished DownloadItem. download: The finished download.
""" """
self._in_progress.remove(download) self._in_progress.remove(download)
if download.successful: if download.successful:
@ -323,4 +316,32 @@ class HostBlocker:
try: try:
self._on_lists_downloaded() self._on_lists_downloaded()
except OSError: 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) @cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
def home(tab: apitypes.Tab) -> None: def home(tab: apitypes.Tab) -> None:
"""Open main startpage in current tab.""" """Open main startpage in current tab."""
if tab.data.pinned: if tab.navigation_blocked():
message.info("Tab is pinned!") message.info("Tab is pinned!")
else: else:
tab.load_url(config.val.url.start_pages[0]) tab.load_url(config.val.url.start_pages[0])
@ -238,7 +238,7 @@ def tab_mute(tab: apitypes.Tab) -> None:
if tab is None: if tab is None:
return return
try: 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: except apitypes.WebTabError as e:
raise cmdutils.CommandError(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)): not configdata.is_valid_prefix(self._option)):
raise configexc.NoOptionError(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.""" """Check if the given option matches the filter."""
if option is None: if option is None:
# Called directly, not from a config change event. # 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) @functools.wraps(func)
def func_wrapper(option: str = None) -> typing.Any: def func_wrapper(option: str = None) -> typing.Any:
"""Call the underlying function.""" """Call the underlying function."""
if self._check_match(option): if self.check_match(option):
return func() return func()
return None return None
return func_wrapper return func_wrapper
@ -128,7 +128,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name
def meth_wrapper(wrapper_self: typing.Any, def meth_wrapper(wrapper_self: typing.Any,
option: str = None) -> typing.Any: option: str = None) -> typing.Any:
"""Call the underlying function.""" """Call the underlying function."""
if self._check_match(option): if self.check_match(option):
return func(wrapper_self) return func(wrapper_self)
return None return None
return meth_wrapper return meth_wrapper

View File

@ -46,7 +46,9 @@ class ConfigCache:
self._cache[attr] = config.instance.get(attr) self._cache[attr] = config.instance.get(attr)
def __getitem__(self, attr: str) -> typing.Any: 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 assert not config.instance.get_opt(attr).supports_pattern
self._cache[attr] = config.instance.get(attr) self._cache[attr] = config.instance.get(attr)
return self._cache[attr] return self._cache[attr]

View File

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

View File

@ -1768,6 +1768,11 @@ tabs.pinned.shrink:
type: Bool type: Bool
desc: Shrink pinned tabs down to their contents. 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: tabs.wrap:
default: true default: true
type: Bool type: Bool

View File

@ -308,7 +308,7 @@ class YamlConfig(QObject):
self._migrate_bool(settings, 'tabs.favicons.show', 'always', 'never') self._migrate_bool(settings, 'tabs.favicons.show', 'always', 'never')
self._migrate_bool(settings, 'scrolling.bar', self._migrate_bool(settings, 'scrolling.bar',
'when-searching', 'never') 'always', 'when-searching')
self._migrate_bool(settings, 'qt.force_software_rendering', self._migrate_bool(settings, 'qt.force_software_rendering',
'software-opengl', 'none') '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, /* eslint-disable max-len, max-statements, complexity,
default-case, valid-jsdoc */ default-case */
// Copyright 2014 The Chromium Authors. All rights reserved. // Copyright 2014 The Chromium Authors. All rights reserved.
// //

View File

@ -64,7 +64,7 @@ class NotInModeError(Exception):
def init(win_id, parent): def init(win_id, parent):
"""Initialize the mode manager and the keyparsers for the given win_id.""" """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) modeman = ModeManager(win_id, parent)
objreg.register('mode-manager', modeman, scope='window', window=win_id) objreg.register('mode-manager', modeman, scope='window', window=win_id)
keyparsers = { keyparsers = {

View File

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

View File

@ -238,6 +238,9 @@ def _handle_wayland():
if has_qt511 and config.val.qt.force_software_rendering == 'chromium': if has_qt511 and config.val.qt.force_software_rendering == 'chromium':
return return
if qtutils.version_check('5.11.2', compiled=False):
return
buttons = [] buttons = []
text = "<p>You can work around this in one of the following ways:</p>" 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') network = logging.getLogger('network')
sql = logging.getLogger('sql') sql = logging.getLogger('sql')
greasemonkey = logging.getLogger('greasemonkey') greasemonkey = logging.getLogger('greasemonkey')
extensions = logging.getLogger('extensions')
LOGGER_NAMES = [ LOGGER_NAMES = [
'statusbar', 'completion', 'init', 'url', 'statusbar', 'completion', 'init', 'url',
@ -146,7 +147,7 @@ LOGGER_NAMES = [
'js', 'qt', 'rfc6266', 'ipc', 'shlexer', 'js', 'qt', 'rfc6266', 'ipc', 'shlexer',
'save', 'message', 'config', 'sessions', 'save', 'message', 'config', 'sessions',
'webelem', 'prompt', 'network', 'sql', '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: 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: Args:
message: The message to show message: The message to show.
stack: The stack trace to show. stack: The stack trace to show (if any).
replace: Replace existing messages with replace=True replace: Replace existing messages which are still being shown.
""" """
if stack is None: if stack is None:
stack = ''.join(traceback.format_stack()) 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: def warning(message: str, *, replace: bool = False) -> None:
"""Convenience function to display a warning message in the statusbar. """Display a warning message.
Args: Args:
message: The message to show message: The message to show.
replace: Replace existing messages with replace=True replace: Replace existing messages which are still being shown.
""" """
_log_stack('warning', ''.join(traceback.format_stack())) _log_stack('warning', ''.join(traceback.format_stack()))
log.message.warning(message) log.message.warning(message)
@ -72,11 +72,11 @@ def warning(message: str, *, replace: bool = False) -> None:
def info(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: Args:
message: The message to show message: The message to show.
replace: Replace existing messages with replace=True replace: Replace existing messages which are still being shown.
""" """
log.message.info(message) log.message.info(message)
global_bridge.show(usertypes.MessageLevel.info, message, replace) global_bridge.show(usertypes.MessageLevel.info, message, replace)

View File

@ -210,15 +210,33 @@ PromptMode = enum.Enum('PromptMode', ['yesno', 'text', 'user_pwd', 'alert',
'download']) 'download'])
# Where to open a clicked link. class ClickTarget(enum.Enum):
ClickTarget = enum.Enum('ClickTarget', ['normal', 'tab', 'tab_bg', 'window',
'hover']) """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 class KeyMode(enum.Enum):
KeyMode = enum.Enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt',
'insert', 'passthrough', 'caret', 'set_mark', """Key input modes."""
'jump_mark', 'record_macro', 'run_macro'])
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): class Exit(enum.IntEnum):
@ -241,8 +259,14 @@ LoadStatus = enum.Enum('LoadStatus', ['none', 'success', 'success_https',
Backend = enum.Enum('Backend', ['QtWebKit', 'QtWebEngine']) Backend = enum.Enum('Backend', ['QtWebKit', 'QtWebEngine'])
# JS world for QtWebEngine class JsWorld(enum.Enum):
JsWorld = enum.Enum('JsWorld', ['main', 'application', 'user', 'jseval'])
"""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 # 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) CSafeDumper as YamlDumper)
YAML_C_EXT = True YAML_C_EXT = True
except ImportError: # pragma: no cover 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 YAML_C_EXT = False
import qutebrowser import qutebrowser

View File

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

View File

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

View File

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

View File

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

View File

@ -30,6 +30,7 @@ import argparse
import vulture import vulture
import qutebrowser.app # pylint: disable=unused-import import qutebrowser.app # pylint: disable=unused-import
from qutebrowser.extensions import loader
from qutebrowser.misc import objects from qutebrowser.misc import objects
from qutebrowser.utils import utils from qutebrowser.utils import utils
from qutebrowser.browser.webkit import rfc6266 from qutebrowser.browser.webkit import rfc6266
@ -43,6 +44,8 @@ from qutebrowser.config import configtypes
def whitelist_generator(): # noqa def whitelist_generator(): # noqa
"""Generator which yields lines to add to a vulture whitelist.""" """Generator which yields lines to add to a vulture whitelist."""
loader.load_components(skip_hooks=True)
# qutebrowser commands # qutebrowser commands
for cmd in objects.commands.values(): for cmd in objects.commands.values():
yield utils.qualname(cmd.handler) yield utils.qualname(cmd.handler)
@ -127,6 +130,9 @@ def whitelist_generator(): # noqa
yield 'scripts.get_coredumpctl_traces.Line.gid' yield 'scripts.get_coredumpctl_traces.Line.gid'
yield 'scripts.importer.import_moz_places.places.row_factory' yield 'scripts.importer.import_moz_places.places.row_factory'
# component hooks
yield 'qutebrowser.components.adblock.on_config_changed'
def filter_func(item): def filter_func(item):
"""Check if a missing function should be filtered or not. """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. # We import qutebrowser.app so all @cmdutils-register decorators are run.
import qutebrowser.app import qutebrowser.app
from qutebrowser import qutebrowser, commands from qutebrowser import qutebrowser, commands
from qutebrowser.extensions import loader
from qutebrowser.commands import argparser from qutebrowser.commands import argparser
from qutebrowser.config import configdata, configtypes from qutebrowser.config import configdata, configtypes
from qutebrowser.utils import docutils, usertypes from qutebrowser.utils import docutils, usertypes
@ -549,6 +550,7 @@ def regenerate_cheatsheet():
def main(): def main():
"""Regenerate all documentation.""" """Regenerate all documentation."""
utils.change_cwd() utils.change_cwd()
loader.load_components(skip_hooks=True)
print("Generating manpage...") print("Generating manpage...")
regenerate_manpage('doc/qutebrowser.1.asciidoc') regenerate_manpage('doc/qutebrowser.1.asciidoc')
print("Generating settings help...") print("Generating settings help...")

View File

@ -27,7 +27,7 @@ import os.path
import urllib.request import urllib.request
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) 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 from qutebrowser.config import configdata

View File

@ -93,7 +93,7 @@ Feature: Downloading things from a website.
Then no crash should happen Then no crash should happen
# https://github.com/qutebrowser/qutebrowser/issues/4240 # https://github.com/qutebrowser/qutebrowser/issues/4240
@qt!=5.11.2 @qt<5.11.2
Scenario: Downloading with SSL errors (issue 1413) Scenario: Downloading with SSL errors (issue 1413)
When SSL is supported When SSL is supported
And I clear SSL errors 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/1190
# https://github.com/qutebrowser/qutebrowser/issues/2495 # 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 # https://github.com/qutebrowser/qutebrowser/issues/4230
@posix @posix @qt<5.12
Scenario: Checking visible/invisible window size Scenario: Checking visible/invisible window size
When I run :tab-only When I run :tab-only
And I open data/javascript/windowsize.html in a new background tab And I open data/javascript/windowsize.html in a new background tab
@ -134,7 +134,7 @@ Feature: Javascript stuff
And I run :tab-next And I run :tab-next
Then the window sizes should be the same Then the window sizes should be the same
@flaky @flaky @qt<5.12
Scenario: Checking visible/invisible window size with vertical tabbar Scenario: Checking visible/invisible window size with vertical tabbar
When I run :tab-only When I run :tab-only
And I set tabs.position to left And I set tabs.position to left

View File

@ -1289,6 +1289,14 @@ Feature: Tab management
And the following tabs should be open: And the following tabs should be open:
- data/numbers/1.txt (active) (pinned) - 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 Scenario: :home on a pinned tab
When I open data/numbers/1.txt When I open data/numbers/1.txt
And I run :tab-pin And I run :tab-pin
@ -1297,6 +1305,16 @@ Feature: Tab management
And the following tabs should be open: And the following tabs should be open:
- data/numbers/1.txt (active) (pinned) - 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 Scenario: Cloning a pinned tab
When I open data/numbers/1.txt When I open data/numbers/1.txt
And I run :tab-pin And I run :tab-pin

View File

@ -356,7 +356,10 @@ class QuteProc(testprocess.Process):
self._focus_ready = True self._focus_ready = True
else: else:
raise ValueError("Invalid value {!r} for 'what'.".format(what)) 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._load_ready = False
self._focus_ready = False self._focus_ready = False
self.ready.emit() self.ready.emit()

View File

@ -61,8 +61,8 @@ def normalize_line(line):
return line return line
def normalize_whole(s): def normalize_whole(s, webengine):
if qtutils.version_check('5.12', compiled=False): if qtutils.version_check('5.12', compiled=False) and webengine:
s = s.replace('\n\n-----=_qute-UUID', '\n-----=_qute-UUID') s = s.replace('\n\n-----=_qute-UUID', '\n-----=_qute-UUID')
return s return s
@ -71,8 +71,9 @@ class DownloadDir:
"""Abstraction over a download directory.""" """Abstraction over a download directory."""
def __init__(self, tmpdir): def __init__(self, tmpdir, config):
self._tmpdir = tmpdir self._tmpdir = tmpdir
self._config = config
self.location = str(tmpdir) self.location = str(tmpdir)
def read_file(self): def read_file(self):
@ -92,14 +93,15 @@ class DownloadDir:
if normalize_line(line) is not None) if normalize_line(line) is not None)
actual_data = '\n'.join(normalize_line(line) actual_data = '\n'.join(normalize_line(line)
for line in self.read_file()) 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 assert actual_data == expected_data
@pytest.fixture @pytest.fixture
def download_dir(tmpdir): def download_dir(tmpdir, pytestconfig):
return DownloadDir(tmpdir) return DownloadDir(tmpdir, pytestconfig)
def _test_mhtml_requests(test_dir, test_path, server): 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 import helpers.stubs as stubsmod
from qutebrowser.config import (config, configdata, configtypes, configexc, from qutebrowser.config import (config, configdata, configtypes, configexc,
configfiles, configcache) configfiles, configcache)
from qutebrowser.api import config as configapi
from qutebrowser.utils import objreg, standarddir, utils, usertypes from qutebrowser.utils import objreg, standarddir, utils, usertypes
from qutebrowser.browser import greasemonkey, history, qutescheme from qutebrowser.browser import greasemonkey, history, qutescheme
from qutebrowser.browser.webkit import cookies from qutebrowser.browser.webkit import cookies
@ -190,8 +191,8 @@ def testdata_scheme(qapp):
@pytest.fixture @pytest.fixture
def web_tab_setup(qtbot, tab_registry, session_manager_stub, def web_tab_setup(qtbot, tab_registry, session_manager_stub,
greasemonkey_manager, fake_args, host_blocker_stub, greasemonkey_manager, fake_args, config_stub,
config_stub, testdata_scheme): testdata_scheme):
"""Shared setup for webkit_tab/webengine_tab.""" """Shared setup for webkit_tab/webengine_tab."""
# Make sure error logging via JS fails tests # Make sure error logging via JS fails tests
config_stub.val.content.javascript.log = { config_stub.val.content.javascript.log = {
@ -306,6 +307,7 @@ def config_stub(stubs, monkeypatch, configdata_init, yaml_config_stub):
container = config.ConfigContainer(conf) container = config.ConfigContainer(conf)
monkeypatch.setattr(config, 'val', container) monkeypatch.setattr(config, 'val', container)
monkeypatch.setattr(configapi, 'val', container)
cache = configcache.ConfigCache() cache = configcache.ConfigCache()
monkeypatch.setattr(config, 'cache', cache) monkeypatch.setattr(config, 'cache', cache)
@ -328,15 +330,6 @@ def key_config_stub(config_stub, monkeypatch):
return keyconf 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 @pytest.fixture
def quickmark_manager_stub(stubs): def quickmark_manager_stub(stubs):
"""Fixture which provides a fake quickmark manager object.""" """Fixture which provides a fake quickmark manager object."""

View File

@ -459,17 +459,6 @@ class QuickmarkManagerStub(UrlMarkManagerStub):
self.delete(key) 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: class SessionManagerStub:
"""Stub for the session-manager object.""" """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) 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): def fetcher_test(test_str):
class PACHandler(http.server.BaseHTTPRequestHandler): class PACHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self): def do_GET(self):

View File

@ -247,7 +247,7 @@ class TestWebKitElement:
pytest.param(lambda e: e[None], id='getitem'), pytest.param(lambda e: e[None], id='getitem'),
pytest.param(lambda e: operator.setitem(e, None, None), id='setitem'), 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: 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(list, id='iter'),
pytest.param(len, id='len'), pytest.param(len, id='len'),
pytest.param(lambda e: e.has_frame(), id='has_frame'), pytest.param(lambda e: e.has_frame(), id='has_frame'),

View File

@ -20,7 +20,8 @@ from unittest import mock
import pytest import pytest
from PyQt5.QtCore import Qt 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 from qutebrowser.completion import completiondelegate
@ -50,3 +51,24 @@ def test_highlight(pat, txt, segments):
highlighter.setFormat.assert_has_calls([ highlighter.setFormat.assert_has_calls([
mock.call(s[0], s[1], mock.ANY) for s in segments 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 PyQt5.QtCore import QUrl
from qutebrowser.browser import adblock from qutebrowser.components import adblock
from qutebrowser.utils import urlmatch from qutebrowser.utils import urlmatch
from tests.helpers import utils 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 # TODO See ../utils/test_standarddirutils for OSError and caplog assertion
@ -58,18 +58,13 @@ URLS_TO_CHECK = ('http://localhost',
'http://veryverygoodhost.edu') 'http://veryverygoodhost.edu')
class BaseDirStub:
"""Mock for objreg.get('args') called in adblock.HostBlocker.read_hosts."""
def __init__(self):
self.basedir = None
@pytest.fixture @pytest.fixture
def basedir(fake_args): def host_blocker_factory(config_tmpdir, data_tmpdir, download_stub,
"""Register a Fake basedir.""" config_stub):
fake_args.basedir = None def factory():
return adblock.HostBlocker(config_dir=config_tmpdir,
data_dir=data_tmpdir)
return factory
def create_zipfile(directory, files, zipname='test'): def create_zipfile(directory, files, zipname='test'):
@ -133,9 +128,9 @@ def assert_urls(host_blocker, blocked=BLOCKLIST_HOSTS,
url = QUrl(str_url) url = QUrl(str_url)
host = url.host() host = url.host()
if host in blocked and host not in whitelisted: if host in blocked and host not in whitelisted:
assert host_blocker.is_blocked(url) assert host_blocker._is_blocked(url)
else: else:
assert not host_blocker.is_blocked(url) assert not host_blocker._is_blocked(url)
def blocklist_to_url(filename): def blocklist_to_url(filename):
@ -202,13 +197,13 @@ def generic_blocklists(directory):
blocklist5.toString()] blocklist5.toString()]
def test_disabled_blocking_update(basedir, config_stub, download_stub, def test_disabled_blocking_update(config_stub, tmpdir, caplog,
data_tmpdir, tmpdir, win_registry, caplog): host_blocker_factory):
"""Ensure no URL is blocked when host blocking is disabled.""" """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.lists = generic_blocklists(tmpdir)
config_stub.val.content.host_blocking.enabled = False config_stub.val.content.host_blocking.enabled = False
host_blocker = adblock.HostBlocker() host_blocker = host_blocker_factory()
host_blocker.adblock_update() host_blocker.adblock_update()
while host_blocker._in_progress: while host_blocker._in_progress:
current_download = host_blocker._in_progress[0] 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() current_download.finished.emit()
host_blocker.read_hosts() host_blocker.read_hosts()
for str_url in URLS_TO_CHECK: 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/' example_com = 'https://www.example.com/'
config_stub.val.content.host_blocking.lists = [] 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') url = QUrl('blocked.example.com')
host_blocker = adblock.HostBlocker() host_blocker = host_blocker_factory()
host_blocker._blocked_hosts.add(url.host()) host_blocker._blocked_hosts.add(url.host())
assert host_blocker.is_blocked(url) assert host_blocker._is_blocked(url)
assert not host_blocker.is_blocked(url, first_party_url=QUrl(example_com)) assert not host_blocker._is_blocked(url, first_party_url=QUrl(example_com))
def test_no_blocklist_update(config_stub, download_stub, def test_no_blocklist_update(config_stub, download_stub, host_blocker_factory):
data_tmpdir, basedir, tmpdir, win_registry):
"""Ensure no URL is blocked when no block list exists.""" """Ensure no URL is blocked when no block list exists."""
config_stub.val.content.host_blocking.lists = None config_stub.val.content.host_blocking.lists = None
config_stub.val.content.host_blocking.enabled = True config_stub.val.content.host_blocking.enabled = True
host_blocker = adblock.HostBlocker() host_blocker = host_blocker_factory()
host_blocker.adblock_update() host_blocker.adblock_update()
host_blocker.read_hosts() host_blocker.read_hosts()
for dl in download_stub.downloads: for dl in download_stub.downloads:
dl.successful = True dl.successful = True
for str_url in URLS_TO_CHECK: 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, def test_successful_update(config_stub, tmpdir, caplog, host_blocker_factory):
data_tmpdir, tmpdir, win_registry, caplog):
"""Ensure hosts from host_blocking.lists are blocked after an update.""" """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.lists = generic_blocklists(tmpdir)
config_stub.val.content.host_blocking.enabled = True config_stub.val.content.host_blocking.enabled = True
config_stub.val.content.host_blocking.whitelist = None config_stub.val.content.host_blocking.whitelist = None
host_blocker = adblock.HostBlocker() host_blocker = host_blocker_factory()
host_blocker.adblock_update() host_blocker.adblock_update()
# Simulate download is finished # Simulate download is finished
while host_blocker._in_progress: while host_blocker._in_progress:
@ -271,11 +264,9 @@ def test_successful_update(config_stub, basedir, download_stub,
assert_urls(host_blocker, whitelisted=[]) assert_urls(host_blocker, whitelisted=[])
def test_parsing_multiple_hosts_on_line(config_stub, basedir, download_stub, def test_parsing_multiple_hosts_on_line(host_blocker_factory):
data_tmpdir, tmpdir, win_registry,
caplog):
"""Ensure multiple hosts on a line get parsed correctly.""" """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') bytes_host_line = ' '.join(BLOCKLIST_HOSTS).encode('utf-8')
host_blocker._parse_line(bytes_host_line) host_blocker._parse_line(bytes_host_line)
assert_urls(host_blocker, whitelisted=[]) 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.1.1', 'myhostname'),
('127.0.0.53', 'myhostname'), ('127.0.0.53', 'myhostname'),
]) ])
def test_whitelisted_lines(config_stub, basedir, download_stub, data_tmpdir, def test_whitelisted_lines(host_blocker_factory, ip, host):
tmpdir, win_registry, caplog, ip, host):
"""Make sure we don't block hosts we don't want to.""" """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') line = ('{} {}'.format(ip, host)).encode('ascii')
host_blocker._parse_line(line) host_blocker._parse_line(line)
assert host not in host_blocker._blocked_hosts assert host not in host_blocker._blocked_hosts
def test_failed_dl_update(config_stub, basedir, download_stub, def test_failed_dl_update(config_stub, tmpdir, caplog, host_blocker_factory):
data_tmpdir, tmpdir, win_registry, caplog):
"""One blocklist fails to download. """One blocklist fails to download.
Ensure hosts from this list are not blocked. 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.enabled = True
config_stub.val.content.host_blocking.whitelist = None config_stub.val.content.host_blocking.whitelist = None
host_blocker = adblock.HostBlocker() host_blocker = host_blocker_factory()
host_blocker.adblock_update() host_blocker.adblock_update()
while host_blocker._in_progress: while host_blocker._in_progress:
current_download = host_blocker._in_progress[0] 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']) @pytest.mark.parametrize('location', ['content', 'comment'])
def test_invalid_utf8(config_stub, download_stub, tmpdir, data_tmpdir, def test_invalid_utf8(config_stub, tmpdir, caplog, host_blocker_factory,
caplog, location): location):
"""Make sure invalid UTF-8 is handled correctly. """Make sure invalid UTF-8 is handled correctly.
See https://github.com/qutebrowser/qutebrowser/issues/2301 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.enabled = True
config_stub.val.content.host_blocking.whitelist = None config_stub.val.content.host_blocking.whitelist = None
host_blocker = adblock.HostBlocker() host_blocker = host_blocker_factory()
host_blocker.adblock_update() host_blocker.adblock_update()
current_download = host_blocker._in_progress[0] 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, 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.""" """Make sure invalid UTF-8 in the compiled file is handled."""
config_stub.val.content.host_blocking.lists = [] config_stub.val.content.host_blocking.lists = []
# Make sure the HostBlocker doesn't delete blocked-hosts in __init__ # 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) lambda _self: None)
(config_tmpdir / 'blocked-hosts').write_binary( (config_tmpdir / 'blocked-hosts').write_binary(
b'https://www.example.org/\xa0') b'https://www.example.org/\xa0')
(data_tmpdir / 'blocked-hosts').ensure() (data_tmpdir / 'blocked-hosts').ensure()
host_blocker = adblock.HostBlocker() host_blocker = host_blocker_factory()
with caplog.at_level(logging.ERROR): with caplog.at_level(logging.ERROR):
host_blocker.read_hosts() host_blocker.read_hosts()
assert caplog.messages[-1] == "Failed to read host blocklist!" assert caplog.messages[-1] == "Failed to read host blocklist!"
def test_blocking_with_whitelist(config_stub, basedir, download_stub, def test_blocking_with_whitelist(config_stub, data_tmpdir, host_blocker_factory):
data_tmpdir, tmpdir):
"""Ensure hosts in content.host_blocking.whitelist are never blocked.""" """Ensure hosts in content.host_blocking.whitelist are never blocked."""
# Simulate adblock_update has already been run # Simulate adblock_update has already been run
# by creating a file named blocked-hosts, # 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.enabled = True
config_stub.val.content.host_blocking.whitelist = list(WHITELISTED_HOSTS) config_stub.val.content.host_blocking.whitelist = list(WHITELISTED_HOSTS)
host_blocker = adblock.HostBlocker() host_blocker = host_blocker_factory()
host_blocker.read_hosts() host_blocker.read_hosts()
assert_urls(host_blocker) assert_urls(host_blocker)
def test_config_change_initial(config_stub, basedir, download_stub, def test_config_change_initial(config_stub, tmpdir, host_blocker_factory):
data_tmpdir, tmpdir):
"""Test emptying host_blocking.lists with existing blocked_hosts. """Test emptying host_blocking.lists with existing blocked_hosts.
- A blocklist is present in host_blocking.lists and blocked_hosts is - 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.enabled = True
config_stub.val.content.host_blocking.whitelist = None config_stub.val.content.host_blocking.whitelist = None
host_blocker = adblock.HostBlocker() host_blocker = host_blocker_factory()
host_blocker.read_hosts() host_blocker.read_hosts()
for str_url in URLS_TO_CHECK: 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, def test_config_change(config_stub, tmpdir, host_blocker_factory):
data_tmpdir, tmpdir):
"""Ensure blocked-hosts resets if host-block-list is changed to None.""" """Ensure blocked-hosts resets if host-block-list is changed to None."""
filtered_blocked_hosts = BLOCKLIST_HOSTS[1:] # Exclude localhost filtered_blocked_hosts = BLOCKLIST_HOSTS[1:] # Exclude localhost
blocklist = blocklist_to_url(create_blocklist( 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.enabled = True
config_stub.val.content.host_blocking.whitelist = None config_stub.val.content.host_blocking.whitelist = None
host_blocker = adblock.HostBlocker() host_blocker = host_blocker_factory()
host_blocker.read_hosts() host_blocker.read_hosts()
config_stub.val.content.host_blocking.lists = None config_stub.val.content.host_blocking.lists = None
host_blocker.read_hosts() host_blocker.read_hosts()
for str_url in URLS_TO_CHECK: 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, def test_add_directory(config_stub, tmpdir, host_blocker_factory):
data_tmpdir, tmpdir):
"""Ensure adblocker can import all files in a directory.""" """Ensure adblocker can import all files in a directory."""
blocklist_hosts2 = [] blocklist_hosts2 = []
for i in BLOCKLIST_HOSTS[1:]: 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.lists = [tmpdir.strpath]
config_stub.val.content.host_blocking.enabled = True config_stub.val.content.host_blocking.enabled = True
host_blocker = adblock.HostBlocker() host_blocker = host_blocker_factory()
host_blocker.adblock_update() host_blocker.adblock_update()
assert len(host_blocker._blocked_hosts) == len(blocklist_hosts2) * 2 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') blocked_hosts = os.path.join(utils.abs_datapath(), 'blocked-hosts')
shutil.copy(blocked_hosts, str(data_tmpdir)) shutil.copy(blocked_hosts, str(data_tmpdir))
url = QUrl('https://www.example.org/') url = QUrl('https://www.example.org/')
blocker = adblock.HostBlocker() blocker = host_blocker_factory()
blocker.read_hosts() blocker.read_hosts()
assert blocker._blocked_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'] assert not config.cache['auto_save.session']
config_stub.val.auto_save.session = True config_stub.val.auto_save.session = True
assert config.cache['auto_save.session'] 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() data = autoconfig.read()
assert data['content.webrtc_ip_handling_policy']['global'] == expected assert data['content.webrtc_ip_handling_policy']['global'] == expected
@pytest.mark.parametrize('show, expected', [ @pytest.mark.parametrize('setting, old, new', [
(True, 'always'), ('tabs.favicons.show', True, 'always'),
(False, 'never'), ('tabs.favicons.show', False, 'never'),
('always', 'always'), ('tabs.favicons.show', 'always', 'always'),
('never', 'never'),
('pinned', 'pinned'), ('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): def test_bool_migrations(self, yaml, autoconfig, setting, old, new):
"""Tests for migration of tabs.favicons.show.""" """Tests for migration of former boolean settings."""
autoconfig.write({'tabs.favicons.show': {'global': show}}) autoconfig.write({setting: {'global': old}})
yaml.load() yaml.load()
yaml._save() yaml._save()
data = autoconfig.read() data = autoconfig.read()
assert data['tabs.favicons.show']['global'] == expected assert data[setting]['global'] == new
@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
def test_renamed_key_unknown_target(self, monkeypatch, yaml, def test_renamed_key_unknown_target(self, monkeypatch, yaml,
autoconfig): 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") QtWebEngineWidgets = pytest.importorskip("PyQt5.QtWebEngineWidgets")
QWebEngineProfile = QtWebEngineWidgets.QWebEngineProfile QWebEngineProfile = QtWebEngineWidgets.QWebEngineProfile
from qutebrowser.utils import javascript from qutebrowser.utils import javascript, qtutils
DEFAULT_BODY_BG = "rgba(0, 0, 0, 0)" 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) 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): def test_appendchild(stylesheet_tester):
stylesheet_tester.js.load('stylesheet/simple.html') stylesheet_tester.js.load('stylesheet/simple.html')
stylesheet_tester.init_stylesheet() stylesheet_tester.init_stylesheet()

View File

@ -34,7 +34,7 @@ from PyQt5.QtTest import QSignalSpy
import qutebrowser import qutebrowser
from qutebrowser.misc import ipc from qutebrowser.misc import ipc
from qutebrowser.utils import standarddir, utils from qutebrowser.utils import standarddir, utils, qtutils
from helpers import stubs from helpers import stubs
@ -630,6 +630,8 @@ class TestSendOrListen:
assert ret_client is None assert ret_client is None
@pytest.mark.posix(reason="Unneeded on Windows") @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): def test_correct_socket_name(self, args):
server = ipc.send_or_listen(args) server = ipc.send_or_listen(args)
expected_dir = ipc._get_socketname(args.basedir) expected_dir = ipc._get_socketname(args.basedir)

11
tox.ini
View File

@ -199,3 +199,14 @@ deps =
-r{toxinidir}/misc/requirements/requirements-mypy.txt -r{toxinidir}/misc/requirements/requirements-mypy.txt
commands = commands =
{envpython} -m mypy qutebrowser {posargs} {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/