Merge branch 'master' of https://github.com/qutebrowser/qutebrowser into donottrack
This commit is contained in:
commit
1c7178c92c
1
.flake8
1
.flake8
@ -46,6 +46,7 @@ ignore =
|
||||
min-version = 3.4.0
|
||||
max-complexity = 12
|
||||
per-file-ignores =
|
||||
/qutebrowser/api/hook.py : N801
|
||||
/tests/**/*.py : D100,D101,D401
|
||||
/tests/unit/browser/test_history.py : N806
|
||||
/tests/helpers/fixtures.py : N806
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -41,3 +41,4 @@ TODO
|
||||
/scripts/testbrowser/cpp/webengine/.qmake.stash
|
||||
/scripts/dev/pylint_checkers/qute_pylint.egg-info
|
||||
/misc/file_version_info.txt
|
||||
/doc/extapi/_build
|
||||
|
13
.travis.yml
13
.travis.yml
@ -68,16 +68,3 @@ after_success:
|
||||
|
||||
after_failure:
|
||||
- bash scripts/dev/ci/travis_backtrace.sh
|
||||
|
||||
notifications:
|
||||
webhooks:
|
||||
- https://buildtimetrend.herokuapp.com/travis
|
||||
irc:
|
||||
channels:
|
||||
- "chat.freenode.net#qutebrowser-dev"
|
||||
on_success: always
|
||||
on_failure: always
|
||||
skip_join: true
|
||||
template:
|
||||
- "%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}"
|
||||
- "%{compare_url} - %{build_url}"
|
||||
|
@ -13,7 +13,7 @@ include qutebrowser/utils/testfile
|
||||
include qutebrowser/git-commit-id
|
||||
include LICENSE doc/* README.asciidoc
|
||||
include misc/qutebrowser.desktop
|
||||
include misc/qutebrowser.appdata.xml
|
||||
include misc/org.qutebrowser.qutebrowser.appdata.xml
|
||||
include misc/Makefile
|
||||
include requirements.txt
|
||||
include tox.ini
|
||||
@ -40,5 +40,6 @@ exclude .*
|
||||
exclude misc/qutebrowser.spec
|
||||
exclude misc/qutebrowser.nsi
|
||||
exclude misc/qutebrowser.rcc
|
||||
prune doc/extapi
|
||||
|
||||
global-exclude __pycache__ *.pyc *.pyo
|
||||
|
@ -25,6 +25,7 @@ Added
|
||||
opened from a page should stack on each other or not.
|
||||
- New `completion.open_categories` setting which allows to configure which
|
||||
categories are shown in the `:open` completion, and how they are ordered.
|
||||
- New `tabs.pinned.frozen` setting to allow/deny navigating in pinned tabs.
|
||||
- New config manipulation commands:
|
||||
* `:config-dict-add` and `:config-list-add` to a new element to a dict/list
|
||||
setting.
|
||||
@ -51,6 +52,13 @@ Changed
|
||||
adblocker can be disabled on a given page.
|
||||
- Elements with a `tabindex` attribute now also get hints by default.
|
||||
- Various small performance improvements for hints and the completion.
|
||||
- The Wayland check for QtWebEngine is now disabled on Qt >= 5.11.2, as those
|
||||
versions should work without any issues.
|
||||
- The JavaScript `console` object is now available in PAC files.
|
||||
- The metainfo file `qutebrowser.appdata.xml` is now renamed to
|
||||
`org.qutebrowser.qutebrowser.appdata.xml`.
|
||||
- The `qute-pass` userscript now understands domains in gpg filenames
|
||||
in addition to directory names.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
@ -65,9 +73,12 @@ Fixed
|
||||
`content.cookies.accept = no-3rdparty` from working properly on some pages
|
||||
like GMail. However, the default for `content.cookies.accept` is still `all`
|
||||
to be in line with what other browsers do.
|
||||
- `:navigate` not incrementing in anchors or queries or anchors.
|
||||
- `:navigate` not incrementing in anchors or queries.
|
||||
- Crash when trying to use a proxy requiring authentication with QtWebKit.
|
||||
- Slashes in search terms are now percent-escaped.
|
||||
- When `scrolling.bar = True` was set in versions before v1.5.0, this now
|
||||
correctly gets migrated to `always` instead of `when-searching`.
|
||||
- Completion highlighting now works again on Qt 5.11.3 and 5.12.1.
|
||||
|
||||
v1.5.2
|
||||
------
|
||||
|
0
doc/extapi/_static/.gitkeep
Normal file
0
doc/extapi/_static/.gitkeep
Normal file
0
doc/extapi/_templates/.gitkeep
Normal file
0
doc/extapi/_templates/.gitkeep
Normal file
48
doc/extapi/api.rst
Normal file
48
doc/extapi/api.rst
Normal 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
179
doc/extapi/conf.py
Normal 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
22
doc/extapi/index.rst
Normal 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
44
doc/extapi/tab.rst
Normal 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:
|
@ -211,9 +211,10 @@ Why does J move to the next (right) tab, and K to the previous (left) one?::
|
||||
|
||||
What's the difference between insert and passthrough mode?::
|
||||
They are quite similar, but insert mode has some bindings (like `Ctrl-e` to
|
||||
open an editor) while passthrough mode only has escape bound. It might also
|
||||
be useful to rebind escape to something else in passthrough mode only, to be
|
||||
able to send an escape keypress to the website.
|
||||
open an editor) while passthrough mode only has shift+escape bound. This is
|
||||
because shift+escape is unlikely to be a useful binding to be passed to a
|
||||
webpage. However, any other keys may be assigned to leaving passthrough mode
|
||||
instead of shift+escape should this be desired.
|
||||
|
||||
Why does it take longer to open a URL in qutebrowser than in chromium?::
|
||||
When opening a URL in an existing instance, the normal qutebrowser
|
||||
|
@ -396,6 +396,7 @@ Pre-built colorschemes
|
||||
- A collection of https://github.com/chriskempson/base16[base16] color-schemes can be found in https://github.com/theova/base16-qutebrowser[base16-qutebrowser] and used with https://github.com/AuditeMarlow/base16-manager[base16-manager].
|
||||
- Two implementations of the https://github.com/arcticicestudio/nord[Nord] colorscheme for qutebrowser exist: https://github.com/Linuus/nord-qutebrowser[Linuus], https://github.com/KnownAsDon/QuteBrowser-Nord-Theme[KnownAsDon]
|
||||
- https://github.com/evannagle/qutebrowser-dracula-theme[Dracula]
|
||||
- https://github.com/jjzmajic/qutewal[Pywal theme]
|
||||
|
||||
Avoiding flake8 errors
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
@ -261,6 +261,7 @@
|
||||
|<<tabs.new_position.stacking,tabs.new_position.stacking>>|Stack related tabs on top of each other when opened consecutively.
|
||||
|<<tabs.new_position.unrelated,tabs.new_position.unrelated>>|Position of new tabs which are not opened from another tab.
|
||||
|<<tabs.padding,tabs.padding>>|Padding (in pixels) around text for tabs.
|
||||
|<<tabs.pinned.frozen,tabs.pinned.frozen>>|Force pinned tabs to stay at fixed URL.
|
||||
|<<tabs.pinned.shrink,tabs.pinned.shrink>>|Shrink pinned tabs down to their contents.
|
||||
|<<tabs.position,tabs.position>>|Position of the tab bar.
|
||||
|<<tabs.select_on_remove,tabs.select_on_remove>>|Which tab to select when the focused tab is removed.
|
||||
@ -3307,6 +3308,14 @@ Default:
|
||||
- +pass:[right]+: +pass:[5]+
|
||||
- +pass:[top]+: +pass:[0]+
|
||||
|
||||
[[tabs.pinned.frozen]]
|
||||
=== tabs.pinned.frozen
|
||||
Force pinned tabs to stay at fixed URL.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[true]+
|
||||
|
||||
[[tabs.pinned.shrink]]
|
||||
=== tabs.pinned.shrink
|
||||
Shrink pinned tabs down to their contents.
|
||||
|
@ -102,18 +102,12 @@ $ python3 scripts/asciidoc2html.py
|
||||
On Fedora
|
||||
---------
|
||||
|
||||
NOTE: Fedora's packages used to be outdated for a long time, but are
|
||||
now (November 2017) maintained and up-to-date again.
|
||||
|
||||
qutebrowser is available in the official repositories:
|
||||
|
||||
-----
|
||||
# dnf install qutebrowser
|
||||
-----
|
||||
|
||||
However, note that Fedora 25/26 won't be updated to qutebrowser v1.0, so you
|
||||
might want to <<tox,install qutebrowser via tox>> instead there.
|
||||
|
||||
Additional hints
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -17,8 +17,8 @@ doc/qutebrowser.1.html:
|
||||
|
||||
install: doc/qutebrowser.1.html
|
||||
$(PYTHON) setup.py install --prefix="$(PREFIX)" --optimize=1 $(SETUPTOOLSOPTS)
|
||||
install -Dm644 misc/qutebrowser.appdata.xml \
|
||||
"$(DESTDIR)$(DATADIR)/metainfo/qutebrowser.appdata.xml"
|
||||
install -Dm644 misc/org.qutebrowser.qutebrowser.appdata.xml \
|
||||
"$(DESTDIR)$(DATADIR)/metainfo/org.qutebrowser.qutebrowser.appdata.xml"
|
||||
install -Dm644 doc/qutebrowser.1 \
|
||||
"$(DESTDIR)$(MANDIR)/man1/qutebrowser.1"
|
||||
install -Dm644 misc/qutebrowser.desktop \
|
||||
|
@ -6,6 +6,8 @@ import os
|
||||
sys.path.insert(0, os.getcwd())
|
||||
from scripts import setupcommon
|
||||
|
||||
from qutebrowser.extensions import loader
|
||||
|
||||
block_cipher = None
|
||||
|
||||
|
||||
@ -27,6 +29,13 @@ def get_data_files():
|
||||
return data_files
|
||||
|
||||
|
||||
def get_hidden_imports():
|
||||
imports = ['PyQt5.QtOpenGL', 'PyQt5._QOpenGLFunctions_2_0']
|
||||
for info in loader.walk_components():
|
||||
imports.append(info.name)
|
||||
return imports
|
||||
|
||||
|
||||
setupcommon.write_git_file()
|
||||
|
||||
|
||||
@ -42,7 +51,7 @@ a = Analysis(['../qutebrowser/__main__.py'],
|
||||
pathex=['misc'],
|
||||
binaries=None,
|
||||
datas=get_data_files(),
|
||||
hiddenimports=['PyQt5.QtOpenGL', 'PyQt5._QOpenGLFunctions_2_0'],
|
||||
hiddenimports=get_hidden_imports(),
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
excludes=['tkinter'],
|
||||
|
@ -4,6 +4,6 @@ certifi==2018.11.29
|
||||
chardet==3.0.4
|
||||
codecov==2.0.15
|
||||
coverage==4.5.2
|
||||
idna==2.7
|
||||
requests==2.20.1
|
||||
idna==2.8
|
||||
requests==2.21.0
|
||||
urllib3==1.24.1
|
||||
|
@ -11,7 +11,7 @@ flake8-deprecated==1.3
|
||||
flake8-docstrings==1.3.0
|
||||
flake8-future-import==0.4.5
|
||||
flake8-mock==0.3
|
||||
flake8-per-file-ignores==0.6
|
||||
flake8-per-file-ignores==0.7
|
||||
flake8-polyfill==1.0.2
|
||||
flake8-string-format==0.2.3
|
||||
flake8-tidy-imports==1.1.0
|
||||
@ -22,6 +22,6 @@ pep8-naming==0.7.0
|
||||
pycodestyle==2.4.0
|
||||
pydocstyle==3.0.0
|
||||
pyflakes==2.0.0
|
||||
six==1.11.0
|
||||
six==1.12.0
|
||||
snowballstemmer==1.2.1
|
||||
typing==3.6.6
|
||||
|
@ -1,8 +1,8 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
mypy==0.641
|
||||
mypy==0.650
|
||||
mypy-extensions==0.4.1
|
||||
PyQt5==5.11.3
|
||||
PyQt5-sip==4.19.13
|
||||
-e git+https://github.com/qutebrowser/PyQt5-stubs.git@wip#egg=PyQt5_stubs
|
||||
typed-ast==1.1.0
|
||||
typed-ast==1.1.1
|
||||
|
@ -4,4 +4,4 @@ colorama==0.4.1
|
||||
cssutils==1.0.2
|
||||
hunter==2.1.0
|
||||
Pympler==0.6
|
||||
six==1.11.0
|
||||
six==1.12.0
|
||||
|
@ -3,6 +3,6 @@
|
||||
appdirs==1.4.3
|
||||
packaging==18.0
|
||||
pyparsing==2.3.0
|
||||
setuptools==40.6.2
|
||||
six==1.11.0
|
||||
setuptools==40.6.3
|
||||
six==1.12.0
|
||||
wheel==0.32.3
|
||||
|
@ -7,7 +7,7 @@ cffi==1.11.5
|
||||
chardet==3.0.4
|
||||
cryptography==2.4.2
|
||||
github3.py==1.2.0
|
||||
idna==2.7
|
||||
idna==2.8
|
||||
isort==4.3.4
|
||||
jwcrypto==0.6.0
|
||||
lazy-object-proxy==1.3.1
|
||||
@ -16,8 +16,8 @@ pycparser==2.19
|
||||
pylint==2.2.2
|
||||
python-dateutil==2.7.5
|
||||
./scripts/dev/pylint_checkers
|
||||
requests==2.20.1
|
||||
six==1.11.0
|
||||
requests==2.21.0
|
||||
six==1.12.0
|
||||
uritemplate==3.0.0
|
||||
urllib3==1.24.1
|
||||
wrapt==1.10.11
|
||||
|
21
misc/requirements/requirements-sphinx.txt
Normal file
21
misc/requirements/requirements-sphinx.txt
Normal 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
|
1
misc/requirements/requirements-sphinx.txt-raw
Normal file
1
misc/requirements/requirements-sphinx.txt-raw
Normal file
@ -0,0 +1 @@
|
||||
sphinx
|
@ -3,39 +3,39 @@
|
||||
atomicwrites==1.2.1
|
||||
attrs==18.2.0
|
||||
backports.functools-lru-cache==1.5
|
||||
beautifulsoup4==4.6.3
|
||||
cheroot==6.5.2
|
||||
beautifulsoup4==4.7.0
|
||||
cheroot==6.5.3
|
||||
Click==7.0
|
||||
# colorama==0.4.1
|
||||
coverage==4.5.2
|
||||
EasyProcess==0.2.3
|
||||
EasyProcess==0.2.5
|
||||
Flask==1.0.2
|
||||
glob2==0.6
|
||||
hunter==2.1.0
|
||||
hypothesis==3.82.1
|
||||
hypothesis==3.85.2
|
||||
itsdangerous==1.1.0
|
||||
# Jinja2==2.10
|
||||
Mako==1.0.7
|
||||
# MarkupSafe==1.1.0
|
||||
more-itertools==4.3.0
|
||||
more-itertools==5.0.0
|
||||
parse==1.9.0
|
||||
parse-type==0.4.2
|
||||
pluggy==0.8.0
|
||||
py==1.7.0
|
||||
py-cpuinfo==4.0.0
|
||||
pytest==4.0.1
|
||||
pytest-bdd==3.0.0
|
||||
pytest==4.0.2
|
||||
pytest-bdd==3.0.1
|
||||
pytest-benchmark==3.1.1
|
||||
pytest-cov==2.6.0
|
||||
pytest-faulthandler==1.5.0
|
||||
pytest-instafail==0.4.0
|
||||
pytest-mock==1.10.0
|
||||
pytest-qt==3.2.1
|
||||
pytest-qt==3.2.2
|
||||
pytest-repeat==0.7.0
|
||||
pytest-rerunfailures==5.0
|
||||
pytest-travis-fold==1.3.0
|
||||
pytest-xvfb==1.1.0
|
||||
PyVirtualDisplay==0.2.1
|
||||
six==1.11.0
|
||||
six==1.12.0
|
||||
vulture==1.0
|
||||
Werkzeug==0.14.1
|
||||
|
@ -3,7 +3,7 @@
|
||||
filelock==3.0.10
|
||||
pluggy==0.8.0
|
||||
py==1.7.0
|
||||
six==1.11.0
|
||||
six==1.12.0
|
||||
toml==0.10.0
|
||||
tox==3.5.3
|
||||
tox==3.6.1
|
||||
virtualenv==16.1.0
|
||||
|
@ -64,6 +64,7 @@ die() {
|
||||
javascript_escape() {
|
||||
# print the first argument in an escaped way, such that it can safely
|
||||
# be used within javascripts double quotes
|
||||
# shellcheck disable=SC2001
|
||||
sed "s,[\\\\'\"],\\\\&,g" <<< "$1"
|
||||
}
|
||||
|
||||
@ -111,6 +112,7 @@ simplify_url() {
|
||||
# are found:
|
||||
no_entries_found() {
|
||||
while [ 0 -eq "${#files[@]}" ] && [ -n "$simple_url" ]; do
|
||||
# shellcheck disable=SC2001
|
||||
shorter_simple_url=$(sed 's,^[^.]*\.,,' <<< "$simple_url")
|
||||
if [ "$shorter_simple_url" = "$simple_url" ] ; then
|
||||
# if no dot, then even remove the top level domain
|
||||
|
@ -97,13 +97,19 @@ def qute_command(command):
|
||||
def find_pass_candidates(domain, password_store_path):
|
||||
candidates = []
|
||||
for path, directories, file_names in os.walk(password_store_path, followlinks=True):
|
||||
if directories or domain not in path.split(os.path.sep):
|
||||
secrets = fnmatch.filter(file_names, '*.gpg')
|
||||
if not secrets:
|
||||
continue
|
||||
|
||||
# Strip password store path prefix to get the relative pass path
|
||||
pass_path = path[len(password_store_path) + 1:]
|
||||
secrets = fnmatch.filter(file_names, '*.gpg')
|
||||
candidates.extend(os.path.join(pass_path, os.path.splitext(secret)[0]) for secret in secrets)
|
||||
split_path = pass_path.split(os.path.sep)
|
||||
for secret in secrets:
|
||||
secret_base = os.path.splitext(secret)[0]
|
||||
if domain not in (split_path + [secret_base]):
|
||||
continue
|
||||
|
||||
candidates.append(os.path.join(pass_path, secret_base))
|
||||
return candidates
|
||||
|
||||
|
||||
|
@ -37,7 +37,7 @@ get_selection() {
|
||||
# https://github.com/halfwit/dotfiles/blob/master/.config/dmenu/font
|
||||
[[ -s $confdir/dmenu/font ]] && read -r font < "$confdir"/dmenu/font
|
||||
|
||||
[[ $font ]] && opts+=(-fn "$font")
|
||||
[[ -n $font ]] && opts+=(-fn "$font")
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
[[ -s $optsfile ]] && source "$optsfile"
|
||||
@ -46,7 +46,7 @@ url=$(get_selection)
|
||||
url=${url/*http/http}
|
||||
|
||||
# If no selection is made, exit (escape pressed, e.g.)
|
||||
[[ ! $url ]] && exit 0
|
||||
[[ -z $url ]] && exit 0
|
||||
|
||||
case $1 in
|
||||
open) printf '%s' "open $url" >> "$QUTE_FIFO" || qutebrowser "$url" ;;
|
||||
|
20
mypy.ini
20
mypy.ini
@ -18,10 +18,6 @@ disallow_untyped_decorators = True
|
||||
# no_implicit_optional = True
|
||||
# warn_return_any = True
|
||||
|
||||
[mypy-faulthandler]
|
||||
# https://github.com/python/typeshed/pull/2627
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-colorama]
|
||||
# https://github.com/tartley/colorama/issues/206
|
||||
ignore_missing_imports = True
|
||||
@ -73,3 +69,19 @@ disallow_incomplete_defs = True
|
||||
[mypy-qutebrowser.components.*]
|
||||
disallow_untyped_defs = True
|
||||
disallow_incomplete_defs = True
|
||||
|
||||
[mypy-qutebrowser.extensions.*]
|
||||
disallow_untyped_defs = True
|
||||
disallow_incomplete_defs = True
|
||||
|
||||
[mypy-qutebrowser.browser.webelem]
|
||||
disallow_untyped_defs = True
|
||||
disallow_incomplete_defs = True
|
||||
|
||||
[mypy-qutebrowser.browser.webkit.webkitelem]
|
||||
disallow_untyped_defs = True
|
||||
disallow_incomplete_defs = True
|
||||
|
||||
[mypy-qutebrowser.browser.webengine.webengineelem]
|
||||
disallow_untyped_defs = True
|
||||
disallow_incomplete_defs = True
|
||||
|
@ -64,6 +64,7 @@ qt_log_ignore =
|
||||
^QSettings::value: Empty key passed
|
||||
^Icon theme ".*" not found
|
||||
^Error receiving trust for a CA certificate
|
||||
^QBackingStore::endPaint\(\) called with active painter on backingstore paint device
|
||||
xfail_strict = true
|
||||
filterwarnings =
|
||||
error
|
||||
|
@ -24,3 +24,4 @@ from qutebrowser.browser.browsertab import WebTabError, AbstractTab as Tab
|
||||
from qutebrowser.browser.webelem import (Error as WebElemError,
|
||||
AbstractWebElement as WebElement)
|
||||
from qutebrowser.utils.usertypes import ClickTarget, JsWorld
|
||||
from qutebrowser.extensions.loader import InitContext
|
||||
|
@ -17,7 +17,37 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Utilities for command handlers."""
|
||||
"""qutebrowser has the concept of functions, exposed to the user as commands.
|
||||
|
||||
Creating a new command is straightforward::
|
||||
|
||||
from qutebrowser.api import cmdutils
|
||||
|
||||
@cmdutils.register(...)
|
||||
def foo():
|
||||
...
|
||||
|
||||
The commands arguments are automatically deduced by inspecting your function.
|
||||
|
||||
The types of the function arguments are inferred based on their default values,
|
||||
e.g., an argument `foo=True` will be converted to a flag `-f`/`--foo` in
|
||||
qutebrowser's commandline.
|
||||
|
||||
The type can be overridden using Python's function annotations::
|
||||
|
||||
@cmdutils.register(...)
|
||||
def foo(bar: int, baz=True):
|
||||
...
|
||||
|
||||
Possible values:
|
||||
|
||||
- A callable (``int``, ``float``, etc.): Gets called to validate/convert the
|
||||
value.
|
||||
- A python enum type: All members of the enum are possible values.
|
||||
- A ``typing.Union`` of multiple types above: Any of these types are valid
|
||||
values, e.g., ``typing.Union[str, int]``.
|
||||
"""
|
||||
|
||||
|
||||
import inspect
|
||||
import typing
|
||||
@ -33,14 +63,16 @@ class CommandError(cmdexc.Error):
|
||||
"""Raised when a command encounters an error while running.
|
||||
|
||||
If your command handler encounters an error and cannot continue, raise this
|
||||
exception with an appropriate error message:
|
||||
exception with an appropriate error message::
|
||||
|
||||
raise cmdexc.CommandError("Message")
|
||||
|
||||
The message will then be shown in the qutebrowser status bar.
|
||||
|
||||
Note that you should only raise this exception while a command handler is
|
||||
run. Raising it at another point causes qutebrowser to crash due to an
|
||||
.. note::
|
||||
|
||||
You should only raise this exception while a command handler is run.
|
||||
Raising it at another point causes qutebrowser to crash due to an
|
||||
unhandled exception.
|
||||
"""
|
||||
|
||||
@ -76,13 +108,7 @@ def check_exclusive(flags: typing.Iterable[bool],
|
||||
|
||||
class register: # noqa: N801,N806 pylint: disable=invalid-name
|
||||
|
||||
"""Decorator to register a new command handler.
|
||||
|
||||
Attributes:
|
||||
_instance: The object from the object registry to be used as "self".
|
||||
_name: The name (as string) or names (as list) of the command.
|
||||
_kwargs: The arguments to pass to Command.
|
||||
"""
|
||||
"""Decorator to register a new command handler."""
|
||||
|
||||
def __init__(self, *,
|
||||
instance: str = None,
|
||||
@ -95,8 +121,11 @@ class register: # noqa: N801,N806 pylint: disable=invalid-name
|
||||
Args:
|
||||
See class attributes.
|
||||
"""
|
||||
# The object from the object registry to be used as "self".
|
||||
self._instance = instance
|
||||
# The name (as string) or names (as list) of the command.
|
||||
self._name = name
|
||||
# The arguments to pass to Command.
|
||||
self._kwargs = kwargs
|
||||
|
||||
def __call__(self, func: typing.Callable) -> typing.Callable:
|
||||
@ -127,16 +156,50 @@ class register: # noqa: N801,N806 pylint: disable=invalid-name
|
||||
|
||||
class argument: # noqa: N801,N806 pylint: disable=invalid-name
|
||||
|
||||
"""Decorator to customize an argument for @cmdutils.register.
|
||||
"""Decorator to customize an argument.
|
||||
|
||||
Attributes:
|
||||
_argname: The name of the argument to handle.
|
||||
_kwargs: Keyword arguments, valid ArgInfo members
|
||||
You can customize how an argument is handled using the
|
||||
``@cmdutils.argument`` decorator *after* ``@cmdutils.register``. This can,
|
||||
for example, be used to customize the flag an argument should get::
|
||||
|
||||
@cmdutils.register(...)
|
||||
@cmdutils.argument('bar', flag='c')
|
||||
def foo(bar):
|
||||
...
|
||||
|
||||
For a ``str`` argument, you can restrict the allowed strings using
|
||||
``choices``::
|
||||
|
||||
@cmdutils.register(...)
|
||||
@cmdutils.argument('bar', choices=['val1', 'val2'])
|
||||
def foo(bar: str):
|
||||
...
|
||||
|
||||
For ``typing.Union`` types, the given ``choices`` are only checked if other
|
||||
types (like ``int``) don't match.
|
||||
|
||||
The following arguments are supported for ``@cmdutils.argument``:
|
||||
|
||||
- ``flag``: Customize the short flag (``-x``) the argument will get.
|
||||
- ``value``: Tell qutebrowser to fill the argument with special values:
|
||||
|
||||
* ``value=cmdutils.Value.count``: The ``count`` given by the user to the
|
||||
command.
|
||||
* ``value=cmdutils.Value.win_id``: The window ID of the current window.
|
||||
* ``value=cmdutils.Value.cur_tab``: The tab object which is currently
|
||||
focused.
|
||||
|
||||
- ``completion``: A completion function to use when completing arguments
|
||||
for the given command.
|
||||
- ``choices``: The allowed string choices for the argument.
|
||||
|
||||
The name of an argument will always be the parameter name, with any
|
||||
trailing underscores stripped and underscores replaced by dashes.
|
||||
"""
|
||||
|
||||
def __init__(self, argname: str, **kwargs: typing.Any) -> None:
|
||||
self._argname = argname
|
||||
self._kwargs = kwargs
|
||||
self._argname = argname # The name of the argument to handle.
|
||||
self._kwargs = kwargs # Valid ArgInfo members.
|
||||
|
||||
def __call__(self, func: typing.Callable) -> typing.Callable:
|
||||
funcname = func.__name__
|
||||
|
@ -21,9 +21,23 @@
|
||||
|
||||
import typing
|
||||
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
# pylint: disable=unused-import,useless-suppression
|
||||
from qutebrowser.config import config
|
||||
from PyQt5.QtCore import QUrl
|
||||
|
||||
from qutebrowser.config import config
|
||||
|
||||
#: Simplified access to config values using attribute acccess.
|
||||
#: For example, to access the ``content.javascript.enabled`` setting,
|
||||
#: you can do::
|
||||
#:
|
||||
#: if config.val.content.javascript.enabled:
|
||||
#: ...
|
||||
#:
|
||||
#: This also supports setting configuration values::
|
||||
#:
|
||||
#: config.val.content.javascript.enabled = False
|
||||
val = typing.cast('config.ConfigContainer', None)
|
||||
|
||||
|
||||
def get(name: str, url: QUrl = None) -> typing.Any:
|
||||
"""Get a value from the config based on a string name."""
|
||||
return config.instance.get(name, url)
|
||||
|
75
qutebrowser/api/downloads.py
Normal file
75
qutebrowser/api/downloads.py
Normal 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
92
qutebrowser/api/hook.py
Normal 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
|
43
qutebrowser/api/interceptor.py
Normal file
43
qutebrowser/api/interceptor.py
Normal 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)
|
@ -63,11 +63,12 @@ from qutebrowser.completion.models import miscmodels
|
||||
from qutebrowser.commands import runners
|
||||
from qutebrowser.api import cmdutils
|
||||
from qutebrowser.config import config, websettings, configfiles, configinit
|
||||
from qutebrowser.browser import (urlmarks, adblock, history, browsertab,
|
||||
from qutebrowser.browser import (urlmarks, history, browsertab,
|
||||
qtnetworkdownloads, downloads, greasemonkey)
|
||||
from qutebrowser.browser.network import proxy
|
||||
from qutebrowser.browser.webkit import cookies, cache
|
||||
from qutebrowser.browser.webkit.network import networkmanager
|
||||
from qutebrowser.extensions import loader
|
||||
from qutebrowser.keyinput import macros
|
||||
from qutebrowser.mainwindow import mainwindow, prompt
|
||||
from qutebrowser.misc import (readline, ipc, savemanager, sessions,
|
||||
@ -77,8 +78,6 @@ from qutebrowser.utils import (log, version, message, utils, urlutils, objreg,
|
||||
usertypes, standarddir, error, qtutils)
|
||||
# pylint: disable=unused-import
|
||||
# We import those to run the cmdutils.register decorators.
|
||||
from qutebrowser.components import (scrollcommands, caretcommands,
|
||||
zoomcommands, misccommands)
|
||||
from qutebrowser.mainwindow.statusbar import command
|
||||
from qutebrowser.misc import utilcmds
|
||||
# pylint: enable=unused-import
|
||||
@ -166,6 +165,8 @@ def init(args, crash_handler):
|
||||
qApp.setQuitOnLastWindowClosed(False)
|
||||
_init_icon()
|
||||
|
||||
loader.init()
|
||||
loader.load_components()
|
||||
try:
|
||||
_init_modules(args, crash_handler)
|
||||
except (OSError, UnicodeDecodeError, browsertab.WebTabError) as e:
|
||||
@ -468,11 +469,6 @@ def _init_modules(args, crash_handler):
|
||||
log.init.debug("Initializing websettings...")
|
||||
websettings.init(args)
|
||||
|
||||
log.init.debug("Initializing adblock...")
|
||||
host_blocker = adblock.HostBlocker()
|
||||
host_blocker.read_hosts()
|
||||
objreg.register('host-blocker', host_blocker)
|
||||
|
||||
log.init.debug("Initializing quickmarks...")
|
||||
quickmark_manager = urlmarks.QuickmarkManager(qApp)
|
||||
objreg.register('quickmark-manager', quickmark_manager)
|
||||
|
@ -141,14 +141,11 @@ class TabData:
|
||||
|
||||
class AbstractAction:
|
||||
|
||||
"""Attribute of AbstractTab for Qt WebActions.
|
||||
|
||||
Class attributes (overridden by subclasses):
|
||||
action_class: The class actions are defined on (QWeb{Engine,}Page)
|
||||
action_base: The type of the actions (QWeb{Engine,}Page.WebAction)
|
||||
"""
|
||||
"""Attribute ``action`` of AbstractTab for Qt WebActions."""
|
||||
|
||||
# The class actions are defined on (QWeb{Engine,}Page)
|
||||
action_class = None # type: type
|
||||
# The type of the actions (QWeb{Engine,}Page.WebAction)
|
||||
action_base = None # type: type
|
||||
|
||||
def __init__(self, tab: 'AbstractTab') -> None:
|
||||
@ -200,7 +197,7 @@ class AbstractAction:
|
||||
|
||||
class AbstractPrinting:
|
||||
|
||||
"""Attribute of AbstractTab for printing the page."""
|
||||
"""Attribute ``printing`` of AbstractTab for printing the page."""
|
||||
|
||||
def __init__(self, tab: 'AbstractTab') -> None:
|
||||
self._widget = None
|
||||
@ -271,7 +268,7 @@ class AbstractPrinting:
|
||||
|
||||
class AbstractSearch(QObject):
|
||||
|
||||
"""Attribute of AbstractTab for doing searches.
|
||||
"""Attribute ``search`` of AbstractTab for doing searches.
|
||||
|
||||
Attributes:
|
||||
text: The last thing this view was searched for.
|
||||
@ -279,15 +276,14 @@ class AbstractSearch(QObject):
|
||||
this view.
|
||||
_flags: The flags of the last search (needs to be set by subclasses).
|
||||
_widget: The underlying WebView widget.
|
||||
|
||||
Signals:
|
||||
finished: Emitted when a search was finished.
|
||||
arg: True if the text was found, False otherwise.
|
||||
cleared: Emitted when an existing search was cleared.
|
||||
"""
|
||||
|
||||
#: Signal emitted when a search was finished
|
||||
#: (True if the text was found, False otherwise)
|
||||
finished = pyqtSignal(bool)
|
||||
#: Signal emitted when an existing search was cleared.
|
||||
cleared = pyqtSignal()
|
||||
|
||||
_Callback = typing.Callable[[bool], None]
|
||||
|
||||
def __init__(self, tab: 'AbstractTab', parent: QWidget = None):
|
||||
@ -350,17 +346,13 @@ class AbstractSearch(QObject):
|
||||
|
||||
class AbstractZoom(QObject):
|
||||
|
||||
"""Attribute of AbstractTab for controlling zoom.
|
||||
|
||||
Attributes:
|
||||
_neighborlist: A NeighborList with the zoom levels.
|
||||
_default_zoom_changed: Whether the zoom was changed from the default.
|
||||
"""
|
||||
"""Attribute ``zoom`` of AbstractTab for controlling zoom."""
|
||||
|
||||
def __init__(self, tab: 'AbstractTab', parent: QWidget = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._tab = tab
|
||||
self._widget = None
|
||||
# Whether zoom was changed from the default.
|
||||
self._default_zoom_changed = False
|
||||
self._init_neighborlist()
|
||||
config.instance.changed.connect(self._on_config_changed)
|
||||
@ -375,7 +367,9 @@ class AbstractZoom(QObject):
|
||||
self._init_neighborlist()
|
||||
|
||||
def _init_neighborlist(self) -> None:
|
||||
"""Initialize self._neighborlist."""
|
||||
"""Initialize self._neighborlist.
|
||||
|
||||
It is a NeighborList with the zoom levels."""
|
||||
levels = config.val.zoom.levels
|
||||
self._neighborlist = usertypes.NeighborList(
|
||||
levels, mode=usertypes.NeighborList.Modes.edge)
|
||||
@ -427,15 +421,12 @@ class AbstractZoom(QObject):
|
||||
|
||||
class AbstractCaret(QObject):
|
||||
|
||||
"""Attribute of AbstractTab for caret browsing.
|
||||
|
||||
Signals:
|
||||
selection_toggled: Emitted when the selection was toggled.
|
||||
arg: Whether the selection is now active.
|
||||
follow_selected_done: Emitted when a follow_selection action is done.
|
||||
"""
|
||||
"""Attribute ``caret`` of AbstractTab for caret browsing."""
|
||||
|
||||
#: Signal emitted when the selection was toggled.
|
||||
#: (argument - whether the selection is now active)
|
||||
selection_toggled = pyqtSignal(bool)
|
||||
#: Emitted when a ``follow_selection`` action is done.
|
||||
follow_selected_done = pyqtSignal()
|
||||
|
||||
def __init__(self,
|
||||
@ -522,16 +513,12 @@ class AbstractCaret(QObject):
|
||||
|
||||
class AbstractScroller(QObject):
|
||||
|
||||
"""Attribute of AbstractTab to manage scroll position.
|
||||
|
||||
Signals:
|
||||
perc_changed: The scroll position changed.
|
||||
before_jump_requested:
|
||||
Emitted by other code when the user requested a jump.
|
||||
Used to set the special ' mark so the user can return.
|
||||
"""
|
||||
"""Attribute ``scroller`` of AbstractTab to manage scroll position."""
|
||||
|
||||
#: Signal emitted when the scroll position changed (int, int)
|
||||
perc_changed = pyqtSignal(int, int)
|
||||
#: Signal emitted before the user requested a jump.
|
||||
#: Used to set the special ' mark so the user can return.
|
||||
before_jump_requested = pyqtSignal()
|
||||
|
||||
def __init__(self, tab: 'AbstractTab', parent: QWidget = None):
|
||||
@ -833,42 +820,46 @@ class AbstractTabPrivate:
|
||||
|
||||
class AbstractTab(QWidget):
|
||||
|
||||
"""An adapter for QWebView/QWebEngineView representing a single tab.
|
||||
|
||||
Signals:
|
||||
See related Qt signals.
|
||||
|
||||
new_tab_requested: Emitted when a new tab should be opened with the
|
||||
given URL.
|
||||
load_status_changed: The loading status changed
|
||||
fullscreen_requested: Fullscreen display was requested by the page.
|
||||
arg: True if fullscreen should be turned on,
|
||||
False if it should be turned off.
|
||||
renderer_process_terminated: Emitted when the underlying renderer
|
||||
process terminated.
|
||||
arg 0: A TerminationStatus member.
|
||||
arg 1: The exit code.
|
||||
before_load_started: Emitted before we tell Qt to open a URL.
|
||||
"""
|
||||
"""An adapter for QWebView/QWebEngineView representing a single tab."""
|
||||
|
||||
#: Signal emitted when a website requests to close this tab.
|
||||
window_close_requested = pyqtSignal()
|
||||
#: Signal emitted when a link is hovered (the hover text)
|
||||
link_hovered = pyqtSignal(str)
|
||||
#: Signal emitted when a page started loading
|
||||
load_started = pyqtSignal()
|
||||
#: Signal emitted when a page is loading (progress percentage)
|
||||
load_progress = pyqtSignal(int)
|
||||
#: Signal emitted when a page finished loading (success as bool)
|
||||
load_finished = pyqtSignal(bool)
|
||||
#: Signal emitted when a page's favicon changed (icon as QIcon)
|
||||
icon_changed = pyqtSignal(QIcon)
|
||||
#: Signal emitted when a page's title changed (new title as str)
|
||||
title_changed = pyqtSignal(str)
|
||||
load_status_changed = pyqtSignal(usertypes.LoadStatus)
|
||||
#: Signal emitted when a new tab should be opened (url as QUrl)
|
||||
new_tab_requested = pyqtSignal(QUrl)
|
||||
#: Signal emitted when a page's URL changed (url as QUrl)
|
||||
url_changed = pyqtSignal(QUrl)
|
||||
shutting_down = pyqtSignal()
|
||||
#: Signal emitted when a tab's content size changed
|
||||
#: (new size as QSizeF)
|
||||
contents_size_changed = pyqtSignal(QSizeF)
|
||||
# url, requested url, title
|
||||
history_item_triggered = pyqtSignal(QUrl, QUrl, str)
|
||||
#: Signal emitted when a page requested full-screen (bool)
|
||||
fullscreen_requested = pyqtSignal(bool)
|
||||
renderer_process_terminated = pyqtSignal(TerminationStatus, int)
|
||||
#: Signal emitted before load starts (URL as QUrl)
|
||||
before_load_started = pyqtSignal(QUrl)
|
||||
|
||||
# Signal emitted when a page's load status changed
|
||||
# (argument: usertypes.LoadStatus)
|
||||
load_status_changed = pyqtSignal(usertypes.LoadStatus)
|
||||
# Signal emitted before shutting down
|
||||
shutting_down = pyqtSignal()
|
||||
# Signal emitted when a history item should be added
|
||||
history_item_triggered = pyqtSignal(QUrl, QUrl, str)
|
||||
# Signal emitted when the underlying renderer process terminated.
|
||||
# arg 0: A TerminationStatus member.
|
||||
# arg 1: The exit code.
|
||||
renderer_process_terminated = pyqtSignal(TerminationStatus, int)
|
||||
|
||||
def __init__(self, *, win_id: int, private: bool,
|
||||
parent: QWidget = None) -> None:
|
||||
self.is_private = private
|
||||
@ -952,6 +943,10 @@ class AbstractTab(QWidget):
|
||||
evt.posted = True
|
||||
QApplication.postEvent(recipient, evt)
|
||||
|
||||
def navigation_blocked(self) -> bool:
|
||||
"""Test if navigation is allowed on the current tab."""
|
||||
return self.data.pinned and config.val.tabs.pinned.frozen
|
||||
|
||||
@pyqtSlot(QUrl)
|
||||
def _on_before_load_started(self, url: QUrl) -> None:
|
||||
"""Adjust the title if we are going to visit a URL soon."""
|
||||
|
@ -34,7 +34,7 @@ from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate,
|
||||
webelem, downloads)
|
||||
from qutebrowser.keyinput import modeman, keyutils
|
||||
from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
|
||||
objreg, utils, standarddir)
|
||||
objreg, utils, standarddir, debug)
|
||||
from qutebrowser.utils.usertypes import KeyMode
|
||||
from qutebrowser.misc import editor, guiprocess, objects
|
||||
from qutebrowser.completion.models import urlmodel, miscmodels
|
||||
@ -316,7 +316,7 @@ class CommandDispatcher:
|
||||
else:
|
||||
# Explicit count with a tab that doesn't exist.
|
||||
return
|
||||
elif curtab.data.pinned:
|
||||
elif curtab.navigation_blocked():
|
||||
message.info("Tab is pinned!")
|
||||
else:
|
||||
curtab.load_url(cur_url)
|
||||
@ -1721,4 +1721,10 @@ class CommandDispatcher:
|
||||
return
|
||||
|
||||
window = self._tabbed_browser.widget.window()
|
||||
|
||||
if not window.isFullScreen():
|
||||
window.state_before_fullscreen = window.windowState()
|
||||
window.setWindowState(window.windowState() ^ Qt.WindowFullScreen)
|
||||
|
||||
log.misc.debug('state before fullscreen: {}'.format(
|
||||
debug.qflags_key(Qt, window.state_before_fullscreen)))
|
||||
|
@ -75,6 +75,7 @@ class DownloadView(QListView):
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
if not utils.is_mac:
|
||||
self.setStyle(QStyleFactory.create('Fusion'))
|
||||
config.set_register_stylesheet(self)
|
||||
self.setResizeMode(QListView.Adjust)
|
||||
|
@ -180,6 +180,8 @@ class PACResolver:
|
||||
"""
|
||||
self._engine = QJSEngine()
|
||||
|
||||
self._engine.installExtensions(QJSEngine.ConsoleExtension)
|
||||
|
||||
self._ctx = _PACContext(self._engine)
|
||||
self._engine.globalObject().setProperty(
|
||||
"PAC", self._engine.newQObject(self._ctx))
|
||||
|
@ -19,15 +19,23 @@
|
||||
|
||||
"""Generic web element related code."""
|
||||
|
||||
import typing
|
||||
import collections.abc
|
||||
|
||||
from PyQt5.QtCore import QUrl, Qt, QEvent, QTimer
|
||||
from PyQt5.QtCore import QUrl, Qt, QEvent, QTimer, QRect, QPoint
|
||||
from PyQt5.QtGui import QMouseEvent
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.mainwindow import mainwindow
|
||||
from qutebrowser.utils import log, usertypes, utils, qtutils, objreg
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
# pylint: disable=unused-import,useless-suppression
|
||||
from qutebrowser.browser import browsertab
|
||||
|
||||
|
||||
JsValueType = typing.Union[int, float, str, None]
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
@ -40,7 +48,7 @@ class OrphanedError(Error):
|
||||
"""Raised when a webelement's parent has vanished."""
|
||||
|
||||
|
||||
def css_selector(group, url):
|
||||
def css_selector(group: str, url: QUrl) -> str:
|
||||
"""Get a CSS selector for the given group/URL."""
|
||||
selectors = config.instance.get('hints.selectors', url)
|
||||
if group not in selectors:
|
||||
@ -54,76 +62,74 @@ def css_selector(group, url):
|
||||
|
||||
class AbstractWebElement(collections.abc.MutableMapping):
|
||||
|
||||
"""A wrapper around QtWebKit/QtWebEngine web element.
|
||||
"""A wrapper around QtWebKit/QtWebEngine web element."""
|
||||
|
||||
Attributes:
|
||||
tab: The tab associated with this element.
|
||||
"""
|
||||
|
||||
def __init__(self, tab):
|
||||
def __init__(self, tab: 'browsertab.AbstractTab') -> None:
|
||||
self._tab = tab
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: object) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def __getitem__(self, key):
|
||||
def __getitem__(self, key: str) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def __setitem__(self, key, val):
|
||||
def __setitem__(self, key: str, val: str) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def __delitem__(self, key):
|
||||
def __delitem__(self, key: str) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> typing.Iterator[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
raise NotImplementedError
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
try:
|
||||
html = utils.compact_text(self.outer_xml(), 500)
|
||||
except Error:
|
||||
html = None
|
||||
return utils.get_repr(self, html=html)
|
||||
|
||||
def has_frame(self):
|
||||
def has_frame(self) -> bool:
|
||||
"""Check if this element has a valid frame attached."""
|
||||
raise NotImplementedError
|
||||
|
||||
def geometry(self):
|
||||
def geometry(self) -> QRect:
|
||||
"""Get the geometry for this element."""
|
||||
raise NotImplementedError
|
||||
|
||||
def classes(self):
|
||||
def classes(self) -> typing.List[str]:
|
||||
"""Get a list of classes assigned to this element."""
|
||||
raise NotImplementedError
|
||||
|
||||
def tag_name(self):
|
||||
def tag_name(self) -> str:
|
||||
"""Get the tag name of this element.
|
||||
|
||||
The returned name will always be lower-case.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def outer_xml(self):
|
||||
def outer_xml(self) -> str:
|
||||
"""Get the full HTML representation of this element."""
|
||||
raise NotImplementedError
|
||||
|
||||
def value(self):
|
||||
def value(self) -> JsValueType:
|
||||
"""Get the value attribute for this element, or None."""
|
||||
raise NotImplementedError
|
||||
|
||||
def set_value(self, value):
|
||||
def set_value(self, value: JsValueType) -> None:
|
||||
"""Set the element value."""
|
||||
raise NotImplementedError
|
||||
|
||||
def dispatch_event(self, event, bubbles=False,
|
||||
cancelable=False, composed=False):
|
||||
def dispatch_event(self, event: str,
|
||||
bubbles: bool = False,
|
||||
cancelable: bool = False,
|
||||
composed: bool = False) -> None:
|
||||
"""Dispatch an event to the element.
|
||||
|
||||
Args:
|
||||
@ -134,35 +140,25 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def insert_text(self, text):
|
||||
def insert_text(self, text: str) -> None:
|
||||
"""Insert the given text into the element."""
|
||||
raise NotImplementedError
|
||||
|
||||
def rect_on_view(self, *, elem_geometry=None, no_js=False):
|
||||
def rect_on_view(self, *, elem_geometry: QRect = None,
|
||||
no_js: bool = False) -> QRect:
|
||||
"""Get the geometry of the element relative to the webview.
|
||||
|
||||
Uses the getClientRects() JavaScript method to obtain the collection of
|
||||
rectangles containing the element and returns the first rectangle which
|
||||
is large enough (larger than 1px times 1px). If all rectangles returned
|
||||
by getClientRects() are too small, falls back to elem.rect_on_view().
|
||||
|
||||
Skipping of small rectangles is due to <a> elements containing other
|
||||
elements with "display:block" style, see
|
||||
https://github.com/qutebrowser/qutebrowser/issues/1298
|
||||
|
||||
Args:
|
||||
elem_geometry: The geometry of the element, or None.
|
||||
Calling QWebElement::geometry is rather expensive so
|
||||
we want to avoid doing it twice.
|
||||
no_js: Fall back to the Python implementation
|
||||
no_js: Fall back to the Python implementation.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def is_writable(self):
|
||||
def is_writable(self) -> bool:
|
||||
"""Check whether an element is writable."""
|
||||
return not ('disabled' in self or 'readonly' in self)
|
||||
|
||||
def is_content_editable(self):
|
||||
def is_content_editable(self) -> bool:
|
||||
"""Check if an element has a contenteditable attribute.
|
||||
|
||||
Args:
|
||||
@ -177,7 +173,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
def _is_editable_object(self):
|
||||
def _is_editable_object(self) -> bool:
|
||||
"""Check if an object-element is editable."""
|
||||
if 'type' not in self:
|
||||
log.webelem.debug("<object> without type clicked...")
|
||||
@ -193,7 +189,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
# Image/Audio/...
|
||||
return False
|
||||
|
||||
def _is_editable_input(self):
|
||||
def _is_editable_input(self) -> bool:
|
||||
"""Check if an input-element is editable.
|
||||
|
||||
Return:
|
||||
@ -210,7 +206,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
else:
|
||||
return False
|
||||
|
||||
def _is_editable_classes(self):
|
||||
def _is_editable_classes(self) -> bool:
|
||||
"""Check if an element is editable based on its classes.
|
||||
|
||||
Return:
|
||||
@ -229,7 +225,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_editable(self, strict=False):
|
||||
def is_editable(self, strict: bool = False) -> bool:
|
||||
"""Check whether we should switch to insert mode for this element.
|
||||
|
||||
Args:
|
||||
@ -260,17 +256,17 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
return self._is_editable_classes() and not strict
|
||||
return False
|
||||
|
||||
def is_text_input(self):
|
||||
def is_text_input(self) -> bool:
|
||||
"""Check if this element is some kind of text box."""
|
||||
roles = ('combobox', 'textbox')
|
||||
tag = self.tag_name()
|
||||
return self.get('role', None) in roles or tag in ['input', 'textarea']
|
||||
|
||||
def remove_blank_target(self):
|
||||
def remove_blank_target(self) -> None:
|
||||
"""Remove target from link."""
|
||||
raise NotImplementedError
|
||||
|
||||
def resolve_url(self, baseurl):
|
||||
def resolve_url(self, baseurl: QUrl) -> typing.Optional[QUrl]:
|
||||
"""Resolve the URL in the element's src/href attribute.
|
||||
|
||||
Args:
|
||||
@ -297,16 +293,16 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
qtutils.ensure_valid(url)
|
||||
return url
|
||||
|
||||
def is_link(self):
|
||||
def is_link(self) -> bool:
|
||||
"""Return True if this AbstractWebElement is a link."""
|
||||
href_tags = ['a', 'area', 'link']
|
||||
return self.tag_name() in href_tags and 'href' in self
|
||||
|
||||
def _requires_user_interaction(self):
|
||||
def _requires_user_interaction(self) -> bool:
|
||||
"""Return True if clicking this element needs user interaction."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _mouse_pos(self):
|
||||
def _mouse_pos(self) -> QPoint:
|
||||
"""Get the position to click/hover."""
|
||||
# Click the center of the largest square fitting into the top/left
|
||||
# corner of the rectangle, this will help if part of the <a> element
|
||||
@ -322,35 +318,38 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
raise Error("Element position is out of view!")
|
||||
return pos
|
||||
|
||||
def _move_text_cursor(self):
|
||||
def _move_text_cursor(self) -> None:
|
||||
"""Move cursor to end after clicking."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _click_fake_event(self, click_target):
|
||||
def _click_fake_event(self, click_target: usertypes.ClickTarget) -> None:
|
||||
"""Send a fake click event to the element."""
|
||||
pos = self._mouse_pos()
|
||||
|
||||
log.webelem.debug("Sending fake click to {!r} at position {} with "
|
||||
"target {}".format(self, pos, click_target))
|
||||
|
||||
modifiers = {
|
||||
target_modifiers = {
|
||||
usertypes.ClickTarget.normal: Qt.NoModifier,
|
||||
usertypes.ClickTarget.window: Qt.AltModifier | Qt.ShiftModifier,
|
||||
usertypes.ClickTarget.tab: Qt.ControlModifier,
|
||||
usertypes.ClickTarget.tab_bg: Qt.ControlModifier,
|
||||
}
|
||||
if config.val.tabs.background:
|
||||
modifiers[usertypes.ClickTarget.tab] |= Qt.ShiftModifier
|
||||
target_modifiers[usertypes.ClickTarget.tab] |= Qt.ShiftModifier
|
||||
else:
|
||||
modifiers[usertypes.ClickTarget.tab_bg] |= Qt.ShiftModifier
|
||||
target_modifiers[usertypes.ClickTarget.tab_bg] |= Qt.ShiftModifier
|
||||
|
||||
modifiers = typing.cast(Qt.KeyboardModifiers,
|
||||
target_modifiers[click_target])
|
||||
|
||||
events = [
|
||||
QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
|
||||
Qt.NoModifier),
|
||||
QMouseEvent(QEvent.MouseButtonPress, pos, Qt.LeftButton,
|
||||
Qt.LeftButton, modifiers[click_target]),
|
||||
Qt.LeftButton, modifiers),
|
||||
QMouseEvent(QEvent.MouseButtonRelease, pos, Qt.LeftButton,
|
||||
Qt.NoButton, modifiers[click_target]),
|
||||
Qt.NoButton, modifiers),
|
||||
]
|
||||
|
||||
for evt in events:
|
||||
@ -358,15 +357,15 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
|
||||
QTimer.singleShot(0, self._move_text_cursor)
|
||||
|
||||
def _click_editable(self, click_target):
|
||||
def _click_editable(self, click_target: usertypes.ClickTarget) -> None:
|
||||
"""Fake a click on an editable input field."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _click_js(self, click_target):
|
||||
def _click_js(self, click_target: usertypes.ClickTarget) -> None:
|
||||
"""Fake a click by using the JS .click() method."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _click_href(self, click_target):
|
||||
def _click_href(self, click_target: usertypes.ClickTarget) -> None:
|
||||
"""Fake a click on an element with a href by opening the link."""
|
||||
baseurl = self._tab.url()
|
||||
url = self.resolve_url(baseurl)
|
||||
@ -388,7 +387,8 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
else:
|
||||
raise ValueError("Unknown ClickTarget {}".format(click_target))
|
||||
|
||||
def click(self, click_target, *, force_event=False):
|
||||
def click(self, click_target: usertypes.ClickTarget, *,
|
||||
force_event: bool = False) -> None:
|
||||
"""Simulate a click on the element.
|
||||
|
||||
Args:
|
||||
@ -425,7 +425,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
else:
|
||||
raise ValueError("Unknown ClickTarget {}".format(click_target))
|
||||
|
||||
def hover(self):
|
||||
def hover(self) -> None:
|
||||
"""Simulate a mouse hover over the element."""
|
||||
pos = self._mouse_pos()
|
||||
event = QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
|
||||
|
@ -26,15 +26,15 @@ from PyQt5.QtWebEngineCore import (QWebEngineUrlRequestInterceptor,
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.browser import shared
|
||||
from qutebrowser.utils import utils, log, debug
|
||||
from qutebrowser.extensions import interceptors
|
||||
|
||||
|
||||
class RequestInterceptor(QWebEngineUrlRequestInterceptor):
|
||||
|
||||
"""Handle ad blocking and custom headers."""
|
||||
|
||||
def __init__(self, host_blocker, args, parent=None):
|
||||
def __init__(self, args, parent=None):
|
||||
super().__init__(parent)
|
||||
self._host_blocker = host_blocker
|
||||
self._args = args
|
||||
|
||||
def install(self, profile):
|
||||
@ -84,9 +84,10 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor):
|
||||
return
|
||||
|
||||
# FIXME:qtwebengine only block ads for NavigationTypeOther?
|
||||
if self._host_blocker.is_blocked(url, first_party):
|
||||
log.webview.info("Request to {} blocked by host blocker.".format(
|
||||
url.host()))
|
||||
request = interceptors.Request(first_party_url=first_party,
|
||||
request_url=url)
|
||||
interceptors.run(request)
|
||||
if request.is_blocked:
|
||||
info.block(True)
|
||||
|
||||
for header, value in shared.custom_headers(url=url):
|
||||
|
@ -22,20 +22,27 @@
|
||||
|
||||
"""QtWebEngine specific part of the web element API."""
|
||||
|
||||
import typing
|
||||
|
||||
from PyQt5.QtCore import QRect, Qt, QPoint, QEventLoop
|
||||
from PyQt5.QtGui import QMouseEvent
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtWebEngineWidgets import QWebEngineSettings
|
||||
|
||||
from qutebrowser.utils import log, javascript, urlutils
|
||||
from qutebrowser.utils import log, javascript, urlutils, usertypes
|
||||
from qutebrowser.browser import webelem
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
# pylint: disable=unused-import,useless-suppression
|
||||
from qutebrowser.browser.webengine import webenginetab
|
||||
|
||||
|
||||
class WebEngineElement(webelem.AbstractWebElement):
|
||||
|
||||
"""A web element for QtWebEngine, using JS under the hood."""
|
||||
|
||||
def __init__(self, js_dict, tab):
|
||||
def __init__(self, js_dict: typing.Dict[str, typing.Any],
|
||||
tab: 'webenginetab.WebEngineTab') -> None:
|
||||
super().__init__(tab)
|
||||
# Do some sanity checks on the data we get from JS
|
||||
js_dict_types = {
|
||||
@ -48,7 +55,7 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
'rects': list,
|
||||
'attributes': dict,
|
||||
'caret_position': (int, type(None)),
|
||||
}
|
||||
} # type: typing.Dict[str, typing.Union[type, typing.Tuple[type,...]]]
|
||||
assert set(js_dict.keys()).issubset(js_dict_types.keys())
|
||||
for name, typ in js_dict_types.items():
|
||||
if name in js_dict and not isinstance(js_dict[name], typ):
|
||||
@ -73,50 +80,51 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
self._id = js_dict['id']
|
||||
self._js_dict = js_dict
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self._js_dict.get('text', '')
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, WebEngineElement):
|
||||
return NotImplemented
|
||||
return self._id == other._id # pylint: disable=protected-access
|
||||
|
||||
def __getitem__(self, key):
|
||||
def __getitem__(self, key: str) -> str:
|
||||
attrs = self._js_dict['attributes']
|
||||
return attrs[key]
|
||||
|
||||
def __setitem__(self, key, val):
|
||||
def __setitem__(self, key: str, val: str) -> None:
|
||||
self._js_dict['attributes'][key] = val
|
||||
self._js_call('set_attribute', key, val)
|
||||
|
||||
def __delitem__(self, key):
|
||||
def __delitem__(self, key: str) -> None:
|
||||
log.stub()
|
||||
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> typing.Iterator[str]:
|
||||
return iter(self._js_dict['attributes'])
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
return len(self._js_dict['attributes'])
|
||||
|
||||
def _js_call(self, name, *args, callback=None):
|
||||
def _js_call(self, name: str, *args: webelem.JsValueType,
|
||||
callback: typing.Callable[[typing.Any], None] = None) -> None:
|
||||
"""Wrapper to run stuff from webelem.js."""
|
||||
if self._tab.is_deleted():
|
||||
raise webelem.OrphanedError("Tab containing element vanished")
|
||||
js_code = javascript.assemble('webelem', name, self._id, *args)
|
||||
self._tab.run_js_async(js_code, callback=callback)
|
||||
|
||||
def has_frame(self):
|
||||
def has_frame(self) -> bool:
|
||||
return True
|
||||
|
||||
def geometry(self):
|
||||
def geometry(self) -> QRect:
|
||||
log.stub()
|
||||
return QRect()
|
||||
|
||||
def classes(self):
|
||||
def classes(self) -> typing.List[str]:
|
||||
"""Get a list of classes assigned to this element."""
|
||||
return self._js_dict['class_name'].split()
|
||||
|
||||
def tag_name(self):
|
||||
def tag_name(self) -> str:
|
||||
"""Get the tag name of this element.
|
||||
|
||||
The returned name will always be lower-case.
|
||||
@ -125,34 +133,37 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
assert isinstance(tag, str), tag
|
||||
return tag.lower()
|
||||
|
||||
def outer_xml(self):
|
||||
def outer_xml(self) -> str:
|
||||
"""Get the full HTML representation of this element."""
|
||||
return self._js_dict['outer_xml']
|
||||
|
||||
def value(self):
|
||||
def value(self) -> webelem.JsValueType:
|
||||
return self._js_dict.get('value', None)
|
||||
|
||||
def set_value(self, value):
|
||||
def set_value(self, value: webelem.JsValueType) -> None:
|
||||
self._js_call('set_value', value)
|
||||
|
||||
def dispatch_event(self, event, bubbles=False,
|
||||
cancelable=False, composed=False):
|
||||
def dispatch_event(self, event: str,
|
||||
bubbles: bool = False,
|
||||
cancelable: bool = False,
|
||||
composed: bool = False) -> None:
|
||||
self._js_call('dispatch_event', event, bubbles, cancelable, composed)
|
||||
|
||||
def caret_position(self):
|
||||
def caret_position(self) -> typing.Optional[int]:
|
||||
"""Get the text caret position for the current element.
|
||||
|
||||
If the element is not a text element, None is returned.
|
||||
"""
|
||||
return self._js_dict.get('caret_position', None)
|
||||
|
||||
def insert_text(self, text):
|
||||
def insert_text(self, text: str) -> None:
|
||||
if not self.is_editable(strict=True):
|
||||
raise webelem.Error("Element is not editable!")
|
||||
log.webelem.debug("Inserting text into element {!r}".format(self))
|
||||
self._js_call('insert_text', text)
|
||||
|
||||
def rect_on_view(self, *, elem_geometry=None, no_js=False):
|
||||
def rect_on_view(self, *, elem_geometry: QRect = None,
|
||||
no_js: bool = False) -> QRect:
|
||||
"""Get the geometry of the element relative to the webview.
|
||||
|
||||
Skipping of small rectangles is due to <a> elements containing other
|
||||
@ -193,16 +204,16 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
self, rects))
|
||||
return QRect()
|
||||
|
||||
def remove_blank_target(self):
|
||||
def remove_blank_target(self) -> None:
|
||||
if self._js_dict['attributes'].get('target') == '_blank':
|
||||
self._js_dict['attributes']['target'] = '_top'
|
||||
self._js_call('remove_blank_target')
|
||||
|
||||
def _move_text_cursor(self):
|
||||
def _move_text_cursor(self) -> None:
|
||||
if self.is_text_input() and self.is_editable():
|
||||
self._js_call('move_cursor_to_end')
|
||||
|
||||
def _requires_user_interaction(self):
|
||||
def _requires_user_interaction(self) -> bool:
|
||||
baseurl = self._tab.url()
|
||||
url = self.resolve_url(baseurl)
|
||||
if url is None:
|
||||
@ -211,7 +222,7 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
return False
|
||||
return url.scheme() not in urlutils.WEBENGINE_SCHEMES
|
||||
|
||||
def _click_editable(self, click_target):
|
||||
def _click_editable(self, click_target: usertypes.ClickTarget) -> None:
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58515
|
||||
ev = QMouseEvent(QMouseEvent.MouseButtonPress, QPoint(0, 0),
|
||||
QPoint(0, 0), QPoint(0, 0), Qt.NoButton, Qt.NoButton,
|
||||
@ -221,10 +232,11 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
self._js_call('focus')
|
||||
self._move_text_cursor()
|
||||
|
||||
def _click_js(self, _click_target):
|
||||
def _click_js(self, _click_target: usertypes.ClickTarget) -> None:
|
||||
# FIXME:qtwebengine Have a proper API for this
|
||||
# pylint: disable=protected-access
|
||||
view = self._tab._widget
|
||||
assert view is not None
|
||||
# pylint: enable=protected-access
|
||||
attribute = QWebEngineSettings.JavascriptCanOpenWindows
|
||||
could_open_windows = view.settings().testAttribute(attribute)
|
||||
@ -238,8 +250,9 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
qapp.processEvents(QEventLoop.ExcludeSocketNotifiers |
|
||||
QEventLoop.ExcludeUserInputEvents)
|
||||
|
||||
def reset_setting(_arg):
|
||||
def reset_setting(_arg: typing.Any) -> None:
|
||||
"""Set the JavascriptCanOpenWindows setting to its old value."""
|
||||
assert view is not None
|
||||
try:
|
||||
view.settings().setAttribute(attribute, could_open_windows)
|
||||
except RuntimeError:
|
||||
|
@ -22,6 +22,11 @@
|
||||
from PyQt5.QtCore import QBuffer, QIODevice, QUrl
|
||||
from PyQt5.QtWebEngineCore import (QWebEngineUrlSchemeHandler,
|
||||
QWebEngineUrlRequestJob)
|
||||
try:
|
||||
from PyQt5.QtWebEngineCore import QWebEngineUrlScheme # type: ignore
|
||||
except ImportError:
|
||||
# Added in Qt 5.12
|
||||
QWebEngineUrlScheme = None
|
||||
|
||||
from qutebrowser.browser import qutescheme
|
||||
from qutebrowser.utils import log, qtutils
|
||||
@ -33,8 +38,12 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
|
||||
|
||||
def install(self, profile):
|
||||
"""Install the handler for qute:// URLs on the given profile."""
|
||||
if QWebEngineUrlScheme is not None:
|
||||
assert QWebEngineUrlScheme.schemeByName(b'qute') is not None
|
||||
|
||||
profile.installUrlSchemeHandler(b'qute', self)
|
||||
if qtutils.version_check('5.11', compiled=False):
|
||||
if (qtutils.version_check('5.11', compiled=False) and
|
||||
not qtutils.version_check('5.12', compiled=False)):
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-63378
|
||||
profile.installUrlSchemeHandler(b'chrome-error', self)
|
||||
profile.installUrlSchemeHandler(b'chrome-extension', self)
|
||||
@ -130,3 +139,16 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
|
||||
buf.seek(0)
|
||||
buf.close()
|
||||
job.reply(mimetype.encode('ascii'), buf)
|
||||
|
||||
|
||||
def init():
|
||||
"""Register the qute:// scheme.
|
||||
|
||||
Note this needs to be called early, before constructing any QtWebEngine
|
||||
classes.
|
||||
"""
|
||||
if QWebEngineUrlScheme is not None:
|
||||
scheme = QWebEngineUrlScheme(b'qute')
|
||||
scheme.setFlags(QWebEngineUrlScheme.LocalScheme |
|
||||
QWebEngineUrlScheme.LocalAccessAllowed)
|
||||
QWebEngineUrlScheme.registerScheme(scheme)
|
||||
|
@ -30,7 +30,7 @@ from PyQt5.QtGui import QFont
|
||||
from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile,
|
||||
QWebEnginePage)
|
||||
|
||||
from qutebrowser.browser.webengine import spell
|
||||
from qutebrowser.browser.webengine import spell, webenginequtescheme
|
||||
from qutebrowser.config import config, websettings
|
||||
from qutebrowser.config.websettings import AttributeInfo as Attr
|
||||
from qutebrowser.utils import utils, standarddir, qtutils, message, log
|
||||
@ -298,6 +298,7 @@ def init(args):
|
||||
not hasattr(QWebEnginePage, 'setInspectedPage')): # only Qt < 5.11
|
||||
os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = str(utils.random_port())
|
||||
|
||||
webenginequtescheme.init()
|
||||
spell.init()
|
||||
|
||||
_init_profiles()
|
||||
|
@ -60,10 +60,8 @@ def init():
|
||||
_qute_scheme_handler.install(webenginesettings.private_profile)
|
||||
|
||||
log.init.debug("Initializing request interceptor...")
|
||||
host_blocker = objreg.get('host-blocker')
|
||||
args = objreg.get('args')
|
||||
req_interceptor = interceptor.RequestInterceptor(
|
||||
host_blocker, args=args, parent=app)
|
||||
req_interceptor = interceptor.RequestInterceptor(args=args, parent=app)
|
||||
req_interceptor.install(webenginesettings.default_profile)
|
||||
req_interceptor.install(webenginesettings.private_profile)
|
||||
|
||||
|
@ -39,6 +39,7 @@ from PyQt5.QtCore import QUrl
|
||||
from qutebrowser.browser import downloads
|
||||
from qutebrowser.browser.webkit import webkitelem
|
||||
from qutebrowser.utils import log, objreg, message, usertypes, utils, urlutils
|
||||
from qutebrowser.extensions import interceptors
|
||||
|
||||
|
||||
@attr.s
|
||||
@ -354,8 +355,9 @@ class _Downloader:
|
||||
# qute, see the comments/discussion on
|
||||
# https://github.com/qutebrowser/qutebrowser/pull/962#discussion_r40256987
|
||||
# and https://github.com/qutebrowser/qutebrowser/issues/1053
|
||||
host_blocker = objreg.get('host-blocker')
|
||||
if host_blocker.is_blocked(url):
|
||||
request = interceptors.Request(first_party_url=None, request_url=url)
|
||||
interceptors.run(request)
|
||||
if request.is_blocked:
|
||||
log.downloads.debug("Skipping {}, host-blocked".format(url))
|
||||
# We still need an empty file in the output, QWebView can be pretty
|
||||
# picky about displaying a file correctly when not all assets are
|
||||
|
@ -38,6 +38,7 @@ if MYPY:
|
||||
from qutebrowser.utils import (message, log, usertypes, utils, objreg,
|
||||
urlutils, debug)
|
||||
from qutebrowser.browser import shared
|
||||
from qutebrowser.extensions import interceptors
|
||||
from qutebrowser.browser.webkit import certificateerror
|
||||
from qutebrowser.browser.webkit.network import (webkitqutescheme, networkreply,
|
||||
filescheme)
|
||||
@ -405,10 +406,10 @@ class NetworkManager(QNetworkAccessManager):
|
||||
# the webpage shutdown here.
|
||||
current_url = QUrl()
|
||||
|
||||
host_blocker = objreg.get('host-blocker')
|
||||
if host_blocker.is_blocked(req.url(), current_url):
|
||||
log.webview.info("Request to {} blocked by host blocker.".format(
|
||||
req.url().host()))
|
||||
request = interceptors.Request(first_party_url=current_url,
|
||||
request_url=req.url())
|
||||
interceptors.run(request)
|
||||
if request.is_blocked:
|
||||
return networkreply.ErrorNetworkReply(
|
||||
req, HOSTBLOCK_ERROR_STRING, QNetworkReply.ContentAccessDenied,
|
||||
self)
|
||||
|
@ -19,12 +19,19 @@
|
||||
|
||||
"""QtWebKit specific part of the web element API."""
|
||||
|
||||
import typing
|
||||
|
||||
from PyQt5.QtCore import QRect
|
||||
from PyQt5.QtWebKit import QWebElement, QWebSettings
|
||||
from PyQt5.QtWebKitWidgets import QWebFrame
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import log, utils, javascript
|
||||
from qutebrowser.utils import log, utils, javascript, usertypes
|
||||
from qutebrowser.browser import webelem
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
# pylint: disable=unused-import,useless-suppression
|
||||
from qutebrowser.browser.webkit import webkittab
|
||||
|
||||
|
||||
class IsNullError(webelem.Error):
|
||||
@ -36,7 +43,7 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
|
||||
"""A wrapper around a QWebElement."""
|
||||
|
||||
def __init__(self, elem, tab):
|
||||
def __init__(self, elem: QWebElement, tab: 'webkittab.WebKitTab') -> None:
|
||||
super().__init__(tab)
|
||||
if isinstance(elem, self.__class__):
|
||||
raise TypeError("Trying to wrap a wrapper!")
|
||||
@ -44,90 +51,94 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
raise IsNullError('{} is a null element!'.format(elem))
|
||||
self._elem = elem
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
self._check_vanished()
|
||||
return self._elem.toPlainText()
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, WebKitElement):
|
||||
return NotImplemented
|
||||
return self._elem == other._elem # pylint: disable=protected-access
|
||||
|
||||
def __getitem__(self, key):
|
||||
def __getitem__(self, key: str) -> str:
|
||||
self._check_vanished()
|
||||
if key not in self:
|
||||
raise KeyError(key)
|
||||
return self._elem.attribute(key)
|
||||
|
||||
def __setitem__(self, key, val):
|
||||
def __setitem__(self, key: str, val: str) -> None:
|
||||
self._check_vanished()
|
||||
self._elem.setAttribute(key, val)
|
||||
|
||||
def __delitem__(self, key):
|
||||
def __delitem__(self, key: str) -> None:
|
||||
self._check_vanished()
|
||||
if key not in self:
|
||||
raise KeyError(key)
|
||||
self._elem.removeAttribute(key)
|
||||
|
||||
def __contains__(self, key):
|
||||
def __contains__(self, key: object) -> bool:
|
||||
assert isinstance(key, str)
|
||||
self._check_vanished()
|
||||
return self._elem.hasAttribute(key)
|
||||
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> typing.Iterator[str]:
|
||||
self._check_vanished()
|
||||
yield from self._elem.attributeNames()
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
self._check_vanished()
|
||||
return len(self._elem.attributeNames())
|
||||
|
||||
def _check_vanished(self):
|
||||
def _check_vanished(self) -> None:
|
||||
"""Raise an exception if the element vanished (is null)."""
|
||||
if self._elem.isNull():
|
||||
raise IsNullError('Element {} vanished!'.format(self._elem))
|
||||
|
||||
def has_frame(self):
|
||||
def has_frame(self) -> bool:
|
||||
self._check_vanished()
|
||||
return self._elem.webFrame() is not None
|
||||
|
||||
def geometry(self):
|
||||
def geometry(self) -> QRect:
|
||||
self._check_vanished()
|
||||
return self._elem.geometry()
|
||||
|
||||
def classes(self):
|
||||
def classes(self) -> typing.List[str]:
|
||||
self._check_vanished()
|
||||
return self._elem.classes()
|
||||
|
||||
def tag_name(self):
|
||||
def tag_name(self) -> str:
|
||||
"""Get the tag name for the current element."""
|
||||
self._check_vanished()
|
||||
return self._elem.tagName().lower()
|
||||
|
||||
def outer_xml(self):
|
||||
def outer_xml(self) -> str:
|
||||
"""Get the full HTML representation of this element."""
|
||||
self._check_vanished()
|
||||
return self._elem.toOuterXml()
|
||||
|
||||
def value(self):
|
||||
def value(self) -> webelem.JsValueType:
|
||||
self._check_vanished()
|
||||
val = self._elem.evaluateJavaScript('this.value')
|
||||
assert isinstance(val, (int, float, str, type(None))), val
|
||||
return val
|
||||
|
||||
def set_value(self, value):
|
||||
def set_value(self, value: webelem.JsValueType) -> None:
|
||||
self._check_vanished()
|
||||
if self._tab.is_deleted():
|
||||
raise webelem.OrphanedError("Tab containing element vanished")
|
||||
if self.is_content_editable():
|
||||
log.webelem.debug("Filling {!r} via set_text.".format(self))
|
||||
assert isinstance(value, str)
|
||||
self._elem.setPlainText(value)
|
||||
else:
|
||||
log.webelem.debug("Filling {!r} via javascript.".format(self))
|
||||
value = javascript.to_js(value)
|
||||
self._elem.evaluateJavaScript("this.value={}".format(value))
|
||||
|
||||
def dispatch_event(self, event, bubbles=False,
|
||||
cancelable=False, composed=False):
|
||||
def dispatch_event(self, event: str,
|
||||
bubbles: bool = False,
|
||||
cancelable: bool = False,
|
||||
composed: bool = False) -> None:
|
||||
self._check_vanished()
|
||||
log.webelem.debug("Firing event on {!r} via javascript.".format(self))
|
||||
self._elem.evaluateJavaScript(
|
||||
@ -138,7 +149,7 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
javascript.to_js(cancelable),
|
||||
javascript.to_js(composed)))
|
||||
|
||||
def caret_position(self):
|
||||
def caret_position(self) -> int:
|
||||
"""Get the text caret position for the current element."""
|
||||
self._check_vanished()
|
||||
pos = self._elem.evaluateJavaScript('this.selectionStart')
|
||||
@ -146,7 +157,7 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
return 0
|
||||
return int(pos)
|
||||
|
||||
def insert_text(self, text):
|
||||
def insert_text(self, text: str) -> None:
|
||||
self._check_vanished()
|
||||
if not self.is_editable(strict=True):
|
||||
raise webelem.Error("Element is not editable!")
|
||||
@ -158,7 +169,7 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
this.dispatchEvent(event);
|
||||
""".format(javascript.to_js(text)))
|
||||
|
||||
def _parent(self):
|
||||
def _parent(self) -> typing.Optional['WebKitElement']:
|
||||
"""Get the parent element of this element."""
|
||||
self._check_vanished()
|
||||
elem = self._elem.parent()
|
||||
@ -166,7 +177,7 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
return None
|
||||
return WebKitElement(elem, tab=self._tab)
|
||||
|
||||
def _rect_on_view_js(self):
|
||||
def _rect_on_view_js(self) -> typing.Optional[QRect]:
|
||||
"""Javascript implementation for rect_on_view."""
|
||||
# FIXME:qtwebengine maybe we can reuse this?
|
||||
rects = self._elem.evaluateJavaScript("this.getClientRects()")
|
||||
@ -178,8 +189,8 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
return None
|
||||
|
||||
text = utils.compact_text(self._elem.toOuterXml(), 500)
|
||||
log.webelem.vdebug("Client rectangles of element '{}': {}".format(
|
||||
text, rects))
|
||||
log.webelem.vdebug( # type: ignore
|
||||
"Client rectangles of element '{}': {}".format(text, rects))
|
||||
|
||||
for i in range(int(rects.get("length", 0))):
|
||||
rect = rects[str(i)]
|
||||
@ -204,7 +215,8 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
|
||||
return None
|
||||
|
||||
def _rect_on_view_python(self, elem_geometry):
|
||||
def _rect_on_view_python(self,
|
||||
elem_geometry: typing.Optional[QRect]) -> QRect:
|
||||
"""Python implementation for rect_on_view."""
|
||||
if elem_geometry is None:
|
||||
geometry = self._elem.geometry()
|
||||
@ -218,7 +230,8 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
frame = frame.parentFrame()
|
||||
return rect
|
||||
|
||||
def rect_on_view(self, *, elem_geometry=None, no_js=False):
|
||||
def rect_on_view(self, *, elem_geometry: QRect = None,
|
||||
no_js: bool = False) -> QRect:
|
||||
"""Get the geometry of the element relative to the webview.
|
||||
|
||||
Uses the getClientRects() JavaScript method to obtain the collection of
|
||||
@ -248,7 +261,7 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
# No suitable rects found via JS, try via the QWebElement API
|
||||
return self._rect_on_view_python(elem_geometry)
|
||||
|
||||
def _is_visible(self, mainframe):
|
||||
def _is_visible(self, mainframe: QWebFrame) -> bool:
|
||||
"""Check if the given element is visible in the given frame.
|
||||
|
||||
This is not public API because it can't be implemented easily here with
|
||||
@ -300,8 +313,8 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
visible_in_frame = visible_on_screen
|
||||
return all([visible_on_screen, visible_in_frame])
|
||||
|
||||
def remove_blank_target(self):
|
||||
elem = self
|
||||
def remove_blank_target(self) -> None:
|
||||
elem = self # type: typing.Optional[WebKitElement]
|
||||
for _ in range(5):
|
||||
if elem is None:
|
||||
break
|
||||
@ -311,14 +324,14 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
break
|
||||
elem = elem._parent() # pylint: disable=protected-access
|
||||
|
||||
def _move_text_cursor(self):
|
||||
def _move_text_cursor(self) -> None:
|
||||
if self.is_text_input() and self.is_editable():
|
||||
self._tab.caret.move_to_end_of_document()
|
||||
|
||||
def _requires_user_interaction(self):
|
||||
def _requires_user_interaction(self) -> bool:
|
||||
return False
|
||||
|
||||
def _click_editable(self, click_target):
|
||||
def _click_editable(self, click_target: usertypes.ClickTarget) -> None:
|
||||
ok = self._elem.evaluateJavaScript('this.focus(); true;')
|
||||
if ok:
|
||||
self._move_text_cursor()
|
||||
@ -326,7 +339,7 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
log.webelem.debug("Failed to focus via JS, falling back to event")
|
||||
self._click_fake_event(click_target)
|
||||
|
||||
def _click_js(self, click_target):
|
||||
def _click_js(self, click_target: usertypes.ClickTarget) -> None:
|
||||
settings = QWebSettings.globalSettings()
|
||||
attribute = QWebSettings.JavascriptCanOpenWindows
|
||||
could_open_windows = settings.testAttribute(attribute)
|
||||
@ -337,12 +350,12 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
log.webelem.debug("Failed to click via JS, falling back to event")
|
||||
self._click_fake_event(click_target)
|
||||
|
||||
def _click_fake_event(self, click_target):
|
||||
def _click_fake_event(self, click_target: usertypes.ClickTarget) -> None:
|
||||
self._tab.data.override_target = click_target
|
||||
super()._click_fake_event(click_target)
|
||||
|
||||
|
||||
def get_child_frames(startframe):
|
||||
def get_child_frames(startframe: QWebFrame) -> typing.List[QWebFrame]:
|
||||
"""Get all children recursively of a given QWebFrame.
|
||||
|
||||
Loosely based on http://blog.nextgenetics.net/?e=64
|
||||
@ -356,7 +369,7 @@ def get_child_frames(startframe):
|
||||
results = []
|
||||
frames = [startframe]
|
||||
while frames:
|
||||
new_frames = []
|
||||
new_frames = [] # type: typing.List[QWebFrame]
|
||||
for frame in frames:
|
||||
results.append(frame)
|
||||
new_frames += frame.childFrames()
|
||||
|
@ -212,11 +212,11 @@ class CompletionItemDelegate(QStyledItemDelegate):
|
||||
view = self.parent()
|
||||
pattern = view.pattern
|
||||
columns_to_filter = index.model().columns_to_filter(index)
|
||||
self._doc.setPlainText(self._opt.text)
|
||||
if index.column() in columns_to_filter and pattern:
|
||||
pat = re.escape(pattern).replace(r'\ ', r'|')
|
||||
_Highlighter(self._doc, pat,
|
||||
config.val.colors.completion.match.fg)
|
||||
self._doc.setPlainText(self._opt.text)
|
||||
else:
|
||||
self._doc.setHtml(
|
||||
'<span style="font: {};">{}</span>'.format(
|
||||
|
@ -17,4 +17,4 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""qutebrowser "extensions" which only use the qutebrowser.API API."""
|
||||
"""qutebrowser "extensions" which only use the qutebrowser.api API."""
|
||||
|
@ -24,19 +24,22 @@ import os.path
|
||||
import functools
|
||||
import posixpath
|
||||
import zipfile
|
||||
import logging
|
||||
import typing
|
||||
import pathlib
|
||||
|
||||
from qutebrowser.browser import downloads
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import objreg, standarddir, log, message
|
||||
from qutebrowser.api import cmdutils
|
||||
from PyQt5.QtCore import QUrl
|
||||
|
||||
from qutebrowser.api import (cmdutils, hook, config, message, downloads,
|
||||
interceptor, apitypes)
|
||||
|
||||
|
||||
def _guess_zip_filename(zf):
|
||||
"""Guess which file to use inside a zip file.
|
||||
logger = logging.getLogger('misc')
|
||||
_host_blocker = typing.cast('HostBlocker', None)
|
||||
|
||||
Args:
|
||||
zf: A ZipFile instance.
|
||||
"""
|
||||
|
||||
def _guess_zip_filename(zf: zipfile.ZipFile) -> str:
|
||||
"""Guess which file to use inside a zip file."""
|
||||
files = zf.namelist()
|
||||
if len(files) == 1:
|
||||
return files[0]
|
||||
@ -47,7 +50,7 @@ def _guess_zip_filename(zf):
|
||||
raise FileNotFoundError("No hosts file found in zip")
|
||||
|
||||
|
||||
def get_fileobj(byte_io):
|
||||
def get_fileobj(byte_io: typing.IO[bytes]) -> typing.IO[bytes]:
|
||||
"""Get a usable file object to read the hosts file from."""
|
||||
byte_io.seek(0) # rewind downloaded file
|
||||
if zipfile.is_zipfile(byte_io):
|
||||
@ -60,24 +63,20 @@ def get_fileobj(byte_io):
|
||||
return byte_io
|
||||
|
||||
|
||||
def _is_whitelisted_url(url):
|
||||
"""Check if the given URL is on the adblock whitelist.
|
||||
|
||||
Args:
|
||||
url: The URL to check as QUrl.
|
||||
"""
|
||||
def _is_whitelisted_url(url: QUrl) -> bool:
|
||||
"""Check if the given URL is on the adblock whitelist."""
|
||||
for pattern in config.val.content.host_blocking.whitelist:
|
||||
if pattern.matches(url):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class _FakeDownload:
|
||||
class _FakeDownload(downloads.TempDownload):
|
||||
|
||||
"""A download stub to use on_download_finished with local files."""
|
||||
|
||||
def __init__(self, fileobj):
|
||||
self.basename = os.path.basename(fileobj.name)
|
||||
def __init__(self, # pylint: disable=super-init-not-called
|
||||
fileobj: typing.IO[bytes]) -> None:
|
||||
self.fileobj = fileobj
|
||||
self.successful = True
|
||||
|
||||
@ -93,37 +92,46 @@ class HostBlocker:
|
||||
_done_count: How many files have been read successfully.
|
||||
_local_hosts_file: The path to the blocked-hosts file.
|
||||
_config_hosts_file: The path to a blocked-hosts in ~/.config
|
||||
_has_basedir: Whether a custom --basedir is set.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._blocked_hosts = set()
|
||||
self._config_blocked_hosts = set()
|
||||
self._in_progress = []
|
||||
def __init__(self, *, data_dir: pathlib.Path, config_dir: pathlib.Path,
|
||||
has_basedir: bool = False) -> None:
|
||||
self._has_basedir = has_basedir
|
||||
self._blocked_hosts = set() # type: typing.Set[str]
|
||||
self._config_blocked_hosts = set() # type: typing.Set[str]
|
||||
self._in_progress = [] # type: typing.List[downloads.TempDownload]
|
||||
self._done_count = 0
|
||||
|
||||
data_dir = standarddir.data()
|
||||
self._local_hosts_file = os.path.join(data_dir, 'blocked-hosts')
|
||||
self._update_files()
|
||||
self._local_hosts_file = str(data_dir / 'blocked-hosts')
|
||||
self.update_files()
|
||||
|
||||
config_dir = standarddir.config()
|
||||
self._config_hosts_file = os.path.join(config_dir, 'blocked-hosts')
|
||||
self._config_hosts_file = str(config_dir / 'blocked-hosts')
|
||||
|
||||
config.instance.changed.connect(self._update_files)
|
||||
|
||||
def is_blocked(self, url, first_party_url=None):
|
||||
"""Check if the given URL (as QUrl) is blocked."""
|
||||
def _is_blocked(self, request_url: QUrl,
|
||||
first_party_url: QUrl = None) -> bool:
|
||||
"""Check whether the given request is blocked."""
|
||||
if first_party_url is not None and not first_party_url.isValid():
|
||||
first_party_url = None
|
||||
if not config.instance.get('content.host_blocking.enabled',
|
||||
|
||||
if not config.get('content.host_blocking.enabled',
|
||||
url=first_party_url):
|
||||
return False
|
||||
|
||||
host = url.host()
|
||||
host = request_url.host()
|
||||
return ((host in self._blocked_hosts or
|
||||
host in self._config_blocked_hosts) and
|
||||
not _is_whitelisted_url(url))
|
||||
not _is_whitelisted_url(request_url))
|
||||
|
||||
def _read_hosts_file(self, filename, target):
|
||||
def filter_request(self, info: interceptor.Request) -> None:
|
||||
"""Block the given request if necessary."""
|
||||
if self._is_blocked(request_url=info.request_url,
|
||||
first_party_url=info.first_party_url):
|
||||
logger.info("Request to {} blocked by host blocker."
|
||||
.format(info.request_url.host()))
|
||||
info.block()
|
||||
|
||||
def _read_hosts_file(self, filename: str, target: typing.Set[str]) -> bool:
|
||||
"""Read hosts from the given filename.
|
||||
|
||||
Args:
|
||||
@ -141,11 +149,11 @@ class HostBlocker:
|
||||
for line in f:
|
||||
target.add(line.strip())
|
||||
except (OSError, UnicodeDecodeError):
|
||||
log.misc.exception("Failed to read host blocklist!")
|
||||
logger.exception("Failed to read host blocklist!")
|
||||
|
||||
return True
|
||||
|
||||
def read_hosts(self):
|
||||
def read_hosts(self) -> None:
|
||||
"""Read hosts from the existing blocked-hosts file."""
|
||||
self._blocked_hosts = set()
|
||||
|
||||
@ -156,24 +164,17 @@ class HostBlocker:
|
||||
self._blocked_hosts)
|
||||
|
||||
if not found:
|
||||
args = objreg.get('args')
|
||||
if (config.val.content.host_blocking.lists and
|
||||
args.basedir is None and
|
||||
not self._has_basedir and
|
||||
config.val.content.host_blocking.enabled):
|
||||
message.info("Run :adblock-update to get adblock lists.")
|
||||
|
||||
@cmdutils.register(instance='host-blocker')
|
||||
def adblock_update(self):
|
||||
"""Update the adblock block lists.
|
||||
|
||||
This updates `~/.local/share/qutebrowser/blocked-hosts` with downloaded
|
||||
host lists and re-reads `~/.config/qutebrowser/blocked-hosts`.
|
||||
"""
|
||||
def adblock_update(self) -> None:
|
||||
"""Update the adblock block lists."""
|
||||
self._read_hosts_file(self._config_hosts_file,
|
||||
self._config_blocked_hosts)
|
||||
self._blocked_hosts = set()
|
||||
self._done_count = 0
|
||||
download_manager = objreg.get('qtnetwork-download-manager')
|
||||
for url in config.val.content.host_blocking.lists:
|
||||
if url.scheme() == 'file':
|
||||
filename = url.toLocalFile()
|
||||
@ -184,16 +185,12 @@ class HostBlocker:
|
||||
else:
|
||||
self._import_local(filename)
|
||||
else:
|
||||
fobj = io.BytesIO()
|
||||
fobj.name = 'adblock: ' + url.host()
|
||||
target = downloads.FileObjDownloadTarget(fobj)
|
||||
download = download_manager.get(url, target=target,
|
||||
auto_remove=True)
|
||||
download = downloads.download_temp(url)
|
||||
self._in_progress.append(download)
|
||||
download.finished.connect(
|
||||
functools.partial(self._on_download_finished, download))
|
||||
|
||||
def _import_local(self, filename):
|
||||
def _import_local(self, filename: str) -> None:
|
||||
"""Adds the contents of a file to the blocklist.
|
||||
|
||||
Args:
|
||||
@ -209,24 +206,24 @@ class HostBlocker:
|
||||
self._in_progress.append(download)
|
||||
self._on_download_finished(download)
|
||||
|
||||
def _parse_line(self, line):
|
||||
def _parse_line(self, raw_line: bytes) -> bool:
|
||||
"""Parse a line from a host file.
|
||||
|
||||
Args:
|
||||
line: The bytes object to parse.
|
||||
raw_line: The bytes object to parse.
|
||||
|
||||
Returns:
|
||||
True if parsing succeeded, False otherwise.
|
||||
"""
|
||||
if line.startswith(b'#'):
|
||||
if raw_line.startswith(b'#'):
|
||||
# Ignoring comments early so we don't have to care about
|
||||
# encoding errors in them.
|
||||
return True
|
||||
|
||||
try:
|
||||
line = line.decode('utf-8')
|
||||
line = raw_line.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
log.misc.error("Failed to decode: {!r}".format(line))
|
||||
logger.error("Failed to decode: {!r}".format(raw_line))
|
||||
return False
|
||||
|
||||
# Remove comments
|
||||
@ -257,14 +254,11 @@ class HostBlocker:
|
||||
|
||||
return True
|
||||
|
||||
def _merge_file(self, byte_io):
|
||||
def _merge_file(self, byte_io: io.BytesIO) -> None:
|
||||
"""Read and merge host files.
|
||||
|
||||
Args:
|
||||
byte_io: The BytesIO object of the completed download.
|
||||
|
||||
Return:
|
||||
A set of the merged hosts.
|
||||
"""
|
||||
error_count = 0
|
||||
line_count = 0
|
||||
@ -282,12 +276,12 @@ class HostBlocker:
|
||||
if not ok:
|
||||
error_count += 1
|
||||
|
||||
log.misc.debug("{}: read {} lines".format(byte_io.name, line_count))
|
||||
logger.debug("{}: read {} lines".format(byte_io.name, line_count))
|
||||
if error_count > 0:
|
||||
message.error("adblock: {} read errors for {}".format(
|
||||
error_count, byte_io.name))
|
||||
|
||||
def _on_lists_downloaded(self):
|
||||
def _on_lists_downloaded(self) -> None:
|
||||
"""Install block lists after files have been downloaded."""
|
||||
with open(self._local_hosts_file, 'w', encoding='utf-8') as f:
|
||||
for host in sorted(self._blocked_hosts):
|
||||
@ -295,8 +289,7 @@ class HostBlocker:
|
||||
message.info("adblock: Read {} hosts from {} sources.".format(
|
||||
len(self._blocked_hosts), self._done_count))
|
||||
|
||||
@config.change_filter('content.host_blocking.lists')
|
||||
def _update_files(self):
|
||||
def update_files(self) -> None:
|
||||
"""Update files when the config changed."""
|
||||
if not config.val.content.host_blocking.lists:
|
||||
try:
|
||||
@ -304,13 +297,13 @@ class HostBlocker:
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except OSError as e:
|
||||
log.misc.exception("Failed to delete hosts file: {}".format(e))
|
||||
logger.exception("Failed to delete hosts file: {}".format(e))
|
||||
|
||||
def _on_download_finished(self, download):
|
||||
def _on_download_finished(self, download: downloads.TempDownload) -> None:
|
||||
"""Check if all downloads are finished and if so, trigger reading.
|
||||
|
||||
Arguments:
|
||||
download: The finished DownloadItem.
|
||||
download: The finished download.
|
||||
"""
|
||||
self._in_progress.remove(download)
|
||||
if download.successful:
|
||||
@ -323,4 +316,32 @@ class HostBlocker:
|
||||
try:
|
||||
self._on_lists_downloaded()
|
||||
except OSError:
|
||||
log.misc.exception("Failed to write host block list!")
|
||||
logger.exception("Failed to write host block list!")
|
||||
|
||||
|
||||
@cmdutils.register()
|
||||
def adblock_update() -> None:
|
||||
"""Update the adblock block lists.
|
||||
|
||||
This updates `~/.local/share/qutebrowser/blocked-hosts` with downloaded
|
||||
host lists and re-reads `~/.config/qutebrowser/blocked-hosts`.
|
||||
"""
|
||||
# FIXME: As soon as we can register instances again, we should move this
|
||||
# back to the class.
|
||||
_host_blocker.adblock_update()
|
||||
|
||||
|
||||
@hook.config_changed('content.host_blocking.lists')
|
||||
def on_config_changed() -> None:
|
||||
_host_blocker.update_files()
|
||||
|
||||
|
||||
@hook.init()
|
||||
def init(context: apitypes.InitContext) -> None:
|
||||
"""Initialize the host blocker."""
|
||||
global _host_blocker
|
||||
_host_blocker = HostBlocker(data_dir=context.data_dir,
|
||||
config_dir=context.config_dir,
|
||||
has_basedir=context.args.basedir is not None)
|
||||
_host_blocker.read_hosts()
|
||||
interceptor.register(_host_blocker.filter_request)
|
@ -118,7 +118,7 @@ def printpage(tab: apitypes.Tab,
|
||||
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
|
||||
def home(tab: apitypes.Tab) -> None:
|
||||
"""Open main startpage in current tab."""
|
||||
if tab.data.pinned:
|
||||
if tab.navigation_blocked():
|
||||
message.info("Tab is pinned!")
|
||||
else:
|
||||
tab.load_url(config.val.url.start_pages[0])
|
||||
@ -238,7 +238,7 @@ def tab_mute(tab: apitypes.Tab) -> None:
|
||||
if tab is None:
|
||||
return
|
||||
try:
|
||||
tab.audio.set_muted(tab.audio.is_muted(), override=True)
|
||||
tab.audio.set_muted(not tab.audio.is_muted(), override=True)
|
||||
except apitypes.WebTabError as e:
|
||||
raise cmdutils.CommandError(e)
|
||||
|
||||
|
@ -86,7 +86,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name
|
||||
not configdata.is_valid_prefix(self._option)):
|
||||
raise configexc.NoOptionError(self._option)
|
||||
|
||||
def _check_match(self, option: typing.Optional[str]) -> bool:
|
||||
def check_match(self, option: typing.Optional[str]) -> bool:
|
||||
"""Check if the given option matches the filter."""
|
||||
if option is None:
|
||||
# Called directly, not from a config change event.
|
||||
@ -119,7 +119,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name
|
||||
@functools.wraps(func)
|
||||
def func_wrapper(option: str = None) -> typing.Any:
|
||||
"""Call the underlying function."""
|
||||
if self._check_match(option):
|
||||
if self.check_match(option):
|
||||
return func()
|
||||
return None
|
||||
return func_wrapper
|
||||
@ -128,7 +128,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name
|
||||
def meth_wrapper(wrapper_self: typing.Any,
|
||||
option: str = None) -> typing.Any:
|
||||
"""Call the underlying function."""
|
||||
if self._check_match(option):
|
||||
if self.check_match(option):
|
||||
return func(wrapper_self)
|
||||
return None
|
||||
return meth_wrapper
|
||||
|
@ -46,7 +46,9 @@ class ConfigCache:
|
||||
self._cache[attr] = config.instance.get(attr)
|
||||
|
||||
def __getitem__(self, attr: str) -> typing.Any:
|
||||
if attr not in self._cache:
|
||||
try:
|
||||
return self._cache[attr]
|
||||
except KeyError:
|
||||
assert not config.instance.get_opt(attr).supports_pattern
|
||||
self._cache[attr] = config.instance.get(attr)
|
||||
return self._cache[attr]
|
||||
|
@ -25,7 +25,7 @@ DATA: A dict of Option objects after init() has been called.
|
||||
"""
|
||||
|
||||
import typing
|
||||
from typing import Optional # pylint: disable=unused-import
|
||||
from typing import Optional # pylint: disable=unused-import,useless-suppression
|
||||
import functools
|
||||
|
||||
import attr
|
||||
|
@ -1768,6 +1768,11 @@ tabs.pinned.shrink:
|
||||
type: Bool
|
||||
desc: Shrink pinned tabs down to their contents.
|
||||
|
||||
tabs.pinned.frozen:
|
||||
type: Bool
|
||||
default: True
|
||||
desc: Force pinned tabs to stay at fixed URL.
|
||||
|
||||
tabs.wrap:
|
||||
default: true
|
||||
type: Bool
|
||||
|
@ -308,7 +308,7 @@ class YamlConfig(QObject):
|
||||
|
||||
self._migrate_bool(settings, 'tabs.favicons.show', 'always', 'never')
|
||||
self._migrate_bool(settings, 'scrolling.bar',
|
||||
'when-searching', 'never')
|
||||
'always', 'when-searching')
|
||||
self._migrate_bool(settings, 'qt.force_software_rendering',
|
||||
'software-opengl', 'none')
|
||||
|
||||
|
0
qutebrowser/extensions/__init__.py
Normal file
0
qutebrowser/extensions/__init__.py
Normal file
63
qutebrowser/extensions/interceptors.py
Normal file
63
qutebrowser/extensions/interceptors.py
Normal 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)
|
187
qutebrowser/extensions/loader.py
Normal file
187
qutebrowser/extensions/loader.py
Normal 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)
|
@ -1,5 +1,5 @@
|
||||
/* eslint-disable max-len, max-statements, complexity,
|
||||
default-case, valid-jsdoc */
|
||||
default-case */
|
||||
|
||||
// Copyright 2014 The Chromium Authors. All rights reserved.
|
||||
//
|
||||
|
@ -64,7 +64,7 @@ class NotInModeError(Exception):
|
||||
|
||||
def init(win_id, parent):
|
||||
"""Initialize the mode manager and the keyparsers for the given win_id."""
|
||||
KM = usertypes.KeyMode # noqa: N801,N806 pylint: disable=invalid-name
|
||||
KM = usertypes.KeyMode # noqa: N806
|
||||
modeman = ModeManager(win_id, parent)
|
||||
objreg.register('mode-manager', modeman, scope='window', window=win_id)
|
||||
keyparsers = {
|
||||
|
@ -32,7 +32,7 @@ from qutebrowser.commands import runners
|
||||
from qutebrowser.api import cmdutils
|
||||
from qutebrowser.config import config, configfiles
|
||||
from qutebrowser.utils import (message, log, usertypes, qtutils, objreg, utils,
|
||||
jinja)
|
||||
jinja, debug)
|
||||
from qutebrowser.mainwindow import messageview, prompt
|
||||
from qutebrowser.completion import completionwidget, completer
|
||||
from qutebrowser.keyinput import modeman
|
||||
@ -137,6 +137,7 @@ class MainWindow(QWidget):
|
||||
Attributes:
|
||||
status: The StatusBar widget.
|
||||
tabbed_browser: The TabbedBrowser widget.
|
||||
state_before_fullscreen: window state before activation of fullscreen.
|
||||
_downloadview: The DownloadView widget.
|
||||
_vbox: The main QVBoxLayout.
|
||||
_commandrunner: The main CommandRunner instance.
|
||||
@ -238,6 +239,8 @@ class MainWindow(QWidget):
|
||||
objreg.get("app").new_window.emit(self)
|
||||
self._set_decoration(config.val.window.hide_decoration)
|
||||
|
||||
self.state_before_fullscreen = self.windowState()
|
||||
|
||||
def _init_geometry(self, geometry):
|
||||
"""Initialize the window geometry or load it from disk."""
|
||||
if geometry is not None:
|
||||
@ -517,9 +520,13 @@ class MainWindow(QWidget):
|
||||
def _on_fullscreen_requested(self, on):
|
||||
if not config.val.content.windowed_fullscreen:
|
||||
if on:
|
||||
self.setWindowState(self.windowState() | Qt.WindowFullScreen)
|
||||
self.state_before_fullscreen = self.windowState()
|
||||
self.setWindowState(
|
||||
Qt.WindowFullScreen | self.state_before_fullscreen)
|
||||
elif self.isFullScreen():
|
||||
self.setWindowState(self.windowState() & ~Qt.WindowFullScreen)
|
||||
self.setWindowState(self.state_before_fullscreen)
|
||||
log.misc.debug('on: {}, state before fullscreen: {}'.format(
|
||||
on, debug.qflags_key(Qt, self.state_before_fullscreen)))
|
||||
|
||||
@cmdutils.register(instance='main-window', scope='window')
|
||||
@pyqtSlot()
|
||||
|
@ -238,6 +238,9 @@ def _handle_wayland():
|
||||
if has_qt511 and config.val.qt.force_software_rendering == 'chromium':
|
||||
return
|
||||
|
||||
if qtutils.version_check('5.11.2', compiled=False):
|
||||
return
|
||||
|
||||
buttons = []
|
||||
text = "<p>You can work around this in one of the following ways:</p>"
|
||||
|
||||
|
@ -137,6 +137,7 @@ prompt = logging.getLogger('prompt')
|
||||
network = logging.getLogger('network')
|
||||
sql = logging.getLogger('sql')
|
||||
greasemonkey = logging.getLogger('greasemonkey')
|
||||
extensions = logging.getLogger('extensions')
|
||||
|
||||
LOGGER_NAMES = [
|
||||
'statusbar', 'completion', 'init', 'url',
|
||||
@ -146,7 +147,7 @@ LOGGER_NAMES = [
|
||||
'js', 'qt', 'rfc6266', 'ipc', 'shlexer',
|
||||
'save', 'message', 'config', 'sessions',
|
||||
'webelem', 'prompt', 'network', 'sql',
|
||||
'greasemonkey'
|
||||
'greasemonkey', 'extensions',
|
||||
]
|
||||
|
||||
|
||||
|
@ -42,12 +42,12 @@ def _log_stack(typ: str, stack: str) -> None:
|
||||
|
||||
|
||||
def error(message: str, *, stack: str = None, replace: bool = False) -> None:
|
||||
"""Convenience function to display an error message in the statusbar.
|
||||
"""Display an error message.
|
||||
|
||||
Args:
|
||||
message: The message to show
|
||||
stack: The stack trace to show.
|
||||
replace: Replace existing messages with replace=True
|
||||
message: The message to show.
|
||||
stack: The stack trace to show (if any).
|
||||
replace: Replace existing messages which are still being shown.
|
||||
"""
|
||||
if stack is None:
|
||||
stack = ''.join(traceback.format_stack())
|
||||
@ -60,11 +60,11 @@ def error(message: str, *, stack: str = None, replace: bool = False) -> None:
|
||||
|
||||
|
||||
def warning(message: str, *, replace: bool = False) -> None:
|
||||
"""Convenience function to display a warning message in the statusbar.
|
||||
"""Display a warning message.
|
||||
|
||||
Args:
|
||||
message: The message to show
|
||||
replace: Replace existing messages with replace=True
|
||||
message: The message to show.
|
||||
replace: Replace existing messages which are still being shown.
|
||||
"""
|
||||
_log_stack('warning', ''.join(traceback.format_stack()))
|
||||
log.message.warning(message)
|
||||
@ -72,11 +72,11 @@ def warning(message: str, *, replace: bool = False) -> None:
|
||||
|
||||
|
||||
def info(message: str, *, replace: bool = False) -> None:
|
||||
"""Convenience function to display an info message in the statusbar.
|
||||
"""Display an info message.
|
||||
|
||||
Args:
|
||||
message: The message to show
|
||||
replace: Replace existing messages with replace=True
|
||||
message: The message to show.
|
||||
replace: Replace existing messages which are still being shown.
|
||||
"""
|
||||
log.message.info(message)
|
||||
global_bridge.show(usertypes.MessageLevel.info, message, replace)
|
||||
|
@ -210,15 +210,33 @@ PromptMode = enum.Enum('PromptMode', ['yesno', 'text', 'user_pwd', 'alert',
|
||||
'download'])
|
||||
|
||||
|
||||
# Where to open a clicked link.
|
||||
ClickTarget = enum.Enum('ClickTarget', ['normal', 'tab', 'tab_bg', 'window',
|
||||
'hover'])
|
||||
class ClickTarget(enum.Enum):
|
||||
|
||||
"""How to open a clicked link."""
|
||||
|
||||
normal = 0 #: Open the link in the current tab
|
||||
tab = 1 #: Open the link in a new foreground tab
|
||||
tab_bg = 2 #: Open the link in a new background tab
|
||||
window = 3 #: Open the link in a new window
|
||||
hover = 4 #: Only hover over the link
|
||||
|
||||
|
||||
# Key input modes
|
||||
KeyMode = enum.Enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt',
|
||||
'insert', 'passthrough', 'caret', 'set_mark',
|
||||
'jump_mark', 'record_macro', 'run_macro'])
|
||||
class KeyMode(enum.Enum):
|
||||
|
||||
"""Key input modes."""
|
||||
|
||||
normal = 1 #: Normal mode (no mode was entered)
|
||||
hint = 2 #: Hint mode (showing labels for links)
|
||||
command = 3 #: Command mode (after pressing the colon key)
|
||||
yesno = 4 #: Yes/No prompts
|
||||
prompt = 5 #: Text prompts
|
||||
insert = 6 #: Insert mode (passing through most keys)
|
||||
passthrough = 7 #: Passthrough mode (passing through all keys)
|
||||
caret = 8 #: Caret mode (moving cursor with keys)
|
||||
set_mark = 9
|
||||
jump_mark = 10
|
||||
record_macro = 11
|
||||
run_macro = 12
|
||||
|
||||
|
||||
class Exit(enum.IntEnum):
|
||||
@ -241,8 +259,14 @@ LoadStatus = enum.Enum('LoadStatus', ['none', 'success', 'success_https',
|
||||
Backend = enum.Enum('Backend', ['QtWebKit', 'QtWebEngine'])
|
||||
|
||||
|
||||
# JS world for QtWebEngine
|
||||
JsWorld = enum.Enum('JsWorld', ['main', 'application', 'user', 'jseval'])
|
||||
class JsWorld(enum.Enum):
|
||||
|
||||
"""World/context to run JavaScript code in."""
|
||||
|
||||
main = 1 #: Same world as the web page's JavaScript.
|
||||
application = 2 #: Application world, used by qutebrowser internally.
|
||||
user = 3 #: User world, currently not used.
|
||||
jseval = 4 #: World used for the jseval-command.
|
||||
|
||||
|
||||
# Log level of a JS message. This needs to match up with the keys allowed for
|
||||
|
@ -45,7 +45,8 @@ try:
|
||||
CSafeDumper as YamlDumper)
|
||||
YAML_C_EXT = True
|
||||
except ImportError: # pragma: no cover
|
||||
from yaml import SafeLoader as YamlLoader, SafeDumper as YamlDumper
|
||||
from yaml import (SafeLoader as YamlLoader, # type: ignore
|
||||
SafeDumper as YamlDumper)
|
||||
YAML_C_EXT = False
|
||||
|
||||
import qutebrowser
|
||||
|
@ -5,6 +5,6 @@ colorama==0.4.1
|
||||
cssutils==1.0.2
|
||||
Jinja2==2.10
|
||||
MarkupSafe==1.1.0
|
||||
Pygments==2.3.0
|
||||
Pygments==2.3.1
|
||||
pyPEG2==2.15.2
|
||||
PyYAML==3.13
|
||||
|
@ -204,6 +204,7 @@ WHITELISTED_FILES = [
|
||||
'browser/webkit/webkitinspector.py',
|
||||
'keyinput/macros.py',
|
||||
'browser/webkit/webkitelem.py',
|
||||
'api/interceptor.py',
|
||||
]
|
||||
|
||||
|
||||
|
@ -71,7 +71,7 @@ EOF
|
||||
|
||||
set -e
|
||||
|
||||
if [[ $DOCKER ]]; then
|
||||
if [[ -n $DOCKER ]]; then
|
||||
exit 0
|
||||
elif [[ $TRAVIS_OS_NAME == osx ]]; then
|
||||
# Disable App Nap
|
||||
|
@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ $DOCKER ]]; then
|
||||
if [[ -n $DOCKER ]]; then
|
||||
docker run \
|
||||
--privileged \
|
||||
-v "$PWD:/outside" \
|
||||
|
@ -30,6 +30,7 @@ import argparse
|
||||
import vulture
|
||||
|
||||
import qutebrowser.app # pylint: disable=unused-import
|
||||
from qutebrowser.extensions import loader
|
||||
from qutebrowser.misc import objects
|
||||
from qutebrowser.utils import utils
|
||||
from qutebrowser.browser.webkit import rfc6266
|
||||
@ -43,6 +44,8 @@ from qutebrowser.config import configtypes
|
||||
|
||||
def whitelist_generator(): # noqa
|
||||
"""Generator which yields lines to add to a vulture whitelist."""
|
||||
loader.load_components(skip_hooks=True)
|
||||
|
||||
# qutebrowser commands
|
||||
for cmd in objects.commands.values():
|
||||
yield utils.qualname(cmd.handler)
|
||||
@ -127,6 +130,9 @@ def whitelist_generator(): # noqa
|
||||
yield 'scripts.get_coredumpctl_traces.Line.gid'
|
||||
yield 'scripts.importer.import_moz_places.places.row_factory'
|
||||
|
||||
# component hooks
|
||||
yield 'qutebrowser.components.adblock.on_config_changed'
|
||||
|
||||
|
||||
def filter_func(item):
|
||||
"""Check if a missing function should be filtered or not.
|
||||
|
@ -35,6 +35,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
|
||||
# We import qutebrowser.app so all @cmdutils-register decorators are run.
|
||||
import qutebrowser.app
|
||||
from qutebrowser import qutebrowser, commands
|
||||
from qutebrowser.extensions import loader
|
||||
from qutebrowser.commands import argparser
|
||||
from qutebrowser.config import configdata, configtypes
|
||||
from qutebrowser.utils import docutils, usertypes
|
||||
@ -549,6 +550,7 @@ def regenerate_cheatsheet():
|
||||
def main():
|
||||
"""Regenerate all documentation."""
|
||||
utils.change_cwd()
|
||||
loader.load_components(skip_hooks=True)
|
||||
print("Generating manpage...")
|
||||
regenerate_manpage('doc/qutebrowser.1.asciidoc')
|
||||
print("Generating settings help...")
|
||||
|
@ -27,7 +27,7 @@ import os.path
|
||||
import urllib.request
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||
from qutebrowser.browser import adblock
|
||||
from qutebrowser.components import adblock
|
||||
from qutebrowser.config import configdata
|
||||
|
||||
|
||||
|
@ -93,7 +93,7 @@ Feature: Downloading things from a website.
|
||||
Then no crash should happen
|
||||
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/4240
|
||||
@qt!=5.11.2
|
||||
@qt<5.11.2
|
||||
Scenario: Downloading with SSL errors (issue 1413)
|
||||
When SSL is supported
|
||||
And I clear SSL errors
|
||||
|
@ -124,9 +124,9 @@ Feature: Javascript stuff
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/1190
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/2495
|
||||
|
||||
# Currently broken on Windows:
|
||||
# Currently broken on Windows and on Qt 5.12
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/4230
|
||||
@posix
|
||||
@posix @qt<5.12
|
||||
Scenario: Checking visible/invisible window size
|
||||
When I run :tab-only
|
||||
And I open data/javascript/windowsize.html in a new background tab
|
||||
@ -134,7 +134,7 @@ Feature: Javascript stuff
|
||||
And I run :tab-next
|
||||
Then the window sizes should be the same
|
||||
|
||||
@flaky
|
||||
@flaky @qt<5.12
|
||||
Scenario: Checking visible/invisible window size with vertical tabbar
|
||||
When I run :tab-only
|
||||
And I set tabs.position to left
|
||||
|
@ -1289,6 +1289,14 @@ Feature: Tab management
|
||||
And the following tabs should be open:
|
||||
- data/numbers/1.txt (active) (pinned)
|
||||
|
||||
Scenario: :tab-pin open url with tabs.pinned.frozen = false
|
||||
When I set tabs.pinned.frozen to false
|
||||
And I open data/numbers/1.txt
|
||||
And I run :tab-pin
|
||||
And I open data/numbers/2.txt
|
||||
Then the following tabs should be open:
|
||||
- data/numbers/2.txt (active) (pinned)
|
||||
|
||||
Scenario: :home on a pinned tab
|
||||
When I open data/numbers/1.txt
|
||||
And I run :tab-pin
|
||||
@ -1297,6 +1305,16 @@ Feature: Tab management
|
||||
And the following tabs should be open:
|
||||
- data/numbers/1.txt (active) (pinned)
|
||||
|
||||
Scenario: :home on a pinned tab with tabs.pinned.frozen = false
|
||||
When I set url.start_pages to ["http://localhost:(port)/data/numbers/2.txt"]
|
||||
And I set tabs.pinned.frozen to false
|
||||
And I open data/numbers/1.txt
|
||||
And I run :tab-pin
|
||||
And I run :home
|
||||
Then data/numbers/2.txt should be loaded
|
||||
And the following tabs should be open:
|
||||
- data/numbers/2.txt (active) (pinned)
|
||||
|
||||
Scenario: Cloning a pinned tab
|
||||
When I open data/numbers/1.txt
|
||||
And I run :tab-pin
|
||||
|
@ -356,7 +356,10 @@ class QuteProc(testprocess.Process):
|
||||
self._focus_ready = True
|
||||
else:
|
||||
raise ValueError("Invalid value {!r} for 'what'.".format(what))
|
||||
if self._load_ready and self._focus_ready:
|
||||
|
||||
is_qt_5_12 = qtutils.version_check('5.12', compiled=False)
|
||||
if ((self._load_ready and self._focus_ready) or
|
||||
(self._load_ready and is_qt_5_12)):
|
||||
self._load_ready = False
|
||||
self._focus_ready = False
|
||||
self.ready.emit()
|
||||
|
@ -61,8 +61,8 @@ def normalize_line(line):
|
||||
return line
|
||||
|
||||
|
||||
def normalize_whole(s):
|
||||
if qtutils.version_check('5.12', compiled=False):
|
||||
def normalize_whole(s, webengine):
|
||||
if qtutils.version_check('5.12', compiled=False) and webengine:
|
||||
s = s.replace('\n\n-----=_qute-UUID', '\n-----=_qute-UUID')
|
||||
return s
|
||||
|
||||
@ -71,8 +71,9 @@ class DownloadDir:
|
||||
|
||||
"""Abstraction over a download directory."""
|
||||
|
||||
def __init__(self, tmpdir):
|
||||
def __init__(self, tmpdir, config):
|
||||
self._tmpdir = tmpdir
|
||||
self._config = config
|
||||
self.location = str(tmpdir)
|
||||
|
||||
def read_file(self):
|
||||
@ -92,14 +93,15 @@ class DownloadDir:
|
||||
if normalize_line(line) is not None)
|
||||
actual_data = '\n'.join(normalize_line(line)
|
||||
for line in self.read_file())
|
||||
actual_data = normalize_whole(actual_data)
|
||||
actual_data = normalize_whole(actual_data,
|
||||
webengine=self._config.webengine)
|
||||
|
||||
assert actual_data == expected_data
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def download_dir(tmpdir):
|
||||
return DownloadDir(tmpdir)
|
||||
def download_dir(tmpdir, pytestconfig):
|
||||
return DownloadDir(tmpdir, pytestconfig)
|
||||
|
||||
|
||||
def _test_mhtml_requests(test_dir, test_path, server):
|
||||
|
@ -44,6 +44,7 @@ from PyQt5.QtNetwork import QNetworkCookieJar
|
||||
import helpers.stubs as stubsmod
|
||||
from qutebrowser.config import (config, configdata, configtypes, configexc,
|
||||
configfiles, configcache)
|
||||
from qutebrowser.api import config as configapi
|
||||
from qutebrowser.utils import objreg, standarddir, utils, usertypes
|
||||
from qutebrowser.browser import greasemonkey, history, qutescheme
|
||||
from qutebrowser.browser.webkit import cookies
|
||||
@ -190,8 +191,8 @@ def testdata_scheme(qapp):
|
||||
|
||||
@pytest.fixture
|
||||
def web_tab_setup(qtbot, tab_registry, session_manager_stub,
|
||||
greasemonkey_manager, fake_args, host_blocker_stub,
|
||||
config_stub, testdata_scheme):
|
||||
greasemonkey_manager, fake_args, config_stub,
|
||||
testdata_scheme):
|
||||
"""Shared setup for webkit_tab/webengine_tab."""
|
||||
# Make sure error logging via JS fails tests
|
||||
config_stub.val.content.javascript.log = {
|
||||
@ -306,6 +307,7 @@ def config_stub(stubs, monkeypatch, configdata_init, yaml_config_stub):
|
||||
|
||||
container = config.ConfigContainer(conf)
|
||||
monkeypatch.setattr(config, 'val', container)
|
||||
monkeypatch.setattr(configapi, 'val', container)
|
||||
|
||||
cache = configcache.ConfigCache()
|
||||
monkeypatch.setattr(config, 'cache', cache)
|
||||
@ -328,15 +330,6 @@ def key_config_stub(config_stub, monkeypatch):
|
||||
return keyconf
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def host_blocker_stub(stubs):
|
||||
"""Fixture which provides a fake host blocker object."""
|
||||
stub = stubs.HostBlockerStub()
|
||||
objreg.register('host-blocker', stub)
|
||||
yield stub
|
||||
objreg.delete('host-blocker')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def quickmark_manager_stub(stubs):
|
||||
"""Fixture which provides a fake quickmark manager object."""
|
||||
|
@ -459,17 +459,6 @@ class QuickmarkManagerStub(UrlMarkManagerStub):
|
||||
self.delete(key)
|
||||
|
||||
|
||||
class HostBlockerStub:
|
||||
|
||||
"""Stub for the host-blocker object."""
|
||||
|
||||
def __init__(self):
|
||||
self.blocked_hosts = set()
|
||||
|
||||
def is_blocked(self, url, first_party_url=None):
|
||||
return url in self.blocked_hosts
|
||||
|
||||
|
||||
class SessionManagerStub:
|
||||
|
||||
"""Stub for the session-manager object."""
|
||||
|
@ -205,6 +205,20 @@ def test_secret_url(url, has_secret, from_file):
|
||||
res.resolve(QNetworkProxyQuery(QUrl(url)), from_file=from_file)
|
||||
|
||||
|
||||
def test_logging(qtlog):
|
||||
"""Make sure console.log() works for PAC files."""
|
||||
test_str = """
|
||||
function FindProxyForURL(domain, host) {
|
||||
console.log("logging test");
|
||||
return "DIRECT";
|
||||
}
|
||||
"""
|
||||
res = pac.PACResolver(test_str)
|
||||
res.resolve(QNetworkProxyQuery(QUrl("https://example.com/test")))
|
||||
assert len(qtlog.records) == 1
|
||||
assert qtlog.records[0].message == 'logging test'
|
||||
|
||||
|
||||
def fetcher_test(test_str):
|
||||
class PACHandler(http.server.BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
|
@ -247,7 +247,7 @@ class TestWebKitElement:
|
||||
pytest.param(lambda e: e[None], id='getitem'),
|
||||
pytest.param(lambda e: operator.setitem(e, None, None), id='setitem'),
|
||||
pytest.param(lambda e: operator.delitem(e, None), id='delitem'),
|
||||
pytest.param(lambda e: None in e, id='contains'),
|
||||
pytest.param(lambda e: '' in e, id='contains'),
|
||||
pytest.param(list, id='iter'),
|
||||
pytest.param(len, id='len'),
|
||||
pytest.param(lambda e: e.has_frame(), id='has_frame'),
|
||||
|
@ -20,7 +20,8 @@ from unittest import mock
|
||||
|
||||
import pytest
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QTextDocument
|
||||
from PyQt5.QtGui import QTextDocument, QColor
|
||||
from PyQt5.QtWidgets import QTextEdit
|
||||
|
||||
from qutebrowser.completion import completiondelegate
|
||||
|
||||
@ -50,3 +51,24 @@ def test_highlight(pat, txt, segments):
|
||||
highlighter.setFormat.assert_has_calls([
|
||||
mock.call(s[0], s[1], mock.ANY) for s in segments
|
||||
])
|
||||
|
||||
|
||||
def test_highlighted(qtbot):
|
||||
"""Make sure highlighting works.
|
||||
|
||||
Note that with Qt 5.11.3 and > 5.12.1 we need to call setPlainText *after*
|
||||
creating the highlighter for highlighting to work. Ideally, we'd test
|
||||
whether CompletionItemDelegate._get_textdoc() works properly, but testing
|
||||
that is kind of hard, so we just test it in isolation here.
|
||||
"""
|
||||
doc = QTextDocument()
|
||||
completiondelegate._Highlighter(doc, 'Hello', Qt.red)
|
||||
doc.setPlainText('Hello World')
|
||||
|
||||
# Needed so the highlighting actually works.
|
||||
edit = QTextEdit()
|
||||
qtbot.addWidget(edit)
|
||||
edit.setDocument(doc)
|
||||
|
||||
colors = [f.foreground().color() for f in doc.allFormats()]
|
||||
assert QColor('red') in colors
|
||||
|
@ -28,12 +28,12 @@ import pytest
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
|
||||
from qutebrowser.browser import adblock
|
||||
from qutebrowser.components import adblock
|
||||
from qutebrowser.utils import urlmatch
|
||||
from tests.helpers import utils
|
||||
|
||||
|
||||
pytestmark = pytest.mark.usefixtures('qapp', 'config_tmpdir')
|
||||
pytestmark = pytest.mark.usefixtures('qapp')
|
||||
|
||||
# TODO See ../utils/test_standarddirutils for OSError and caplog assertion
|
||||
|
||||
@ -58,18 +58,13 @@ URLS_TO_CHECK = ('http://localhost',
|
||||
'http://veryverygoodhost.edu')
|
||||
|
||||
|
||||
class BaseDirStub:
|
||||
|
||||
"""Mock for objreg.get('args') called in adblock.HostBlocker.read_hosts."""
|
||||
|
||||
def __init__(self):
|
||||
self.basedir = None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def basedir(fake_args):
|
||||
"""Register a Fake basedir."""
|
||||
fake_args.basedir = None
|
||||
def host_blocker_factory(config_tmpdir, data_tmpdir, download_stub,
|
||||
config_stub):
|
||||
def factory():
|
||||
return adblock.HostBlocker(config_dir=config_tmpdir,
|
||||
data_dir=data_tmpdir)
|
||||
return factory
|
||||
|
||||
|
||||
def create_zipfile(directory, files, zipname='test'):
|
||||
@ -133,9 +128,9 @@ def assert_urls(host_blocker, blocked=BLOCKLIST_HOSTS,
|
||||
url = QUrl(str_url)
|
||||
host = url.host()
|
||||
if host in blocked and host not in whitelisted:
|
||||
assert host_blocker.is_blocked(url)
|
||||
assert host_blocker._is_blocked(url)
|
||||
else:
|
||||
assert not host_blocker.is_blocked(url)
|
||||
assert not host_blocker._is_blocked(url)
|
||||
|
||||
|
||||
def blocklist_to_url(filename):
|
||||
@ -202,13 +197,13 @@ def generic_blocklists(directory):
|
||||
blocklist5.toString()]
|
||||
|
||||
|
||||
def test_disabled_blocking_update(basedir, config_stub, download_stub,
|
||||
data_tmpdir, tmpdir, win_registry, caplog):
|
||||
def test_disabled_blocking_update(config_stub, tmpdir, caplog,
|
||||
host_blocker_factory):
|
||||
"""Ensure no URL is blocked when host blocking is disabled."""
|
||||
config_stub.val.content.host_blocking.lists = generic_blocklists(tmpdir)
|
||||
config_stub.val.content.host_blocking.enabled = False
|
||||
|
||||
host_blocker = adblock.HostBlocker()
|
||||
host_blocker = host_blocker_factory()
|
||||
host_blocker.adblock_update()
|
||||
while host_blocker._in_progress:
|
||||
current_download = host_blocker._in_progress[0]
|
||||
@ -217,10 +212,10 @@ def test_disabled_blocking_update(basedir, config_stub, download_stub,
|
||||
current_download.finished.emit()
|
||||
host_blocker.read_hosts()
|
||||
for str_url in URLS_TO_CHECK:
|
||||
assert not host_blocker.is_blocked(QUrl(str_url))
|
||||
assert not host_blocker._is_blocked(QUrl(str_url))
|
||||
|
||||
|
||||
def test_disabled_blocking_per_url(config_stub, data_tmpdir):
|
||||
def test_disabled_blocking_per_url(config_stub, host_blocker_factory):
|
||||
example_com = 'https://www.example.com/'
|
||||
|
||||
config_stub.val.content.host_blocking.lists = []
|
||||
@ -230,36 +225,34 @@ def test_disabled_blocking_per_url(config_stub, data_tmpdir):
|
||||
|
||||
url = QUrl('blocked.example.com')
|
||||
|
||||
host_blocker = adblock.HostBlocker()
|
||||
host_blocker = host_blocker_factory()
|
||||
host_blocker._blocked_hosts.add(url.host())
|
||||
|
||||
assert host_blocker.is_blocked(url)
|
||||
assert not host_blocker.is_blocked(url, first_party_url=QUrl(example_com))
|
||||
assert host_blocker._is_blocked(url)
|
||||
assert not host_blocker._is_blocked(url, first_party_url=QUrl(example_com))
|
||||
|
||||
|
||||
def test_no_blocklist_update(config_stub, download_stub,
|
||||
data_tmpdir, basedir, tmpdir, win_registry):
|
||||
def test_no_blocklist_update(config_stub, download_stub, host_blocker_factory):
|
||||
"""Ensure no URL is blocked when no block list exists."""
|
||||
config_stub.val.content.host_blocking.lists = None
|
||||
config_stub.val.content.host_blocking.enabled = True
|
||||
|
||||
host_blocker = adblock.HostBlocker()
|
||||
host_blocker = host_blocker_factory()
|
||||
host_blocker.adblock_update()
|
||||
host_blocker.read_hosts()
|
||||
for dl in download_stub.downloads:
|
||||
dl.successful = True
|
||||
for str_url in URLS_TO_CHECK:
|
||||
assert not host_blocker.is_blocked(QUrl(str_url))
|
||||
assert not host_blocker._is_blocked(QUrl(str_url))
|
||||
|
||||
|
||||
def test_successful_update(config_stub, basedir, download_stub,
|
||||
data_tmpdir, tmpdir, win_registry, caplog):
|
||||
def test_successful_update(config_stub, tmpdir, caplog, host_blocker_factory):
|
||||
"""Ensure hosts from host_blocking.lists are blocked after an update."""
|
||||
config_stub.val.content.host_blocking.lists = generic_blocklists(tmpdir)
|
||||
config_stub.val.content.host_blocking.enabled = True
|
||||
config_stub.val.content.host_blocking.whitelist = None
|
||||
|
||||
host_blocker = adblock.HostBlocker()
|
||||
host_blocker = host_blocker_factory()
|
||||
host_blocker.adblock_update()
|
||||
# Simulate download is finished
|
||||
while host_blocker._in_progress:
|
||||
@ -271,11 +264,9 @@ def test_successful_update(config_stub, basedir, download_stub,
|
||||
assert_urls(host_blocker, whitelisted=[])
|
||||
|
||||
|
||||
def test_parsing_multiple_hosts_on_line(config_stub, basedir, download_stub,
|
||||
data_tmpdir, tmpdir, win_registry,
|
||||
caplog):
|
||||
def test_parsing_multiple_hosts_on_line(host_blocker_factory):
|
||||
"""Ensure multiple hosts on a line get parsed correctly."""
|
||||
host_blocker = adblock.HostBlocker()
|
||||
host_blocker = host_blocker_factory()
|
||||
bytes_host_line = ' '.join(BLOCKLIST_HOSTS).encode('utf-8')
|
||||
host_blocker._parse_line(bytes_host_line)
|
||||
assert_urls(host_blocker, whitelisted=[])
|
||||
@ -299,17 +290,15 @@ def test_parsing_multiple_hosts_on_line(config_stub, basedir, download_stub,
|
||||
('127.0.1.1', 'myhostname'),
|
||||
('127.0.0.53', 'myhostname'),
|
||||
])
|
||||
def test_whitelisted_lines(config_stub, basedir, download_stub, data_tmpdir,
|
||||
tmpdir, win_registry, caplog, ip, host):
|
||||
def test_whitelisted_lines(host_blocker_factory, ip, host):
|
||||
"""Make sure we don't block hosts we don't want to."""
|
||||
host_blocker = adblock.HostBlocker()
|
||||
host_blocker = host_blocker_factory()
|
||||
line = ('{} {}'.format(ip, host)).encode('ascii')
|
||||
host_blocker._parse_line(line)
|
||||
assert host not in host_blocker._blocked_hosts
|
||||
|
||||
|
||||
def test_failed_dl_update(config_stub, basedir, download_stub,
|
||||
data_tmpdir, tmpdir, win_registry, caplog):
|
||||
def test_failed_dl_update(config_stub, tmpdir, caplog, host_blocker_factory):
|
||||
"""One blocklist fails to download.
|
||||
|
||||
Ensure hosts from this list are not blocked.
|
||||
@ -323,7 +312,7 @@ def test_failed_dl_update(config_stub, basedir, download_stub,
|
||||
config_stub.val.content.host_blocking.enabled = True
|
||||
config_stub.val.content.host_blocking.whitelist = None
|
||||
|
||||
host_blocker = adblock.HostBlocker()
|
||||
host_blocker = host_blocker_factory()
|
||||
host_blocker.adblock_update()
|
||||
while host_blocker._in_progress:
|
||||
current_download = host_blocker._in_progress[0]
|
||||
@ -339,8 +328,8 @@ def test_failed_dl_update(config_stub, basedir, download_stub,
|
||||
|
||||
|
||||
@pytest.mark.parametrize('location', ['content', 'comment'])
|
||||
def test_invalid_utf8(config_stub, download_stub, tmpdir, data_tmpdir,
|
||||
caplog, location):
|
||||
def test_invalid_utf8(config_stub, tmpdir, caplog, host_blocker_factory,
|
||||
location):
|
||||
"""Make sure invalid UTF-8 is handled correctly.
|
||||
|
||||
See https://github.com/qutebrowser/qutebrowser/issues/2301
|
||||
@ -359,7 +348,7 @@ def test_invalid_utf8(config_stub, download_stub, tmpdir, data_tmpdir,
|
||||
config_stub.val.content.host_blocking.enabled = True
|
||||
config_stub.val.content.host_blocking.whitelist = None
|
||||
|
||||
host_blocker = adblock.HostBlocker()
|
||||
host_blocker = host_blocker_factory()
|
||||
host_blocker.adblock_update()
|
||||
current_download = host_blocker._in_progress[0]
|
||||
|
||||
@ -379,26 +368,25 @@ def test_invalid_utf8(config_stub, download_stub, tmpdir, data_tmpdir,
|
||||
|
||||
|
||||
def test_invalid_utf8_compiled(config_stub, config_tmpdir, data_tmpdir,
|
||||
monkeypatch, caplog):
|
||||
monkeypatch, caplog, host_blocker_factory):
|
||||
"""Make sure invalid UTF-8 in the compiled file is handled."""
|
||||
config_stub.val.content.host_blocking.lists = []
|
||||
|
||||
# Make sure the HostBlocker doesn't delete blocked-hosts in __init__
|
||||
monkeypatch.setattr(adblock.HostBlocker, '_update_files',
|
||||
monkeypatch.setattr(adblock.HostBlocker, 'update_files',
|
||||
lambda _self: None)
|
||||
|
||||
(config_tmpdir / 'blocked-hosts').write_binary(
|
||||
b'https://www.example.org/\xa0')
|
||||
(data_tmpdir / 'blocked-hosts').ensure()
|
||||
|
||||
host_blocker = adblock.HostBlocker()
|
||||
host_blocker = host_blocker_factory()
|
||||
with caplog.at_level(logging.ERROR):
|
||||
host_blocker.read_hosts()
|
||||
assert caplog.messages[-1] == "Failed to read host blocklist!"
|
||||
|
||||
|
||||
def test_blocking_with_whitelist(config_stub, basedir, download_stub,
|
||||
data_tmpdir, tmpdir):
|
||||
def test_blocking_with_whitelist(config_stub, data_tmpdir, host_blocker_factory):
|
||||
"""Ensure hosts in content.host_blocking.whitelist are never blocked."""
|
||||
# Simulate adblock_update has already been run
|
||||
# by creating a file named blocked-hosts,
|
||||
@ -412,13 +400,12 @@ def test_blocking_with_whitelist(config_stub, basedir, download_stub,
|
||||
config_stub.val.content.host_blocking.enabled = True
|
||||
config_stub.val.content.host_blocking.whitelist = list(WHITELISTED_HOSTS)
|
||||
|
||||
host_blocker = adblock.HostBlocker()
|
||||
host_blocker = host_blocker_factory()
|
||||
host_blocker.read_hosts()
|
||||
assert_urls(host_blocker)
|
||||
|
||||
|
||||
def test_config_change_initial(config_stub, basedir, download_stub,
|
||||
data_tmpdir, tmpdir):
|
||||
def test_config_change_initial(config_stub, tmpdir, host_blocker_factory):
|
||||
"""Test emptying host_blocking.lists with existing blocked_hosts.
|
||||
|
||||
- A blocklist is present in host_blocking.lists and blocked_hosts is
|
||||
@ -432,14 +419,13 @@ def test_config_change_initial(config_stub, basedir, download_stub,
|
||||
config_stub.val.content.host_blocking.enabled = True
|
||||
config_stub.val.content.host_blocking.whitelist = None
|
||||
|
||||
host_blocker = adblock.HostBlocker()
|
||||
host_blocker = host_blocker_factory()
|
||||
host_blocker.read_hosts()
|
||||
for str_url in URLS_TO_CHECK:
|
||||
assert not host_blocker.is_blocked(QUrl(str_url))
|
||||
assert not host_blocker._is_blocked(QUrl(str_url))
|
||||
|
||||
|
||||
def test_config_change(config_stub, basedir, download_stub,
|
||||
data_tmpdir, tmpdir):
|
||||
def test_config_change(config_stub, tmpdir, host_blocker_factory):
|
||||
"""Ensure blocked-hosts resets if host-block-list is changed to None."""
|
||||
filtered_blocked_hosts = BLOCKLIST_HOSTS[1:] # Exclude localhost
|
||||
blocklist = blocklist_to_url(create_blocklist(
|
||||
@ -449,16 +435,15 @@ def test_config_change(config_stub, basedir, download_stub,
|
||||
config_stub.val.content.host_blocking.enabled = True
|
||||
config_stub.val.content.host_blocking.whitelist = None
|
||||
|
||||
host_blocker = adblock.HostBlocker()
|
||||
host_blocker = host_blocker_factory()
|
||||
host_blocker.read_hosts()
|
||||
config_stub.val.content.host_blocking.lists = None
|
||||
host_blocker.read_hosts()
|
||||
for str_url in URLS_TO_CHECK:
|
||||
assert not host_blocker.is_blocked(QUrl(str_url))
|
||||
assert not host_blocker._is_blocked(QUrl(str_url))
|
||||
|
||||
|
||||
def test_add_directory(config_stub, basedir, download_stub,
|
||||
data_tmpdir, tmpdir):
|
||||
def test_add_directory(config_stub, tmpdir, host_blocker_factory):
|
||||
"""Ensure adblocker can import all files in a directory."""
|
||||
blocklist_hosts2 = []
|
||||
for i in BLOCKLIST_HOSTS[1:]:
|
||||
@ -471,18 +456,18 @@ def test_add_directory(config_stub, basedir, download_stub,
|
||||
|
||||
config_stub.val.content.host_blocking.lists = [tmpdir.strpath]
|
||||
config_stub.val.content.host_blocking.enabled = True
|
||||
host_blocker = adblock.HostBlocker()
|
||||
host_blocker = host_blocker_factory()
|
||||
host_blocker.adblock_update()
|
||||
assert len(host_blocker._blocked_hosts) == len(blocklist_hosts2) * 2
|
||||
|
||||
|
||||
def test_adblock_benchmark(config_stub, data_tmpdir, basedir, benchmark):
|
||||
def test_adblock_benchmark(data_tmpdir, benchmark, host_blocker_factory):
|
||||
blocked_hosts = os.path.join(utils.abs_datapath(), 'blocked-hosts')
|
||||
shutil.copy(blocked_hosts, str(data_tmpdir))
|
||||
|
||||
url = QUrl('https://www.example.org/')
|
||||
blocker = adblock.HostBlocker()
|
||||
blocker = host_blocker_factory()
|
||||
blocker.read_hosts()
|
||||
assert blocker._blocked_hosts
|
||||
|
||||
benchmark(lambda: blocker.is_blocked(url))
|
||||
benchmark(lambda: blocker._is_blocked(url))
|
@ -50,3 +50,17 @@ def test_configcache_get_after_set(config_stub):
|
||||
assert not config.cache['auto_save.session']
|
||||
config_stub.val.auto_save.session = True
|
||||
assert config.cache['auto_save.session']
|
||||
|
||||
|
||||
def test_configcache_naive_benchmark(config_stub, benchmark):
|
||||
def _run_bench():
|
||||
for _i in range(10000):
|
||||
# pylint: disable=pointless-statement
|
||||
config.cache['tabs.padding']
|
||||
config.cache['tabs.indicator.width']
|
||||
config.cache['tabs.indicator.padding']
|
||||
config.cache['tabs.min_width']
|
||||
config.cache['tabs.max_width']
|
||||
config.cache['tabs.pinned.shrink']
|
||||
# pylint: enable=pointless-statement
|
||||
benchmark(_run_bench)
|
||||
|
@ -250,36 +250,28 @@ class TestYaml:
|
||||
data = autoconfig.read()
|
||||
assert data['content.webrtc_ip_handling_policy']['global'] == expected
|
||||
|
||||
@pytest.mark.parametrize('show, expected', [
|
||||
(True, 'always'),
|
||||
(False, 'never'),
|
||||
('always', 'always'),
|
||||
('never', 'never'),
|
||||
('pinned', 'pinned'),
|
||||
@pytest.mark.parametrize('setting, old, new', [
|
||||
('tabs.favicons.show', True, 'always'),
|
||||
('tabs.favicons.show', False, 'never'),
|
||||
('tabs.favicons.show', 'always', 'always'),
|
||||
|
||||
('scrolling.bar', True, 'always'),
|
||||
('scrolling.bar', False, 'when-searching'),
|
||||
('scrolling.bar', 'always', 'always'),
|
||||
|
||||
('qt.force_software_rendering', True, 'software-opengl'),
|
||||
('qt.force_software_rendering', False, 'none'),
|
||||
('qt.force_software_rendering', 'chromium', 'chromium'),
|
||||
])
|
||||
def test_tabs_favicons_show(self, yaml, autoconfig, show, expected):
|
||||
"""Tests for migration of tabs.favicons.show."""
|
||||
autoconfig.write({'tabs.favicons.show': {'global': show}})
|
||||
def test_bool_migrations(self, yaml, autoconfig, setting, old, new):
|
||||
"""Tests for migration of former boolean settings."""
|
||||
autoconfig.write({setting: {'global': old}})
|
||||
|
||||
yaml.load()
|
||||
yaml._save()
|
||||
|
||||
data = autoconfig.read()
|
||||
assert data['tabs.favicons.show']['global'] == expected
|
||||
|
||||
@pytest.mark.parametrize('force, expected', [
|
||||
(True, 'software-opengl'),
|
||||
(False, 'none'),
|
||||
('chromium', 'chromium'),
|
||||
])
|
||||
def test_force_software_rendering(self, yaml, autoconfig, force, expected):
|
||||
autoconfig.write({'qt.force_software_rendering': {'global': force}})
|
||||
|
||||
yaml.load()
|
||||
yaml._save()
|
||||
|
||||
data = autoconfig.read()
|
||||
assert data['qt.force_software_rendering']['global'] == expected
|
||||
assert data[setting]['global'] == new
|
||||
|
||||
def test_renamed_key_unknown_target(self, monkeypatch, yaml,
|
||||
autoconfig):
|
||||
|
143
tests/unit/extensions/test_loader.py
Normal file
143
tests/unit/extensions/test_loader.py
Normal 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
|
@ -25,7 +25,7 @@ import pytest
|
||||
QtWebEngineWidgets = pytest.importorskip("PyQt5.QtWebEngineWidgets")
|
||||
QWebEngineProfile = QtWebEngineWidgets.QWebEngineProfile
|
||||
|
||||
from qutebrowser.utils import javascript
|
||||
from qutebrowser.utils import javascript, qtutils
|
||||
|
||||
|
||||
DEFAULT_BODY_BG = "rgba(0, 0, 0, 0)"
|
||||
@ -128,6 +128,8 @@ def test_set_error(stylesheet_tester, config_stub):
|
||||
stylesheet_tester.check_set(GREEN_BODY_BG)
|
||||
|
||||
|
||||
@pytest.mark.skip(qtutils.version_check('5.12', compiled=False),
|
||||
reason='Broken with Qt 5.12')
|
||||
def test_appendchild(stylesheet_tester):
|
||||
stylesheet_tester.js.load('stylesheet/simple.html')
|
||||
stylesheet_tester.init_stylesheet()
|
||||
|
@ -34,7 +34,7 @@ from PyQt5.QtTest import QSignalSpy
|
||||
|
||||
import qutebrowser
|
||||
from qutebrowser.misc import ipc
|
||||
from qutebrowser.utils import standarddir, utils
|
||||
from qutebrowser.utils import standarddir, utils, qtutils
|
||||
from helpers import stubs
|
||||
|
||||
|
||||
@ -630,6 +630,8 @@ class TestSendOrListen:
|
||||
assert ret_client is None
|
||||
|
||||
@pytest.mark.posix(reason="Unneeded on Windows")
|
||||
@pytest.mark.xfail(qtutils.version_check('5.12', compiled=False) and
|
||||
utils.is_mac, reason="Broken, see #4471")
|
||||
def test_correct_socket_name(self, args):
|
||||
server = ipc.send_or_listen(args)
|
||||
expected_dir = ipc._get_socketname(args.basedir)
|
||||
|
11
tox.ini
11
tox.ini
@ -199,3 +199,14 @@ deps =
|
||||
-r{toxinidir}/misc/requirements/requirements-mypy.txt
|
||||
commands =
|
||||
{envpython} -m mypy qutebrowser {posargs}
|
||||
|
||||
[testenv:sphinx]
|
||||
basepython = {env:PYTHON:python3}
|
||||
passenv =
|
||||
usedevelop = true
|
||||
deps =
|
||||
-r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/misc/requirements/requirements-pyqt.txt
|
||||
-r{toxinidir}/misc/requirements/requirements-sphinx.txt
|
||||
commands =
|
||||
{envpython} -m sphinx -jauto -W --color {posargs} {toxinidir}/doc/extapi/ {toxinidir}/doc/extapi/_build/
|
||||
|
Loading…
Reference in New Issue
Block a user