Merge branch 'master' into stylesheet-fix

This commit is contained in:
Jay Kamat 2018-12-29 08:02:11 -08:00
commit 57ef3b9b5b
No known key found for this signature in database
GPG Key ID: 5D2E399600F4F7B5
194 changed files with 5126 additions and 3001 deletions

View File

@ -14,6 +14,7 @@ exclude_lines =
raise NotImplementedError raise NotImplementedError
raise utils\.Unreachable raise utils\.Unreachable
if __name__ == ["']__main__["']: if __name__ == ["']__main__["']:
if MYPY:
[xml] [xml]
output=coverage.xml output=coverage.xml

View File

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

BIN
.github/img/hsr.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

1
.gitignore vendored
View File

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

View File

@ -1,37 +1,26 @@
sudo: false dist: xenial
dist: trusty
language: python language: python
group: edge group: edge
python: 3.6 python: 3.6
os: linux
matrix: matrix:
include: include:
- os: linux - env: DOCKER=archlinux
env: DOCKER=archlinux
services: docker services: docker
- os: linux - env: DOCKER=archlinux-webengine QUTE_BDD_WEBENGINE=true
env: DOCKER=archlinux-webengine QUTE_BDD_WEBENGINE=true
services: docker services: docker
- os: linux - env: TESTENV=py36-pyqt571
env: TESTENV=py36-pyqt571 - python: 3.5
- os: linux
python: 3.5
env: TESTENV=py35-pyqt571 env: TESTENV=py35-pyqt571
- os: linux - env: TESTENV=py36-pyqt59
env: TESTENV=py36-pyqt59 - env: TESTENV=py36-pyqt510
- os: linux
env: TESTENV=py36-pyqt510
addons: addons:
apt: apt:
packages: packages:
- xfonts-base - xfonts-base
- os: linux - env: TESTENV=py36-pyqt511-cov
env: TESTENV=py36-pyqt511-cov - python: 3.7
# https://github.com/travis-ci/travis-ci/issues/9069
- os: linux
python: 3.7
sudo: required
dist: xenial
env: TESTENV=py37-pyqt511 env: TESTENV=py37-pyqt511
- os: osx - os: osx
env: TESTENV=py37 OSX=sierra env: TESTENV=py37 OSX=sierra
@ -41,38 +30,26 @@ matrix:
# - os: osx # - os: osx
# env: TESTENV=py35 OSX=yosemite # env: TESTENV=py35 OSX=yosemite
# osx_image: xcode6.4 # osx_image: xcode6.4
- os: linux - env: TESTENV=pylint
env: TESTENV=pylint PYTHON=python3.6 - env: TESTENV=flake8
- os: linux - env: TESTENV=mypy
env: TESTENV=flake8 - env: TESTENV=docs
- os: linux
env: TESTENV=docs
addons: addons:
apt: apt:
packages: packages:
- asciidoc - asciidoc
- os: linux - env: TESTENV=vulture
env: TESTENV=vulture - env: TESTENV=misc
- os: linux - env: TESTENV=pyroma
env: TESTENV=misc - env: TESTENV=check-manifest
- os: linux - env: TESTENV=eslint
env: TESTENV=pyroma
- os: linux
env: TESTENV=check-manifest
- os: linux
env: TESTENV=eslint
language: node_js language: node_js
python: null python: null
node_js: "lts/*" node_js: "lts/*"
- os: linux - language: generic
language: generic
env: TESTENV=shellcheck env: TESTENV=shellcheck
services: docker services: docker
fast_finish: true fast_finish: true
allow_failures:
# https://github.com/qutebrowser/qutebrowser/issues/4055
- os: linux
env: TESTENV=py36-pyqt510
cache: cache:
directories: directories:

View File

@ -32,6 +32,7 @@ include doc/changelog.asciidoc
prune tests prune tests
prune qutebrowser/3rdparty prune qutebrowser/3rdparty
exclude pytest.ini exclude pytest.ini
exclude mypy.ini
exclude qutebrowser/javascript/.eslintrc.yaml exclude qutebrowser/javascript/.eslintrc.yaml
exclude qutebrowser/javascript/.eslintignore exclude qutebrowser/javascript/.eslintignore
exclude doc/help exclude doc/help
@ -39,5 +40,6 @@ exclude .*
exclude misc/qutebrowser.spec exclude misc/qutebrowser.spec
exclude misc/qutebrowser.nsi exclude misc/qutebrowser.nsi
exclude misc/qutebrowser.rcc exclude misc/qutebrowser.rcc
prune doc/extapi
global-exclude __pycache__ *.pyc *.pyo global-exclude __pycache__ *.pyc *.pyo

View File

@ -154,7 +154,11 @@ https://www.macstadium.com/opensource[Open Source Project].
(They don't require including this here - I've just been very happy with their (They don't require including this here - I've just been very happy with their
offer, and without them, no macOS releases or tests would exist) offer, and without them, no macOS releases or tests would exist)
Thanks to the https://www.hsr.ch/[HSR Hochschule für Technik Rapperswil], which
made it possible to work on qutebrowser extensions as a student research project.
image:.github/img/macstadium.png["powered by MacStadium",width=200,link="https://www.macstadium.com/"] image:.github/img/macstadium.png["powered by MacStadium",width=200,link="https://www.macstadium.com/"]
image:.github/img/hsr.png["HSR Hochschule für Technik Rapperswil",link="https://www.hsr.ch/"]
Authors Authors
------- -------

View File

@ -51,6 +51,8 @@ Changed
adblocker can be disabled on a given page. adblocker can be disabled on a given page.
- Elements with a `tabindex` attribute now also get hints by default. - Elements with a `tabindex` attribute now also get hints by default.
- Various small performance improvements for hints and the completion. - Various small performance improvements for hints and the completion.
- The Wayland check for QtWebEngine is now disabled on Qt >= 5.11.2, as those
versions should work without any issues.
Fixed Fixed
~~~~~ ~~~~~
@ -66,6 +68,8 @@ Fixed
like GMail. However, the default for `content.cookies.accept` is still `all` like GMail. However, the default for `content.cookies.accept` is still `all`
to be in line with what other browsers do. to be in line with what other browsers do.
- `:navigate` not incrementing in anchors or queries or anchors. - `:navigate` not incrementing in anchors or queries or anchors.
- Crash when trying to use a proxy requiring authentication with QtWebKit.
- Slashes in search terms are now percent-escaped.
v1.5.2 v1.5.2
------ ------
@ -1244,7 +1248,7 @@ Added
- New `:debug-log-filter` command to change console log filtering on-the-fly. - New `:debug-log-filter` command to change console log filtering on-the-fly.
- New `:debug-log-level` command to change the console loglevel on-the-fly. - New `:debug-log-level` command to change the console loglevel on-the-fly.
- New `general -> yank-ignored-url-parameters` option to configure which URL - New `general -> yank-ignored-url-parameters` option to configure which URL
parameters (like `utm_source` etc.) to strip off when yanking an URL. parameters (like `utm_source` etc.) to strip off when yanking a URL.
- Support for the - Support for the
https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API[HTML5 page visibility API] https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API[HTML5 page visibility API]
- New `readability` userscript which shows a readable version of a page (using - New `readability` userscript which shows a readable version of a page (using
@ -1355,7 +1359,7 @@ Changed
- `:hint` has a new `--add-history` argument to add the URL to the history for - `:hint` has a new `--add-history` argument to add the URL to the history for
yank/spawn targets. yank/spawn targets.
- `:set` now cycles through values if more than one argument is given. - `:set` now cycles through values if more than one argument is given.
- `:open` now opens `default-page` without an URL even without `-t`/`-b`/`-w` given. - `:open` now opens `default-page` without a URL even without `-t`/`-b`/`-w` given.
Deprecated Deprecated
~~~~~~~~~~ ~~~~~~~~~~

View File

@ -407,7 +407,7 @@ Creating a new command is straightforward:
[source,python] [source,python]
---- ----
import qutebrowser.commands.cmdutils from qutebrowser.api import cmdutils
... ...
@ -429,7 +429,7 @@ selects which object registry (global, per-tab, etc.) to use. See the
There are also other arguments to customize the way the command is There are also other arguments to customize the way the command is
registered; see the class documentation for `register` in registered; see the class documentation for `register` in
`qutebrowser.commands.cmdutils` for details. `qutebrowser.api.cmdutils` for details.
The types of the function arguments are inferred based on their default values, 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 e.g., an argument `foo=True` will be converted to a flag `-f`/`--foo` in
@ -480,8 +480,10 @@ For `typing.Union` types, the given `choices` are only checked if other types
The following arguments are supported for `@cmdutils.argument`: The following arguments are supported for `@cmdutils.argument`:
- `flag`: Customize the short flag (`-x`) the argument will get. - `flag`: Customize the short flag (`-x`) the argument will get.
- `win_id=True`: Mark the argument as special window ID argument. - `value`: Tell qutebrowser to fill the argument with special values:
- `count=True`: Mark the argument as special count argument. - `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 (see `qutebrowser.completions.models.*`) - `completion`: A completion function (see `qutebrowser.completions.models.*`)
to use when completing arguments for the given command. to use when completing arguments for the given command.
- `choices`: The allowed string choices for the argument. - `choices`: The allowed string choices for the argument.

View File

View File

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

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

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

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

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

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

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

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

View File

@ -215,11 +215,11 @@ What's the difference between insert and passthrough mode?::
be useful to rebind escape to something else in passthrough mode only, to be be useful to rebind escape to something else in passthrough mode only, to be
able to send an escape keypress to the website. able to send an escape keypress to the website.
Why takes it longer to open an URL in qutebrowser than in chromium?:: Why does it take longer to open a URL in qutebrowser than in chromium?::
When opening an URL in an existing instance the normal qutebrowser When opening a URL in an existing instance, the normal qutebrowser
Python script is started and a few PyQt libraries need to be Python script is started and a few PyQt libraries need to be
loaded until it is detected that there is an instance running loaded until it is detected that there is an instance running
where the URL is then passed to. This takes some time. to which the URL is then passed. This takes some time.
One workaround is to use this One workaround is to use this
https://github.com/qutebrowser/qutebrowser/blob/master/scripts/open_url_in_instance.sh[script] https://github.com/qutebrowser/qutebrowser/blob/master/scripts/open_url_in_instance.sh[script]
and place it in your $PATH with the name "qutebrowser". This and place it in your $PATH with the name "qutebrowser". This
@ -260,6 +260,12 @@ Note that there are some missing features which you may run into:
. Any greasemonkey API function to do with adding UI elements is not currently . Any greasemonkey API function to do with adding UI elements is not currently
supported. That means context menu extentensions and background pages. supported. That means context menu extentensions and background pages.
How do I change the `WM_CLASS` used by qutebrowser windows?::
Qt only supports setting `WM_CLASS` globally, which you can do by starting
with `--qt-arg name foo`. Note that all windows are part of the same
qutebrowser instance (unless you use `--temp-basedir` or `--basedir`), so
they all will share the same `WM_CLASS`.
== Troubleshooting == Troubleshooting
Unable to view flash content.:: Unable to view flash content.::

View File

@ -1484,14 +1484,14 @@ Yank something to the clipboard or primary selection.
[[zoom]] [[zoom]]
=== zoom === zoom
Syntax: +:zoom [*--quiet*] ['zoom']+ Syntax: +:zoom [*--quiet*] ['level']+
Set the zoom level for the current tab. Set the zoom level for the current tab.
The zoom can be given as argument or as [count]. If neither is given, the zoom is set to the default zoom. If both are given, use [count]. The zoom can be given as argument or as [count]. If neither is given, the zoom is set to the default zoom. If both are given, use [count].
==== positional arguments ==== positional arguments
* +'zoom'+: The zoom percentage to set. * +'level'+: The zoom percentage to set.
==== optional arguments ==== optional arguments
* +*-q*+, +*--quiet*+: Don't show a zoom level message. * +*-q*+, +*--quiet*+: Don't show a zoom level message.

View File

@ -19,10 +19,10 @@ hand, you can simply use those - see
<<autoconfig,"Configuring qutebrowser via the user interface">> for details. <<autoconfig,"Configuring qutebrowser via the user interface">> for details.
For more advanced configuration, you can write a `config.py` file - see For more advanced configuration, you can write a `config.py` file - see
<<configpy,"Configuring qutebrowser via config.py">>. As soon as a `config.py` <<configpy,"Configuring qutebrowser via config.py">>. When a `config.py`
exists, the `autoconfig.yml` file **is not read anymore** by default. You need exists, the `autoconfig.yml` file **is not read anymore** by default. You need
to <<configpy-autoconfig,load it by hand>> if you want settings done via to <<configpy-autoconfig,load it from `config.py`>> if you want settings changed via
`:set`/`:bind` to still persist. `:set`/`:bind` to persist between restarts.
[[autoconfig]] [[autoconfig]]
Configuring qutebrowser via the user interface Configuring qutebrowser via the user interface
@ -229,18 +229,18 @@ Loading `autoconfig.yml`
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~
All customization done via the UI (`:set`, `:bind` and `:unbind`) is All customization done via the UI (`:set`, `:bind` and `:unbind`) is
stored in the `autoconfig.yml` file, which is not loaded automatically as soon stored in the `autoconfig.yml` file. When a `config.py` file exists, `autoconfig.yml`
as a `config.py` exists. If you want those settings to be loaded, you'll need to is not loaded automatically. To load `autoconfig.yml` automatically, add the
explicitly load the `autoconfig.yml` file in your `config.py` by doing: following snippet to `config.py`:
.config.py:
[source,python] [source,python]
---- ----
config.load_autoconfig() config.load_autoconfig()
---- ----
If you do so at the top of your file, your `config.py` settings will take You can configure which file overrides the other by the location of the above code snippet.
precedence as they overwrite the settings done in `autoconfig.yml`. Place the snippet at the top to allow `config.py` to override `autoconfig.yml`.
Place the snippet at the bottom for the opposite effect.
Importing other modules Importing other modules
~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -2960,7 +2960,7 @@ Default: +pass:[false]+
=== search.ignore_case === search.ignore_case
When to find text on a page case-insensitively. When to find text on a page case-insensitively.
Type: <<types,String>> Type: <<types,IgnoreCase>>
Valid values: Valid values:
@ -3624,6 +3624,7 @@ Lists with duplicate flags are invalid. Each item is checked against the valid v
|FontFamily|A Qt font family. |FontFamily|A Qt font family.
|FormatString|A string with placeholders. |FormatString|A string with placeholders.
|FuzzyUrl|A URL which gets interpreted as search if needed. |FuzzyUrl|A URL which gets interpreted as search if needed.
|IgnoreCase|Whether to search case insensitively.
|Int|Base class for an integer setting. |Int|Base class for an integer setting.
|Key|A name of a key. |Key|A name of a key.
|List|A list of values. |List|A list of values.

View File

@ -1,7 +1,45 @@
[Desktop Entry] [Desktop Entry]
Name=qutebrowser Name=qutebrowser
GenericName=Web Browser GenericName=Web Browser
GenericName[ar]=
GenericName[bg]=Уеб браузър
GenericName[ca]=Navegador web
GenericName[cs]=WWW prohlížeč
GenericName[da]=Browser
GenericName[de]=Web-Browser
GenericName[el]=Περιηγητής ιστού
GenericName[en_GB]=Web Browser
GenericName[es]=Navegador web
GenericName[et]=Veebibrauser
GenericName[fi]=WWW-selain
GenericName[fr]=Navigateur Web
GenericName[gu]=
GenericName[he]=דפדפן אינטרנט
GenericName[hi]=
GenericName[hu]=Webböngésző
GenericName[it]=Browser Web
GenericName[ja]=
GenericName[kn]=
GenericName[ko]=
GenericName[lt]=Žiniatinklio naršyklė
GenericName[lv]=Tīmekļa pārlūks
GenericName[ml]= <200d>
GenericName[mr]=
GenericName[nb]=Nettleser
GenericName[nl]=Webbrowser
GenericName[pl]=Przeglądarka WWW
GenericName[pt]=Navegador Web
GenericName[pt_BR]=Navegador da Internet
GenericName[ro]=Navigator de Internet
GenericName[ru]=Веб-браузер
GenericName[sl]=Spletni brskalnik
GenericName[sv]=Webbläsare
GenericName[ta]= ி
GenericName[th]=
GenericName[tr]=Web Tarayıcı
GenericName[uk]=Навігатор Тенет
Comment=A keyboard-driven, vim-like browser based on PyQt5 Comment=A keyboard-driven, vim-like browser based on PyQt5
Comment[it]= Un browser web vim-like utilizzabile da tastiera basato su PyQt5
Icon=qutebrowser Icon=qutebrowser
Type=Application Type=Application
Categories=Network;WebBrowser; Categories=Network;WebBrowser;
@ -10,3 +48,128 @@ Terminal=false
StartupNotify=false StartupNotify=false
MimeType=text/html;text/xml;application/xhtml+xml;application/xml;application/rdf+xml;image/gif;image/jpeg;image/png;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/qute; MimeType=text/html;text/xml;application/xhtml+xml;application/xml;application/rdf+xml;image/gif;image/jpeg;image/png;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/qute;
Keywords=Browser Keywords=Browser
[Desktop Action new-window]
Name=New Window
Name[am]=
Name[ar]=
Name[bg]=Нов прозорец
Name[bn]=
Name[ca]=Finestra nova
Name[cs]=Nové okno
Name[da]=Nyt vindue
Name[de]=Neues Fenster
Name[el]=Νέο Παράθυρο
Name[en_GB]=New Window
Name[es]=Nueva ventana
Name[et]=Uus aken
Name[fa]=پ ی
Name[fi]=Uusi ikkuna
Name[fil]=New Window
Name[fr]=Nouvelle fenêtre
Name[gu]= િ
Name[hi]= ि
Name[hr]=Novi prozor
Name[hu]=Új ablak
Name[id]=Jendela Baru
Name[it]=Nuova finestra
Name[iw]=חלון חדש
Name[ja]=
Name[kn]= ಿ
Name[ko]=
Name[lt]=Naujas langas
Name[lv]=Jauns logs
Name[ml]=ി ി<200d>
Name[mr]= ि
Name[nl]=Nieuw venster
Name[no]=Nytt vindu
Name[pl]=Nowe okno
Name[pt]=Nova janela
Name[pt_BR]=Nova janela
Name[ro]=Fereastră nouă
Name[ru]=Новое окно
Name[sk]=Nové okno
Name[sl]=Novo okno
Name[sr]=Нови прозор
Name[sv]=Nytt fönster
Name[sw]=Dirisha Jipya
Name[ta]=ி
Name[te]= ి
Name[th]=
Name[tr]=Yeni Pencere
Name[uk]=Нове вікно
Name[vi]=Ca s Mi
Exec=qutebrowser
[Desktop Action preferences]
Name=Preferences
Name[an]=Preferencias
Name[ar]=
Name[as]=
Name[be]=Настройкі
Name[bg]=Настройки
Name[bn_IN]=
Name[bs]=Postavke
Name[ca]=Preferències
Name[ca@valencia]=Preferències
Name[cs]=Předvolby
Name[da]=Indstillinger
Name[de]=Einstellungen
Name[el]=Προτιμήσεις
Name[en_GB]=Preferences
Name[eo]=Agordoj
Name[es]=Preferencias
Name[et]=Eelistused
Name[eu]=Hobespenak
Name[fa]=ی
Name[fi]=Asetukset
Name[fr]=Préférences
Name[fur]=Preferencis
Name[ga]=Sainroghanna
Name[gd]=Roghainnean
Name[gl]=Preferencias
Name[gu]=
Name[he]=העדפות
Name[hi]=
Name[hr]=Osobitosti
Name[hu]=Beállítások
Name[id]=Preferensi
Name[is]=Kjörstillingar
Name[it]=Preferenze
Name[ja]=
Name[kk]=Баптаулар
Name[km]=
Name[kn]=
Name[ko]=
Name[lt]=Nuostatos
Name[lv]=Iestatījumi
Name[ml]=<200d><200d>
Name[mr]=
Name[nb]=Brukervalg
Name[ne]=ि
Name[nl]=Voorkeuren
Name[oc]=Preferéncias
Name[or]=
Name[pa]=
Name[pl]=Preferencje
Name[pt]=Preferências
Name[pt_BR]=Preferências
Name[ro]=Preferințe
Name[ru]=Параметры
Name[sk]=Nastavenia
Name[sl]=Možnosti
Name[sr]=Поставке
Name[sr@latin]=Postavke
Name[sv]=Inställningar
Name[ta]=ி
Name[te]=
Name[tg]=Хусусиятҳо
Name[th]=
Name[tr]=Tercihler
Name[ug]=
Name[uk]=Параметри
Name[vi]=Tùy thích
Name[zh_CN]=
Name[zh_HK]=
Name[zh_TW]=
Exec=qutebrowser "qute://settings"

View File

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

View File

@ -1,9 +1,9 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
certifi==2018.10.15 certifi==2018.11.29
chardet==3.0.4 chardet==3.0.4
codecov==2.0.15 codecov==2.0.15
coverage==4.5.2 coverage==4.5.2
idna==2.7 idna==2.8
requests==2.20.1 requests==2.21.0
urllib3==1.24.1 urllib3==1.24.1

View File

@ -22,6 +22,6 @@ pep8-naming==0.7.0
pycodestyle==2.4.0 pycodestyle==2.4.0
pydocstyle==3.0.0 pydocstyle==3.0.0
pyflakes==2.0.0 pyflakes==2.0.0
six==1.11.0 six==1.12.0
snowballstemmer==1.2.1 snowballstemmer==1.2.1
typing==3.6.6 typing==3.6.6

View File

@ -0,0 +1,8 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
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.1

View File

@ -0,0 +1,5 @@
mypy
-e git+https://github.com/qutebrowser/PyQt5-stubs.git@wip#egg=PyQt5-stubs
# remove @commit-id for scm installs
#@ replace: @.*# @wip#

View File

@ -0,0 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
colorama==0.4.1
cssutils==1.0.2
hunter==2.1.0
Pympler==0.6
six==1.12.0

View File

@ -0,0 +1,3 @@
hunter
cssutils
pympler

View File

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

View File

@ -1,23 +1,23 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
asn1crypto==0.24.0 asn1crypto==0.24.0
astroid==2.0.4 astroid==2.1.0
certifi==2018.10.15 certifi==2018.11.29
cffi==1.11.5 cffi==1.11.5
chardet==3.0.4 chardet==3.0.4
cryptography==2.4.1 cryptography==2.4.2
github3.py==1.2.0 github3.py==1.2.0
idna==2.7 idna==2.8
isort==4.3.4 isort==4.3.4
jwcrypto==0.6.0 jwcrypto==0.6.0
lazy-object-proxy==1.3.1 lazy-object-proxy==1.3.1
mccabe==0.6.1 mccabe==0.6.1
pycparser==2.19 pycparser==2.19
pylint==2.1.1 pylint==2.2.2
python-dateutil==2.7.5 python-dateutil==2.7.5
./scripts/dev/pylint_checkers ./scripts/dev/pylint_checkers
requests==2.20.1 requests==2.21.0
six==1.11.0 six==1.12.0
uritemplate==3.0.0 uritemplate==3.0.0
urllib3==1.24.1 urllib3==1.24.1
wrapt==1.10.11 wrapt==1.10.11

View File

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

View File

@ -0,0 +1 @@
sphinx

View File

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

View File

@ -1,8 +1,9 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
filelock==3.0.10
pluggy==0.8.0 pluggy==0.8.0
py==1.7.0 py==1.7.0
six==1.11.0 six==1.12.0
toml==0.10.0 toml==0.10.0
tox==3.5.3 tox==3.6.1
virtualenv==16.1.0 virtualenv==16.1.0

View File

@ -53,9 +53,10 @@ The following userscripts can be found on their own repositories.
- [qtb.us](https://github.com/Chinggis6/qtb.us): small pack of userscripts. - [qtb.us](https://github.com/Chinggis6/qtb.us): small pack of userscripts.
- [pinboard.zsh](https://github.com/dmix/pinboard.zsh): Add URL to your - [pinboard.zsh](https://github.com/dmix/pinboard.zsh): Add URL to your
[Pinboard][] bookmark manager. [Pinboard][] bookmark manager.
- [qute-capture](https://github.com/alcah/qute-capture): Capture links with
Emacs's org-mode to a read-later file.
[Zotero]: https://www.zotero.org/ [Zotero]: https://www.zotero.org/
[Pocket]: https://getpocket.com/ [Pocket]: https://getpocket.com/
[Instapaper]: https://www.instapaper.com/ [Instapaper]: https://www.instapaper.com/
[Pinboard]: https://pinboard.in/ [Pinboard]: https://pinboard.in/

87
mypy.ini Normal file
View File

@ -0,0 +1,87 @@
[mypy]
# We also need to support 3.5, but if we'd chose that here, we'd need to deal
# with conditional imports (like secrets.py).
python_version = 3.6
# --strict
warn_redundant_casts = True
warn_unused_ignores = True
disallow_subclassing_any = True
disallow_untyped_decorators = True
## https://github.com/python/mypy/issues/5957
# warn_unused_configs = True
# disallow_untyped_calls = True
# disallow_untyped_defs = True
## https://github.com/python/mypy/issues/5954
# disallow_incomplete_defs = True
# check_untyped_defs = True
# no_implicit_optional = True
# warn_return_any = True
[mypy-colorama]
# https://github.com/tartley/colorama/issues/206
ignore_missing_imports = True
[mypy-hunter]
# https://github.com/ionelmc/python-hunter/issues/43
ignore_missing_imports = True
[mypy-pygments.*]
# https://bitbucket.org/birkenfeld/pygments-main/issues/1485/type-hints
ignore_missing_imports = True
[mypy-cssutils]
# Pretty much inactive currently
ignore_missing_imports = True
[mypy-pypeg2]
# Pretty much inactive currently
ignore_missing_imports = True
[mypy-bdb]
# stdlib, missing in typeshed
ignore_missing_imports = True
[mypy-qutebrowser.browser.webkit.rfc6266]
# subclasses dynamic PyPEG2 classes
disallow_subclassing_any = False
[mypy-qutebrowser.browser.browsertab]
disallow_untyped_defs = True
disallow_incomplete_defs = True
[mypy-qutebrowser.misc.objects]
disallow_untyped_defs = True
disallow_incomplete_defs = True
[mypy-qutebrowser.commands.cmdutils]
disallow_untyped_defs = True
disallow_incomplete_defs = True
[mypy-qutebrowser.config.*]
disallow_untyped_defs = True
disallow_incomplete_defs = True
[mypy-qutebrowser.api.*]
disallow_untyped_defs = True
disallow_incomplete_defs = True
[mypy-qutebrowser.components.*]
disallow_untyped_defs = True
disallow_incomplete_defs = True
[mypy-qutebrowser.extensions.*]
disallow_untyped_defs = True
disallow_incomplete_defs = True
[mypy-qutebrowser.browser.webelem]
disallow_untyped_defs = True
disallow_incomplete_defs = True
[mypy-qutebrowser.browser.webkit.webkitelem]
disallow_untyped_defs = True
disallow_incomplete_defs = True
[mypy-qutebrowser.browser.webengine.webengineelem]
disallow_untyped_defs = True
disallow_incomplete_defs = True

View File

@ -0,0 +1,26 @@
# 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/>.
"""API for extensions.
This API currently isn't exposed to third-party extensions yet, but will be in
the future. Thus, care must be taken when adding new APIs here.
Code in qutebrowser.components only uses this API.
"""

View File

@ -0,0 +1,27 @@
# 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/>.
"""A single tab."""
# pylint: disable=unused-import
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

219
qutebrowser/api/cmdutils.py Normal file
View File

@ -0,0 +1,219 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-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/>.
"""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
from qutebrowser.utils import qtutils
from qutebrowser.commands import command, cmdexc
# pylint: disable=unused-import
from qutebrowser.utils.usertypes import KeyMode, CommandValue as Value
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::
raise cmdexc.CommandError("Message")
The message will then be shown in the qutebrowser status bar.
.. 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.
"""
def check_overflow(arg: int, ctype: str) -> None:
"""Check if the given argument is in bounds for the given type.
Args:
arg: The argument to check.
ctype: The C++/Qt type to check as a string ('int'/'int64').
"""
try:
qtutils.check_overflow(arg, ctype)
except OverflowError:
raise CommandError("Numeric argument is too large for internal {} "
"representation.".format(ctype))
def check_exclusive(flags: typing.Iterable[bool],
names: typing.Iterable[str]) -> None:
"""Check if only one flag is set with exclusive flags.
Raise a CommandError if not.
Args:
flags: The flag values to check.
names: A list of names (corresponding to the flags argument).
"""
if sum(1 for e in flags if e) > 1:
argstr = '/'.join('-' + e for e in names)
raise CommandError("Only one of {} can be given!".format(argstr))
class register: # noqa: N801,N806 pylint: disable=invalid-name
"""Decorator to register a new command handler."""
def __init__(self, *,
instance: str = None,
name: str = None,
**kwargs: typing.Any) -> None:
"""Save decorator arguments.
Gets called on parse-time with the decorator arguments.
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:
"""Register the command before running the function.
Gets called when a function should be decorated.
Doesn't actually decorate anything, but creates a Command object and
registers it in the global commands dict.
Args:
func: The function to be decorated.
Return:
The original function (unmodified).
"""
if self._name is None:
name = func.__name__.lower().replace('_', '-')
else:
assert isinstance(self._name, str), self._name
name = self._name
cmd = command.Command(name=name, instance=self._instance,
handler=func, **self._kwargs)
cmd.register()
return func
class argument: # noqa: N801,N806 pylint: disable=invalid-name
"""Decorator to customize an argument.
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 # The name of the argument to handle.
self._kwargs = kwargs # Valid ArgInfo members.
def __call__(self, func: typing.Callable) -> typing.Callable:
funcname = func.__name__
if self._argname not in inspect.signature(func).parameters:
raise ValueError("{} has no argument {}!".format(funcname,
self._argname))
if not hasattr(func, 'qute_args'):
func.qute_args = {} # type: ignore
elif func.qute_args is None: # type: ignore
raise ValueError("@cmdutils.argument got called above (after) "
"@cmdutils.register for {}!".format(funcname))
arginfo = command.ArgInfo(**self._kwargs)
func.qute_args[self._argname] = arginfo # type: ignore
return func

43
qutebrowser/api/config.py Normal file
View File

@ -0,0 +1,43 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Access to the qutebrowser configuration."""
import typing
from PyQt5.QtCore import QUrl
from qutebrowser.config import config
#: Simplified access to config values using attribute acccess.
#: For example, to access the ``content.javascript.enabled`` setting,
#: you can do::
#:
#: if config.val.content.javascript.enabled:
#: ...
#:
#: This also supports setting configuration values::
#:
#: config.val.content.javascript.enabled = False
val = typing.cast('config.ConfigContainer', None)
def get(name: str, url: QUrl = None) -> typing.Any:
"""Get a value from the config based on a string name."""
return config.instance.get(name, url)

View File

@ -0,0 +1,75 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""APIs related to downloading files."""
import io
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, QUrl
from qutebrowser.browser import downloads, qtnetworkdownloads
from qutebrowser.utils import objreg
class TempDownload(QObject):
"""A download of some data into a file object."""
finished = pyqtSignal()
def __init__(self, item: qtnetworkdownloads.DownloadItem) -> None:
super().__init__()
self._item = item
self._item.finished.connect(self._on_download_finished)
self.successful = False
self.fileobj = item.fileobj
@pyqtSlot()
def _on_download_finished(self) -> None:
self.successful = self._item.successful
self.finished.emit()
def download_temp(url: QUrl) -> TempDownload:
"""Download the given URL into a file object.
The download is not saved to disk.
Returns a ``TempDownload`` object, which triggers a ``finished`` signal
when the download has finished::
dl = downloads.download_temp(QUrl("https://www.example.com/"))
dl.finished.connect(functools.partial(on_download_finished, dl))
After the download has finished, its ``successful`` attribute can be
checked to make sure it finished successfully. If so, its contents can be
read by accessing the ``fileobj`` attribute::
def on_download_finished(download: downloads.TempDownload) -> None:
if download.successful:
print(download.fileobj.read())
download.fileobj.close()
"""
fobj = io.BytesIO()
fobj.name = 'temporary: ' + url.host()
target = downloads.FileObjDownloadTarget(fobj)
download_manager = objreg.get('qtnetwork-download-manager')
return download_manager.get(url, target=target, auto_remove=True)

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

@ -0,0 +1,92 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=invalid-name
"""Hooks for extensions."""
import importlib
import typing
from qutebrowser.extensions import loader
def _add_module_info(func: typing.Callable) -> loader.ModuleInfo:
"""Add module info to the given function."""
module = importlib.import_module(func.__module__)
return loader.add_module_info(module)
class init:
"""Decorator to mark a function to run when initializing.
The decorated function gets called with a
:class:`qutebrowser.api.apitypes.InitContext` as argument.
Example::
@hook.init()
def init(_context):
message.info("Extension initialized.")
"""
def __call__(self, func: typing.Callable) -> typing.Callable:
info = _add_module_info(func)
if info.init_hook is not None:
raise ValueError("init hook is already registered!")
info.init_hook = func
return func
class config_changed:
"""Decorator to get notified about changed configs.
By default, the decorated function is called when any change in the config
occurs::
@hook.config_changed()
def on_config_changed():
...
When an option name is passed, it's only called when the given option was
changed::
@hook.config_changed('content.javascript.enabled')
def on_config_changed():
...
Alternatively, a part of an option name can be specified. In the following
snippet, ``on_config_changed`` gets called when either
``bindings.commands`` or ``bindings.key_mappings`` have changed::
@hook.config_changed('bindings')
def on_config_changed():
...
"""
def __init__(self, option_filter: str = None) -> None:
self._filter = option_filter
def __call__(self, func: typing.Callable) -> typing.Callable:
info = _add_module_info(func)
info.config_changed_hooks.append((self._filter, func))
return func

View File

@ -0,0 +1,43 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""APIs related to intercepting/blocking requests."""
from qutebrowser.extensions import interceptors
# pylint: disable=unused-import
from qutebrowser.extensions.interceptors import Request
#: Type annotation for an interceptor function.
InterceptorType = interceptors.InterceptorType
def register(interceptor: InterceptorType) -> None:
"""Register a request interceptor.
Whenever a request happens, the interceptor gets called with a
:class:`Request` object.
Example::
def intercept(request: interceptor.Request) -> None:
if request.request_url.host() == 'badhost.example.com':
request.block()
"""
interceptors.register(interceptor)

View File

@ -0,0 +1,23 @@
# 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/>.
"""Utilities to display messages above the status bar."""
# pylint: disable=unused-import
from qutebrowser.utils.message import error, warning, info

View File

@ -60,13 +60,15 @@ except ImportError:
import qutebrowser import qutebrowser
import qutebrowser.resources import qutebrowser.resources
from qutebrowser.completion.models import miscmodels from qutebrowser.completion.models import miscmodels
from qutebrowser.commands import cmdutils, runners, cmdexc from qutebrowser.commands import runners
from qutebrowser.api import cmdutils
from qutebrowser.config import config, websettings, configfiles, configinit from qutebrowser.config import config, websettings, configfiles, configinit
from qutebrowser.browser import (urlmarks, adblock, history, browsertab, from qutebrowser.browser import (urlmarks, history, browsertab,
qtnetworkdownloads, downloads, greasemonkey) qtnetworkdownloads, downloads, greasemonkey)
from qutebrowser.browser.network import proxy from qutebrowser.browser.network import proxy
from qutebrowser.browser.webkit import cookies, cache from qutebrowser.browser.webkit import cookies, cache
from qutebrowser.browser.webkit.network import networkmanager from qutebrowser.browser.webkit.network import networkmanager
from qutebrowser.extensions import loader
from qutebrowser.keyinput import macros from qutebrowser.keyinput import macros
from qutebrowser.mainwindow import mainwindow, prompt from qutebrowser.mainwindow import mainwindow, prompt
from qutebrowser.misc import (readline, ipc, savemanager, sessions, from qutebrowser.misc import (readline, ipc, savemanager, sessions,
@ -163,6 +165,8 @@ def init(args, crash_handler):
qApp.setQuitOnLastWindowClosed(False) qApp.setQuitOnLastWindowClosed(False)
_init_icon() _init_icon()
loader.init()
loader.load_components()
try: try:
_init_modules(args, crash_handler) _init_modules(args, crash_handler)
except (OSError, UnicodeDecodeError, browsertab.WebTabError) as e: except (OSError, UnicodeDecodeError, browsertab.WebTabError) as e:
@ -193,7 +197,7 @@ def _init_icon():
icon = QIcon() icon = QIcon()
fallback_icon = QIcon() fallback_icon = QIcon()
for size in [16, 24, 32, 48, 64, 96, 128, 256, 512]: for size in [16, 24, 32, 48, 64, 96, 128, 256, 512]:
filename = ':/icons/qutebrowser-{}x{}.png'.format(size, size) filename = ':/icons/qutebrowser-{size}x{size}.png'.format(size=size)
pixmap = QPixmap(filename) pixmap = QPixmap(filename)
if pixmap.isNull(): if pixmap.isNull():
log.init.warning("Failed to load {}".format(filename)) log.init.warning("Failed to load {}".format(filename))
@ -303,10 +307,10 @@ def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None):
def open_url(url, target=None, no_raise=False, via_ipc=True): def open_url(url, target=None, no_raise=False, via_ipc=True):
"""Open an URL in new window/tab. """Open a URL in new window/tab.
Args: Args:
url: An URL to open. url: A URL to open.
target: same as new_instance_open_target (used as a default). target: same as new_instance_open_target (used as a default).
no_raise: suppress target window raising. no_raise: suppress target window raising.
via_ipc: Whether the arguments were transmitted over IPC. via_ipc: Whether the arguments were transmitted over IPC.
@ -465,11 +469,6 @@ def _init_modules(args, crash_handler):
log.init.debug("Initializing websettings...") log.init.debug("Initializing websettings...")
websettings.init(args) websettings.init(args)
log.init.debug("Initializing adblock...")
host_blocker = adblock.HostBlocker()
host_blocker.read_hosts()
objreg.register('host-blocker', host_blocker)
log.init.debug("Initializing quickmarks...") log.init.debug("Initializing quickmarks...")
quickmark_manager = urlmarks.QuickmarkManager(qApp) quickmark_manager = urlmarks.QuickmarkManager(qApp)
objreg.register('quickmark-manager', quickmark_manager) objreg.register('quickmark-manager', quickmark_manager)
@ -619,10 +618,11 @@ class Quitter:
ok = self.restart(session='_restart') ok = self.restart(session='_restart')
except sessions.SessionError as e: except sessions.SessionError as e:
log.destroy.exception("Failed to save session!") log.destroy.exception("Failed to save session!")
raise cmdexc.CommandError("Failed to save session: {}!".format(e)) raise cmdutils.CommandError("Failed to save session: {}!"
.format(e))
except SyntaxError as e: except SyntaxError as e:
log.destroy.exception("Got SyntaxError") log.destroy.exception("Got SyntaxError")
raise cmdexc.CommandError("SyntaxError in {}:{}: {}".format( raise cmdutils.CommandError("SyntaxError in {}:{}: {}".format(
e.filename, e.lineno, e)) e.filename, e.lineno, e))
if ok: if ok:
self.shutdown(restart=True) self.shutdown(restart=True)
@ -684,7 +684,7 @@ class Quitter:
session: The name of the session to save. session: The name of the session to save.
""" """
if session is not None and not save: if session is not None and not save:
raise cmdexc.CommandError("Session name given without --save!") raise cmdutils.CommandError("Session name given without --save!")
if save: if save:
if session is None: if session is None:
session = sessions.default session = sessions.default

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -33,14 +33,18 @@ from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QObject, QModelIndex,
QTimer, QAbstractListModel, QUrl) QTimer, QAbstractListModel, QUrl)
from qutebrowser.browser import pdfjs from qutebrowser.browser import pdfjs
from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.api import cmdutils
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import (usertypes, standarddir, utils, message, log, from qutebrowser.utils import (usertypes, standarddir, utils, message, log,
qtutils, objreg) qtutils, objreg)
from qutebrowser.qt import sip from qutebrowser.qt import sip
ModelRole = enum.IntEnum('ModelRole', ['item'], start=Qt.UserRole) class ModelRole(enum.IntEnum):
"""Custom download model roles."""
item = Qt.UserRole
# Remember the last used directory # Remember the last used directory
@ -60,8 +64,6 @@ class UnsupportedAttribute:
supported with QtWebengine. supported with QtWebengine.
""" """
pass
class UnsupportedOperationError(Exception): class UnsupportedOperationError(Exception):
@ -1007,11 +1009,11 @@ class DownloadModel(QAbstractListModel):
count: The index of the download count: The index of the download
""" """
if not count: if not count:
raise cmdexc.CommandError("There's no download!") raise cmdutils.CommandError("There's no download!")
raise cmdexc.CommandError("There's no download {}!".format(count)) raise cmdutils.CommandError("There's no download {}!".format(count))
@cmdutils.register(instance='download-model', scope='window') @cmdutils.register(instance='download-model', scope='window')
@cmdutils.argument('count', count=True) @cmdutils.argument('count', value=cmdutils.Value.count)
def download_cancel(self, all_=False, count=0): def download_cancel(self, all_=False, count=0):
"""Cancel the last/[count]th download. """Cancel the last/[count]th download.
@ -1032,12 +1034,12 @@ class DownloadModel(QAbstractListModel):
if download.done: if download.done:
if not count: if not count:
count = len(self) count = len(self)
raise cmdexc.CommandError("Download {} is already done!" raise cmdutils.CommandError("Download {} is already done!"
.format(count)) .format(count))
download.cancel() download.cancel()
@cmdutils.register(instance='download-model', scope='window') @cmdutils.register(instance='download-model', scope='window')
@cmdutils.argument('count', count=True) @cmdutils.argument('count', value=cmdutils.Value.count)
def download_delete(self, count=0): def download_delete(self, count=0):
"""Delete the last/[count]th download from disk. """Delete the last/[count]th download from disk.
@ -1051,14 +1053,15 @@ class DownloadModel(QAbstractListModel):
if not download.successful: if not download.successful:
if not count: if not count:
count = len(self) count = len(self)
raise cmdexc.CommandError("Download {} is not done!".format(count)) raise cmdutils.CommandError("Download {} is not done!"
.format(count))
download.delete() download.delete()
download.remove() download.remove()
log.downloads.debug("deleted download {}".format(download)) log.downloads.debug("deleted download {}".format(download))
@cmdutils.register(instance='download-model', scope='window', maxsplit=0) @cmdutils.register(instance='download-model', scope='window', maxsplit=0)
@cmdutils.argument('count', count=True) @cmdutils.argument('count', value=cmdutils.Value.count)
def download_open(self, cmdline: str = None, count=0): def download_open(self, cmdline: str = None, count: int = 0) -> None:
"""Open the last/[count]th download. """Open the last/[count]th download.
If no specific command is given, this will use the system's default If no specific command is given, this will use the system's default
@ -1078,11 +1081,12 @@ class DownloadModel(QAbstractListModel):
if not download.successful: if not download.successful:
if not count: if not count:
count = len(self) count = len(self)
raise cmdexc.CommandError("Download {} is not done!".format(count)) raise cmdutils.CommandError("Download {} is not done!"
.format(count))
download.open_file(cmdline) download.open_file(cmdline)
@cmdutils.register(instance='download-model', scope='window') @cmdutils.register(instance='download-model', scope='window')
@cmdutils.argument('count', count=True) @cmdutils.argument('count', value=cmdutils.Value.count)
def download_retry(self, count=0): def download_retry(self, count=0):
"""Retry the first failed/[count]th download. """Retry the first failed/[count]th download.
@ -1095,12 +1099,12 @@ class DownloadModel(QAbstractListModel):
except IndexError: except IndexError:
self._raise_no_download(count) self._raise_no_download(count)
if download.successful or not download.done: if download.successful or not download.done:
raise cmdexc.CommandError("Download {} did not fail!".format( raise cmdutils.CommandError("Download {} did not fail!"
count)) .format(count))
else: else:
to_retry = [d for d in self if d.done and not d.successful] to_retry = [d for d in self if d.done and not d.successful]
if not to_retry: if not to_retry:
raise cmdexc.CommandError("No failed downloads!") raise cmdutils.CommandError("No failed downloads!")
else: else:
download = to_retry[0] download = to_retry[0]
download.try_retry() download.try_retry()
@ -1117,7 +1121,7 @@ class DownloadModel(QAbstractListModel):
download.remove() download.remove()
@cmdutils.register(instance='download-model', scope='window') @cmdutils.register(instance='download-model', scope='window')
@cmdutils.argument('count', count=True) @cmdutils.argument('count', value=cmdutils.Value.count)
def download_remove(self, all_=False, count=0): def download_remove(self, all_=False, count=0):
"""Remove the last/[count]th download from the list. """Remove the last/[count]th download from the list.
@ -1135,7 +1139,7 @@ class DownloadModel(QAbstractListModel):
if not download.done: if not download.done:
if not count: if not count:
count = len(self) count = len(self)
raise cmdexc.CommandError("Download {} is not done!" raise cmdutils.CommandError("Download {} is not done!"
.format(count)) .format(count))
download.remove() download.remove()

View File

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

View File

@ -32,7 +32,7 @@ from PyQt5.QtCore import pyqtSignal, QObject, QUrl
from qutebrowser.utils import (log, standarddir, jinja, objreg, utils, from qutebrowser.utils import (log, standarddir, jinja, objreg, utils,
javascript, urlmatch, version, usertypes) javascript, urlmatch, version, usertypes)
from qutebrowser.commands import cmdutils from qutebrowser.api import cmdutils
from qutebrowser.browser import downloads from qutebrowser.browser import downloads
from qutebrowser.misc import objects from qutebrowser.misc import objects

View File

@ -34,7 +34,8 @@ from PyQt5.QtWidgets import QLabel
from qutebrowser.config import config, configexc from qutebrowser.config import config, configexc
from qutebrowser.keyinput import modeman, modeparsers from qutebrowser.keyinput import modeman, modeparsers
from qutebrowser.browser import webelem from qutebrowser.browser import webelem
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners from qutebrowser.commands import userscripts, runners
from qutebrowser.api import cmdutils
from qutebrowser.utils import usertypes, log, qtutils, message, objreg, utils from qutebrowser.utils import usertypes, log, qtutils, message, objreg, utils
@ -217,9 +218,7 @@ class HintActions:
if context.target in [Target.normal, Target.current]: if context.target in [Target.normal, Target.current]:
# Set the pre-jump mark ', so we can jump back here after following # Set the pre-jump mark ', so we can jump back here after following
tabbed_browser = objreg.get('tabbed-browser', scope='window', context.tab.scroller.before_jump_requested.emit()
window=self._win_id)
tabbed_browser.set_mark("'")
try: try:
if context.target == Target.hover: if context.target == Target.hover:
@ -304,8 +303,8 @@ class HintActions:
raise HintingError("No suitable link found for this element.") raise HintingError("No suitable link found for this element.")
prompt = False if context.rapid else None prompt = False if context.rapid else None
qnam = context.tab.networkaccessmanager() qnam = context.tab.private_api.networkaccessmanager()
user_agent = context.tab.user_agent() user_agent = context.tab.private_api.user_agent()
# FIXME:qtwebengine do this with QtWebEngine downloads? # FIXME:qtwebengine do this with QtWebEngine downloads?
download_manager = objreg.get('qtnetwork-download-manager') download_manager = objreg.get('qtnetwork-download-manager')
@ -563,12 +562,12 @@ class HintManager(QObject):
if target in [Target.userscript, Target.spawn, Target.run, if target in [Target.userscript, Target.spawn, Target.run,
Target.fill]: Target.fill]:
if not args: if not args:
raise cmdexc.CommandError( raise cmdutils.CommandError(
"'args' is required with target userscript/spawn/run/" "'args' is required with target userscript/spawn/run/"
"fill.") "fill.")
else: else:
if args: if args:
raise cmdexc.CommandError( raise cmdutils.CommandError(
"'args' is only allowed with target userscript/spawn.") "'args' is only allowed with target userscript/spawn.")
def _filter_matches(self, filterstr, elemstr): def _filter_matches(self, filterstr, elemstr):
@ -596,13 +595,6 @@ class HintManager(QObject):
log.hints.debug("In _start_cb without context!") log.hints.debug("In _start_cb without context!")
return return
if elems is None:
message.error("Unknown error while getting hint elements.")
return
elif isinstance(elems, webelem.Error):
message.error(str(elems))
return
if not elems: if not elems:
message.error("No elements found.") message.error("No elements found.")
return return
@ -705,7 +697,7 @@ class HintManager(QObject):
window=self._win_id) window=self._win_id)
tab = tabbed_browser.widget.currentWidget() tab = tabbed_browser.widget.currentWidget()
if tab is None: if tab is None:
raise cmdexc.CommandError("No WebView available yet!") raise cmdutils.CommandError("No WebView available yet!")
mode_manager = objreg.get('mode-manager', scope='window', mode_manager = objreg.get('mode-manager', scope='window',
window=self._win_id) window=self._win_id)
@ -722,8 +714,8 @@ class HintManager(QObject):
pass pass
else: else:
name = target.name.replace('_', '-') name = target.name.replace('_', '-')
raise cmdexc.CommandError("Rapid hinting makes no sense with " raise cmdutils.CommandError("Rapid hinting makes no sense "
"target {}!".format(name)) "with target {}!".format(name))
self._check_args(target, *args) self._check_args(target, *args)
self._context = HintContext() self._context = HintContext()
@ -736,17 +728,20 @@ class HintManager(QObject):
try: try:
self._context.baseurl = tabbed_browser.current_url() self._context.baseurl = tabbed_browser.current_url()
except qtutils.QtValueError: except qtutils.QtValueError:
raise cmdexc.CommandError("No URL set for this page yet!") raise cmdutils.CommandError("No URL set for this page yet!")
self._context.args = args self._context.args = list(args)
self._context.group = group self._context.group = group
try: try:
selector = webelem.css_selector(self._context.group, selector = webelem.css_selector(self._context.group,
self._context.baseurl) self._context.baseurl)
except webelem.Error as e: except webelem.Error as e:
raise cmdexc.CommandError(str(e)) raise cmdutils.CommandError(str(e))
self._context.tab.elements.find_css(selector, self._start_cb, self._context.tab.elements.find_css(
selector,
callback=self._start_cb,
error_cb=lambda err: message.error(str(err)),
only_visible=True) only_visible=True)
def _get_hint_mode(self, mode): def _get_hint_mode(self, mode):
@ -758,7 +753,7 @@ class HintManager(QObject):
try: try:
opt.typ.to_py(mode) opt.typ.to_py(mode)
except configexc.ValidationError as e: except configexc.ValidationError as e:
raise cmdexc.CommandError("Invalid mode: {}".format(e)) raise cmdutils.CommandError("Invalid mode: {}".format(e))
return mode return mode
def current_mode(self): def current_mode(self):
@ -960,13 +955,13 @@ class HintManager(QObject):
""" """
if keystring is None: if keystring is None:
if self._context.to_follow is None: if self._context.to_follow is None:
raise cmdexc.CommandError("No hint to follow") raise cmdutils.CommandError("No hint to follow")
elif select: elif select:
raise cmdexc.CommandError("Can't use --select without hint.") raise cmdutils.CommandError("Can't use --select without hint.")
else: else:
keystring = self._context.to_follow keystring = self._context.to_follow
elif keystring not in self._context.labels: elif keystring not in self._context.labels:
raise cmdexc.CommandError("No hint {}!".format(keystring)) raise cmdutils.CommandError("No hint {}!".format(keystring))
if select: if select:
self.handle_partial_key(keystring) self.handle_partial_key(keystring)

View File

@ -27,7 +27,7 @@ from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal
from PyQt5.QtWidgets import QProgressDialog, QApplication from PyQt5.QtWidgets import QProgressDialog, QApplication
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.commands import cmdutils, cmdexc from qutebrowser.api import cmdutils
from qutebrowser.utils import utils, objreg, log, usertypes, message, qtutils from qutebrowser.utils import utils, objreg, log, usertypes, message, qtutils
from qutebrowser.misc import objects, sql from qutebrowser.misc import objects, sql
@ -365,7 +365,8 @@ class WebHistory(sql.SqlTable):
f.write('\n'.join(lines)) f.write('\n'.join(lines))
message.info("Dumped history to {}".format(dest)) message.info("Dumped history to {}".format(dest))
except OSError as e: except OSError as e:
raise cmdexc.CommandError('Could not write history: {}'.format(e)) raise cmdutils.CommandError('Could not write history: {}'
.format(e))
def init(parent=None): def init(parent=None):

View File

@ -49,8 +49,6 @@ class WebInspectorError(Exception):
"""Raised when the inspector could not be initialized.""" """Raised when the inspector could not be initialized."""
pass
class AbstractWebInspector(QWidget): class AbstractWebInspector(QWidget):

View File

@ -240,7 +240,7 @@ class MouseEventFilter(QObject):
evtype = event.type() evtype = event.type()
if evtype not in self._handlers: if evtype not in self._handlers:
return False return False
if obj is not self._tab.event_target(): if obj is not self._tab.private_api.event_target():
log.mouse.debug("Ignoring {} to {}".format( log.mouse.debug("Ignoring {} to {}".format(
event.__class__.__name__, obj)) event.__class__.__name__, obj))
return False return False

View File

@ -116,13 +116,6 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False,
window: True to open in a new window, False for the current one. window: True to open in a new window, False for the current one.
""" """
def _prevnext_cb(elems): def _prevnext_cb(elems):
if elems is None:
message.error("Unknown error while getting hint elements")
return
elif isinstance(elems, webelem.Error):
message.error(str(elems))
return
elem = _find_prevnext(prev, elems) elem = _find_prevnext(prev, elems)
word = 'prev' if prev else 'forward' word = 'prev' if prev else 'forward'
@ -140,7 +133,7 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False,
if window: if window:
new_window = mainwindow.MainWindow( new_window = mainwindow.MainWindow(
private=cur_tabbed_browser.private) private=cur_tabbed_browser.is_private)
new_window.show() new_window.show()
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=new_window.win_id) window=new_window.win_id)
@ -148,11 +141,12 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False,
elif tab: elif tab:
cur_tabbed_browser.tabopen(url, background=background) cur_tabbed_browser.tabopen(url, background=background)
else: else:
browsertab.openurl(url) browsertab.load_url(url)
try: try:
link_selector = webelem.css_selector('links', baseurl) link_selector = webelem.css_selector('links', baseurl)
except webelem.Error as e: except webelem.Error as e:
raise Error(str(e)) raise Error(str(e))
browsertab.elements.find_css(link_selector, _prevnext_cb) browsertab.elements.find_css(link_selector, callback=_prevnext_cb,
error_cb=lambda err: message.error(str(err)))

View File

@ -35,15 +35,11 @@ class ParseProxyError(Exception):
"""Error while parsing PAC result string.""" """Error while parsing PAC result string."""
pass
class EvalProxyError(Exception): class EvalProxyError(Exception):
"""Error while evaluating PAC script.""" """Error while evaluating PAC script."""
pass
def _js_slot(*args): def _js_slot(*args):
"""Wrap a methods as a JavaScript function. """Wrap a methods as a JavaScript function.

View File

@ -37,7 +37,7 @@ try:
import secrets import secrets
except ImportError: except ImportError:
# New in Python 3.6 # New in Python 3.6
secrets = None secrets = None # type: ignore
from PyQt5.QtCore import QUrlQuery, QUrl, qVersion from PyQt5.QtCore import QUrlQuery, QUrl, qVersion
@ -61,36 +61,26 @@ class Error(Exception):
"""Exception for generic errors on a qute:// page.""" """Exception for generic errors on a qute:// page."""
pass
class NotFoundError(Error): class NotFoundError(Error):
"""Raised when the given URL was not found.""" """Raised when the given URL was not found."""
pass
class SchemeOSError(Error): class SchemeOSError(Error):
"""Raised when there was an OSError inside a handler.""" """Raised when there was an OSError inside a handler."""
pass
class UrlInvalidError(Error): class UrlInvalidError(Error):
"""Raised when an invalid URL was opened.""" """Raised when an invalid URL was opened."""
pass
class RequestDeniedError(Error): class RequestDeniedError(Error):
"""Raised when the request is forbidden.""" """Raised when the request is forbidden."""
pass
class Redirect(Exception): class Redirect(Exception):

View File

@ -262,7 +262,7 @@ def get_tab(win_id, target):
elif target == usertypes.ClickTarget.window: elif target == usertypes.ClickTarget.window:
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id) window=win_id)
window = mainwindow.MainWindow(private=tabbed_browser.private) window = mainwindow.MainWindow(private=tabbed_browser.is_private)
window.show() window.show()
win_id = window.win_id win_id = window.win_id
bg_tab = False bg_tab = False

View File

@ -35,7 +35,7 @@ from PyQt5.QtCore import pyqtSignal, QUrl, QObject
from qutebrowser.utils import (message, usertypes, qtutils, urlutils, from qutebrowser.utils import (message, usertypes, qtutils, urlutils,
standarddir, objreg, log) standarddir, objreg, log)
from qutebrowser.commands import cmdutils from qutebrowser.api import cmdutils
from qutebrowser.misc import lineparser from qutebrowser.misc import lineparser
@ -43,29 +43,21 @@ class Error(Exception):
"""Base class for all errors in this module.""" """Base class for all errors in this module."""
pass
class InvalidUrlError(Error): class InvalidUrlError(Error):
"""Exception emitted when a URL is invalid.""" """Exception emitted when a URL is invalid."""
pass
class DoesNotExistError(Error): class DoesNotExistError(Error):
"""Exception emitted when a given URL does not exist.""" """Exception emitted when a given URL does not exist."""
pass
class AlreadyExistsError(Error): class AlreadyExistsError(Error):
"""Exception emitted when a given URL does already exist.""" """Exception emitted when a given URL does already exist."""
pass
class UrlMarkManager(QObject): class UrlMarkManager(QObject):
@ -174,7 +166,7 @@ class QuickmarkManager(UrlMarkManager):
url: The url to add as quickmark. url: The url to add as quickmark.
name: The name for the new quickmark. name: The name for the new quickmark.
""" """
# We don't raise cmdexc.CommandError here as this can be called async # We don't raise cmdutils.CommandError here as this can be called async
# via prompt_save. # via prompt_save.
if not name: if not name:
message.error("Can't set mark with empty name!") message.error("Can't set mark with empty name!")

View File

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

View File

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

View File

@ -117,8 +117,7 @@ class DownloadItem(downloads.AbstractDownloadItem):
def _get_open_filename(self): def _get_open_filename(self):
return self._filename return self._filename
def _set_fileobj(self, fileobj, *, def _set_fileobj(self, fileobj, *, autoclose=True):
autoclose=True): # pylint: disable=unused-argument
raise downloads.UnsupportedOperationError raise downloads.UnsupportedOperationError
def _set_tempfile(self, fileobj): def _set_tempfile(self, fileobj):

View File

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

View File

@ -24,9 +24,9 @@ import functools
import re import re
import html as html_utils import html as html_utils
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QEvent, QPoint, QPointF, from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QUrl,
QUrl, QTimer, QObject) QTimer, QObject)
from PyQt5.QtGui import QKeyEvent, QIcon from PyQt5.QtGui import QIcon
from PyQt5.QtNetwork import QAuthenticator from PyQt5.QtNetwork import QAuthenticator
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript
@ -60,10 +60,8 @@ def init():
_qute_scheme_handler.install(webenginesettings.private_profile) _qute_scheme_handler.install(webenginesettings.private_profile)
log.init.debug("Initializing request interceptor...") log.init.debug("Initializing request interceptor...")
host_blocker = objreg.get('host-blocker')
args = objreg.get('args') args = objreg.get('args')
req_interceptor = interceptor.RequestInterceptor( req_interceptor = interceptor.RequestInterceptor(args=args, parent=app)
host_blocker, args=args, parent=app)
req_interceptor.install(webenginesettings.default_profile) req_interceptor.install(webenginesettings.default_profile)
req_interceptor.install(webenginesettings.private_profile) req_interceptor.install(webenginesettings.private_profile)
@ -132,7 +130,7 @@ class WebEnginePrinting(browsertab.AbstractPrinting):
"""QtWebEngine implementations related to printing.""" """QtWebEngine implementations related to printing."""
def check_pdf_support(self): def check_pdf_support(self):
return True pass
def check_printer_support(self): def check_printer_support(self):
if not hasattr(self._widget.page(), 'print'): if not hasattr(self._widget.page(), 'print'):
@ -205,8 +203,8 @@ class WebEngineSearch(browsertab.AbstractSearch):
self._widget.findText(text, flags, wrapped_callback) self._widget.findText(text, flags, wrapped_callback)
def search(self, text, *, ignore_case='never', reverse=False, def search(self, text, *, ignore_case=usertypes.IgnoreCase.never,
result_cb=None): reverse=False, result_cb=None):
# Don't go to next entry on duplicate search # Don't go to next entry on duplicate search
if self.text == text and self.search_displayed: if self.text == text and self.search_displayed:
log.webview.debug("Ignoring duplicate search request" log.webview.debug("Ignoring duplicate search request"
@ -423,7 +421,7 @@ class WebEngineScroller(browsertab.AbstractScroller):
def _repeated_key_press(self, key, count=1, modifier=Qt.NoModifier): def _repeated_key_press(self, key, count=1, modifier=Qt.NoModifier):
"""Send count fake key presses to this scroller's WebEngineTab.""" """Send count fake key presses to this scroller's WebEngineTab."""
for _ in range(min(count, 1000)): for _ in range(min(count, 1000)):
self._tab.key_press(key, modifier) self._tab.fake_key_press(key, modifier)
@pyqtSlot(QPointF) @pyqtSlot(QPointF)
def _update_pos(self, pos): def _update_pos(self, pos):
@ -478,7 +476,7 @@ class WebEngineScroller(browsertab.AbstractScroller):
def to_anchor(self, name): def to_anchor(self, name):
url = self._tab.url() url = self._tab.url()
url.setFragment(name) url.setFragment(name)
self._tab.openurl(url) self._tab.load_url(url)
def delta(self, x=0, y=0): def delta(self, x=0, y=0):
self._tab.run_js_async(javascript.assemble('window', 'scrollBy', x, y)) self._tab.run_js_async(javascript.assemble('window', 'scrollBy', x, y))
@ -500,10 +498,10 @@ class WebEngineScroller(browsertab.AbstractScroller):
self._repeated_key_press(Qt.Key_Right, count) self._repeated_key_press(Qt.Key_Right, count)
def top(self): def top(self):
self._tab.key_press(Qt.Key_Home) self._tab.fake_key_press(Qt.Key_Home)
def bottom(self): def bottom(self):
self._tab.key_press(Qt.Key_End) self._tab.fake_key_press(Qt.Key_End)
def page_up(self, count=1): def page_up(self, count=1):
self._repeated_key_press(Qt.Key_PageUp, count) self._repeated_key_press(Qt.Key_PageUp, count)
@ -518,25 +516,9 @@ class WebEngineScroller(browsertab.AbstractScroller):
return self._at_bottom return self._at_bottom
class WebEngineHistory(browsertab.AbstractHistory): class WebEngineHistoryPrivate(browsertab.AbstractHistoryPrivate):
"""QtWebEngine implementations related to page history.""" """History-related methods which are not part of the extension API."""
def current_idx(self):
return self._history.currentItemIndex()
def can_go_back(self):
return self._history.canGoBack()
def can_go_forward(self):
return self._history.canGoForward()
def _item_at(self, i):
return self._history.itemAt(i)
def _go_to_item(self, item):
self._tab.predicted_navigation.emit(item.url())
self._history.goToItem(item)
def serialize(self): def serialize(self):
if not qtutils.version_check('5.9', compiled=False): if not qtutils.version_check('5.9', compiled=False):
@ -551,11 +533,11 @@ class WebEngineHistory(browsertab.AbstractHistory):
return qtutils.serialize(self._history) return qtutils.serialize(self._history)
def deserialize(self, data): def deserialize(self, data):
return qtutils.deserialize(data, self._history) qtutils.deserialize(data, self._history)
def load_items(self, items): def load_items(self, items):
if items: if items:
self._tab.predicted_navigation.emit(items[-1].url) self._tab.before_load_started.emit(items[-1].url)
stream, _data, cur_data = tabhistory.serialize(items) stream, _data, cur_data = tabhistory.serialize(items)
qtutils.deserialize_stream(stream, self._history) qtutils.deserialize_stream(stream, self._history)
@ -573,6 +555,37 @@ class WebEngineHistory(browsertab.AbstractHistory):
self._tab.load_finished.connect(_on_load_finished) self._tab.load_finished.connect(_on_load_finished)
class WebEngineHistory(browsertab.AbstractHistory):
"""QtWebEngine implementations related to page history."""
def __init__(self, tab):
super().__init__(tab)
self.private_api = WebEngineHistoryPrivate(tab)
def __len__(self):
return len(self._history)
def __iter__(self):
return iter(self._history.items())
def current_idx(self):
return self._history.currentItemIndex()
def can_go_back(self):
return self._history.canGoBack()
def can_go_forward(self):
return self._history.canGoForward()
def _item_at(self, i):
return self._history.itemAt(i)
def _go_to_item(self, item):
self._tab.before_load_started.emit(item.url())
self._history.goToItem(item)
class WebEngineZoom(browsertab.AbstractZoom): class WebEngineZoom(browsertab.AbstractZoom):
"""QtWebEngine implementations related to zooming.""" """QtWebEngine implementations related to zooming."""
@ -585,19 +598,20 @@ class WebEngineElements(browsertab.AbstractElements):
"""QtWebEngine implemementations related to elements on the page.""" """QtWebEngine implemementations related to elements on the page."""
def _js_cb_multiple(self, callback, js_elems): def _js_cb_multiple(self, callback, error_cb, js_elems):
"""Handle found elements coming from JS and call the real callback. """Handle found elements coming from JS and call the real callback.
Args: Args:
callback: The callback to call with the found elements. callback: The callback to call with the found elements.
Called with None if there was an error. error_cb: The callback to call in case of an error.
js_elems: The elements serialized from javascript. js_elems: The elements serialized from javascript.
""" """
if js_elems is None: if js_elems is None:
callback(None) error_cb(webelem.Error("Unknown error while getting "
"elements"))
return return
elif not js_elems['success']: elif not js_elems['success']:
callback(webelem.Error(js_elems['error'])) error_cb(webelem.Error(js_elems['error']))
return return
elems = [] elems = []
@ -624,10 +638,11 @@ class WebEngineElements(browsertab.AbstractElements):
elem = webengineelem.WebEngineElement(js_elem, tab=self._tab) elem = webengineelem.WebEngineElement(js_elem, tab=self._tab)
callback(elem) callback(elem)
def find_css(self, selector, callback, *, only_visible=False): def find_css(self, selector, callback, error_cb, *,
only_visible=False):
js_code = javascript.assemble('webelem', 'find_css', selector, js_code = javascript.assemble('webelem', 'find_css', selector,
only_visible) only_visible)
js_cb = functools.partial(self._js_cb_multiple, callback) js_cb = functools.partial(self._js_cb_multiple, callback, error_cb)
self._tab.run_js_async(js_code, js_cb) self._tab.run_js_async(js_code, js_cb)
def find_id(self, elem_id, callback): def find_id(self, elem_id, callback):
@ -670,8 +685,9 @@ class WebEngineAudio(browsertab.AbstractAudio):
self._tab.url_changed.connect(self._on_url_changed) self._tab.url_changed.connect(self._on_url_changed)
config.instance.changed.connect(self._on_config_changed) config.instance.changed.connect(self._on_config_changed)
def set_muted(self, muted: bool, override: bool = False): def set_muted(self, muted: bool, override: bool = False) -> None:
self._overridden = override self._overridden = override
assert self._widget is not None
page = self._widget.page() page = self._widget.page()
page.setAudioMuted(muted) page.setAudioMuted(muted)
@ -1031,6 +1047,28 @@ class _WebEngineScripts(QObject):
page_scripts.insert(new_script) page_scripts.insert(new_script)
class WebEngineTabPrivate(browsertab.AbstractTabPrivate):
"""QtWebEngine-related methods which aren't part of the public API."""
def networkaccessmanager(self):
return None
def user_agent(self):
return None
def clear_ssl_errors(self):
raise browsertab.UnsupportedOperationError
def event_target(self):
return self._widget.render_widget()
def shutdown(self):
self._tab.shutting_down.emit()
self._tab.action.exit_fullscreen()
self._widget.shutdown()
class WebEngineTab(browsertab.AbstractTab): class WebEngineTab(browsertab.AbstractTab):
"""A QtWebEngine tab in the browser. """A QtWebEngine tab in the browser.
@ -1044,8 +1082,7 @@ class WebEngineTab(browsertab.AbstractTab):
_load_finished_fake = pyqtSignal(bool) _load_finished_fake = pyqtSignal(bool)
def __init__(self, *, win_id, mode_manager, private, parent=None): def __init__(self, *, win_id, mode_manager, private, parent=None):
super().__init__(win_id=win_id, mode_manager=mode_manager, super().__init__(win_id=win_id, private=private, parent=parent)
private=private, parent=parent)
widget = webview.WebEngineView(tabdata=self.data, win_id=win_id, widget = webview.WebEngineView(tabdata=self.data, win_id=win_id,
private=private) private=private)
self.history = WebEngineHistory(tab=self) self.history = WebEngineHistory(tab=self)
@ -1058,6 +1095,8 @@ class WebEngineTab(browsertab.AbstractTab):
self.elements = WebEngineElements(tab=self) self.elements = WebEngineElements(tab=self)
self.action = WebEngineAction(tab=self) self.action = WebEngineAction(tab=self)
self.audio = WebEngineAudio(tab=self, parent=self) self.audio = WebEngineAudio(tab=self, parent=self)
self.private_api = WebEngineTabPrivate(mode_manager=mode_manager,
tab=self)
self._permissions = _WebEnginePermissions(tab=self, parent=self) self._permissions = _WebEnginePermissions(tab=self, parent=self)
self._scripts = _WebEngineScripts(tab=self, parent=self) self._scripts = _WebEngineScripts(tab=self, parent=self)
# We're assigning settings in _set_widget # We're assigning settings in _set_widget
@ -1095,21 +1134,23 @@ class WebEngineTab(browsertab.AbstractTab):
self.zoom.set_factor(self._saved_zoom) self.zoom.set_factor(self._saved_zoom)
self._saved_zoom = None self._saved_zoom = None
def openurl(self, url, *, predict=True): def load_url(self, url, *, emit_before_load_started=True):
"""Open the given URL in this tab. """Load the given URL in this tab.
Arguments: Arguments:
url: The QUrl to open. url: The QUrl to load.
predict: If set to False, predicted_navigation is not emitted. emit_before_load_started: If set to False, before_load_started is
not emitted.
""" """
if sip.isdeleted(self._widget): if sip.isdeleted(self._widget):
# https://github.com/qutebrowser/qutebrowser/issues/3896 # https://github.com/qutebrowser/qutebrowser/issues/3896
return return
self._saved_zoom = self.zoom.factor() self._saved_zoom = self.zoom.factor()
self._openurl_prepare(url, predict=predict) self._load_url_prepare(
url, emit_before_load_started=emit_before_load_started)
self._widget.load(url) self._widget.load(url)
def url(self, requested=False): def url(self, *, requested=False):
page = self._widget.page() page = self._widget.page()
if requested: if requested:
return page.requestedUrl() return page.requestedUrl()
@ -1139,11 +1180,6 @@ class WebEngineTab(browsertab.AbstractTab):
else: else:
self._widget.page().runJavaScript(code, world_id, callback) self._widget.page().runJavaScript(code, world_id, callback)
def shutdown(self):
self.shutting_down.emit()
self.action.exit_fullscreen()
self._widget.shutdown()
def reload(self, *, force=False): def reload(self, *, force=False):
if force: if force:
action = QWebEnginePage.ReloadAndBypassCache action = QWebEnginePage.ReloadAndBypassCache
@ -1168,22 +1204,6 @@ class WebEngineTab(browsertab.AbstractTab):
# percent encoded content is 2 megabytes minus 30 bytes. # percent encoded content is 2 megabytes minus 30 bytes.
self._widget.setHtml(html, base_url) self._widget.setHtml(html, base_url)
def networkaccessmanager(self):
return None
def user_agent(self):
return None
def clear_ssl_errors(self):
raise browsertab.UnsupportedOperationError
def key_press(self, key, modifier=Qt.NoModifier):
press_evt = QKeyEvent(QEvent.KeyPress, key, modifier, 0, 0, 0)
release_evt = QKeyEvent(QEvent.KeyRelease, key, modifier,
0, 0, 0)
self.send_event(press_evt)
self.send_event(release_evt)
def _show_error_page(self, url, error): def _show_error_page(self, url, error):
"""Show an error page in the tab.""" """Show an error page in the tab."""
log.misc.debug("Showing error page for {}".format(error)) log.misc.debug("Showing error page for {}".format(error))
@ -1220,7 +1240,7 @@ class WebEngineTab(browsertab.AbstractTab):
log.misc.debug("Ignoring invalid URL being added to history") log.misc.debug("Ignoring invalid URL being added to history")
return return
self.add_history_item.emit(url, requested_url, title) self.history_item_triggered.emit(url, requested_url, title)
@pyqtSlot(QUrl, 'QAuthenticator*', 'QString') @pyqtSlot(QUrl, 'QAuthenticator*', 'QString')
def _on_proxy_authentication_required(self, url, authenticator, def _on_proxy_authentication_required(self, url, authenticator,
@ -1348,9 +1368,9 @@ class WebEngineTab(browsertab.AbstractTab):
log.config.debug( log.config.debug(
"Loading {} again because of config change".format( "Loading {} again because of config change".format(
self._reload_url.toDisplayString())) self._reload_url.toDisplayString()))
QTimer.singleShot(100, functools.partial(self.openurl, QTimer.singleShot(100, functools.partial(
self._reload_url, self.load_url, self._reload_url,
predict=False)) emit_before_load_started=False))
self._reload_url = None self._reload_url = None
if not qtutils.version_check('5.10', compiled=False): if not qtutils.version_check('5.10', compiled=False):
@ -1389,12 +1409,12 @@ class WebEngineTab(browsertab.AbstractTab):
self._show_error_page(url, str(error)) self._show_error_page(url, str(error))
@pyqtSlot(QUrl) @pyqtSlot(QUrl)
def _on_predicted_navigation(self, url): def _on_before_load_started(self, url):
"""If we know we're going to visit an URL soon, change the settings. """If we know we're going to visit a URL soon, change the settings.
This is a WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66656 This is a WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66656
""" """
super()._on_predicted_navigation(url) super()._on_before_load_started(url)
if not qtutils.version_check('5.11.1', compiled=False): if not qtutils.version_check('5.11.1', compiled=False):
self.settings.update_for_url(url) self.settings.update_for_url(url)
@ -1472,12 +1492,9 @@ class WebEngineTab(browsertab.AbstractTab):
page.loadFinished.connect(self._restore_zoom) page.loadFinished.connect(self._restore_zoom)
page.loadFinished.connect(self._on_load_finished) page.loadFinished.connect(self._on_load_finished)
self.predicted_navigation.connect(self._on_predicted_navigation) self.before_load_started.connect(self._on_before_load_started)
# pylint: disable=protected-access # pylint: disable=protected-access
self.audio._connect_signals() self.audio._connect_signals()
self._permissions.connect_signals() self._permissions.connect_signals()
self._scripts.connect_signals() self._scripts.connect_signals()
def event_target(self):
return self._widget.render_widget()

View File

@ -240,7 +240,7 @@ class WebEnginePage(QWebEnginePage):
def acceptNavigationRequest(self, def acceptNavigationRequest(self,
url: QUrl, url: QUrl,
typ: QWebEnginePage.NavigationType, typ: QWebEnginePage.NavigationType,
is_main_frame: bool): is_main_frame: bool) -> bool:
"""Override acceptNavigationRequest to forward it to the tab API.""" """Override acceptNavigationRequest to forward it to the tab API."""
type_map = { type_map = {
QWebEnginePage.NavigationTypeLinkClicked: QWebEnginePage.NavigationTypeLinkClicked:

View File

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

View File

@ -21,6 +21,7 @@
import collections import collections
import html import html
import typing # pylint: disable=unused-import
import attr import attr
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QCoreApplication, QUrl, from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QCoreApplication, QUrl,
@ -28,16 +29,23 @@ from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QCoreApplication, QUrl,
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QSslSocket from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QSslSocket
from qutebrowser.config import config from qutebrowser.config import config
MYPY = False
if MYPY:
# pylint can't interpret type comments with Python 3.7
# pylint: disable=unused-import,useless-suppression
from qutebrowser.mainwindow import prompt
from qutebrowser.utils import (message, log, usertypes, utils, objreg, from qutebrowser.utils import (message, log, usertypes, utils, objreg,
urlutils, debug) urlutils, debug)
from qutebrowser.browser import shared from qutebrowser.browser import shared
from qutebrowser.extensions import interceptors
from qutebrowser.browser.webkit import certificateerror from qutebrowser.browser.webkit import certificateerror
from qutebrowser.browser.webkit.network import (webkitqutescheme, networkreply, from qutebrowser.browser.webkit.network import (webkitqutescheme, networkreply,
filescheme) filescheme)
HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%' HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%'
_proxy_auth_cache = {} _proxy_auth_cache = {} # type: typing.Dict[ProxyId, prompt.AuthInfo]
@attr.s(frozen=True) @attr.s(frozen=True)
@ -295,9 +303,9 @@ class NetworkManager(QNetworkAccessManager):
"""Called when a proxy needs authentication.""" """Called when a proxy needs authentication."""
proxy_id = ProxyId(proxy.type(), proxy.hostName(), proxy.port()) proxy_id = ProxyId(proxy.type(), proxy.hostName(), proxy.port())
if proxy_id in _proxy_auth_cache: if proxy_id in _proxy_auth_cache:
user, password = _proxy_auth_cache[proxy_id] authinfo = _proxy_auth_cache[proxy_id]
authenticator.setUser(user) authenticator.setUser(authinfo.user)
authenticator.setPassword(password) authenticator.setPassword(authinfo.password)
else: else:
msg = '<b>{}</b> says:<br/>{}'.format( msg = '<b>{}</b> says:<br/>{}'.format(
html.escape(proxy.hostName()), html.escape(proxy.hostName()),
@ -398,10 +406,10 @@ class NetworkManager(QNetworkAccessManager):
# the webpage shutdown here. # the webpage shutdown here.
current_url = QUrl() current_url = QUrl()
host_blocker = objreg.get('host-blocker') request = interceptors.Request(first_party_url=current_url,
if host_blocker.is_blocked(req.url(), current_url): request_url=req.url())
log.webview.info("Request to {} blocked by host blocker.".format( interceptors.run(request)
req.url().host())) if request.is_blocked:
return networkreply.ErrorNetworkReply( return networkreply.ErrorNetworkReply(
req, HOSTBLOCK_ERROR_STRING, QNetworkReply.ContentAccessDenied, req, HOSTBLOCK_ERROR_STRING, QNetworkReply.ContentAccessDenied,
self) self)

View File

@ -67,7 +67,6 @@ class FixedDataNetworkReply(QNetworkReply):
@pyqtSlot() @pyqtSlot()
def abort(self): def abort(self):
"""Abort the operation.""" """Abort the operation."""
pass
def bytesAvailable(self): def bytesAvailable(self):
"""Determine the bytes available for being read. """Determine the bytes available for being read.
@ -123,7 +122,6 @@ class ErrorNetworkReply(QNetworkReply):
def abort(self): def abort(self):
"""Do nothing since it's a fake reply.""" """Do nothing since it's a fake reply."""
pass
def bytesAvailable(self): def bytesAvailable(self):
"""We always have 0 bytes available.""" """We always have 0 bytes available."""
@ -151,7 +149,6 @@ class RedirectNetworkReply(QNetworkReply):
def abort(self): def abort(self):
"""Called when there's e.g. a redirection limit.""" """Called when there's e.g. a redirection limit."""
pass
def readData(self, _maxlen): def readData(self, _maxlen):
return bytes() return bytes()

View File

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

View File

@ -41,7 +41,6 @@ class WebHistoryInterface(QWebHistoryInterface):
def addHistoryEntry(self, url_string): def addHistoryEntry(self, url_string):
"""Required for a QWebHistoryInterface impl, obsoleted by add_url.""" """Required for a QWebHistoryInterface impl, obsoleted by add_url."""
pass
@functools.lru_cache(maxsize=32768) @functools.lru_cache(maxsize=32768)
def historyContains(self, url_string): def historyContains(self, url_string):

View File

@ -23,9 +23,8 @@ import re
import functools import functools
import xml.etree.ElementTree import xml.etree.ElementTree
from PyQt5.QtCore import (pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer, QSizeF, from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QPoint, QTimer, QSizeF, QSize
QSize) from PyQt5.QtGui import QIcon
from PyQt5.QtGui import QKeyEvent, QIcon
from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtPrintSupport import QPrinter from PyQt5.QtPrintSupport import QPrinter
@ -125,8 +124,8 @@ class WebKitSearch(browsertab.AbstractSearch):
self._widget.findText('') self._widget.findText('')
self._widget.findText('', QWebPage.HighlightAllOccurrences) self._widget.findText('', QWebPage.HighlightAllOccurrences)
def search(self, text, *, ignore_case='never', reverse=False, def search(self, text, *, ignore_case=usertypes.IgnoreCase.never,
result_cb=None): reverse=False, result_cb=None):
# Don't go to next entry on duplicate search # Don't go to next entry on duplicate search
if self.text == text and self.search_displayed: if self.text == text and self.search_displayed:
log.webview.debug("Ignoring duplicate search request" log.webview.debug("Ignoring duplicate search request"
@ -391,7 +390,7 @@ class WebKitCaret(browsertab.AbstractCaret):
if tab: if tab:
self._tab.new_tab_requested.emit(url) self._tab.new_tab_requested.emit(url)
else: else:
self._tab.openurl(url) self._tab.load_url(url)
def follow_selected(self, *, tab=False): def follow_selected(self, *, tab=False):
try: try:
@ -474,7 +473,7 @@ class WebKitScroller(browsertab.AbstractScroller):
if (getter is not None and if (getter is not None and
frame.scrollBarValue(direction) == getter(direction)): frame.scrollBarValue(direction) == getter(direction)):
return return
self._tab.key_press(key) self._tab.fake_key_press(key)
def up(self, count=1): def up(self, count=1):
self._key_press(Qt.Key_Up, count, 'scrollBarMinimum', Qt.Vertical) self._key_press(Qt.Key_Up, count, 'scrollBarMinimum', Qt.Vertical)
@ -509,35 +508,19 @@ class WebKitScroller(browsertab.AbstractScroller):
return self.pos_px().y() >= frame.scrollBarMaximum(Qt.Vertical) return self.pos_px().y() >= frame.scrollBarMaximum(Qt.Vertical)
class WebKitHistory(browsertab.AbstractHistory): class WebKitHistoryPrivate(browsertab.AbstractHistoryPrivate):
"""QtWebKit implementations related to page history.""" """History-related methods which are not part of the extension API."""
def current_idx(self):
return self._history.currentItemIndex()
def can_go_back(self):
return self._history.canGoBack()
def can_go_forward(self):
return self._history.canGoForward()
def _item_at(self, i):
return self._history.itemAt(i)
def _go_to_item(self, item):
self._tab.predicted_navigation.emit(item.url())
self._history.goToItem(item)
def serialize(self): def serialize(self):
return qtutils.serialize(self._history) return qtutils.serialize(self._history)
def deserialize(self, data): def deserialize(self, data):
return qtutils.deserialize(data, self._history) qtutils.deserialize(data, self._history)
def load_items(self, items): def load_items(self, items):
if items: if items:
self._tab.predicted_navigation.emit(items[-1].url) self._tab.before_load_started.emit(items[-1].url)
stream, _data, user_data = tabhistory.serialize(items) stream, _data, user_data = tabhistory.serialize(items)
qtutils.deserialize_stream(stream, self._history) qtutils.deserialize_stream(stream, self._history)
@ -553,11 +536,43 @@ class WebKitHistory(browsertab.AbstractHistory):
self._tab.scroller.to_point, cur_data['scroll-pos'])) self._tab.scroller.to_point, cur_data['scroll-pos']))
class WebKitHistory(browsertab.AbstractHistory):
"""QtWebKit implementations related to page history."""
def __init__(self, tab):
super().__init__(tab)
self.private_api = WebKitHistoryPrivate(tab)
def __len__(self):
return len(self._history)
def __iter__(self):
return iter(self._history.items())
def current_idx(self):
return self._history.currentItemIndex()
def can_go_back(self):
return self._history.canGoBack()
def can_go_forward(self):
return self._history.canGoForward()
def _item_at(self, i):
return self._history.itemAt(i)
def _go_to_item(self, item):
self._tab.before_load_started.emit(item.url())
self._history.goToItem(item)
class WebKitElements(browsertab.AbstractElements): class WebKitElements(browsertab.AbstractElements):
"""QtWebKit implemementations related to elements on the page.""" """QtWebKit implemementations related to elements on the page."""
def find_css(self, selector, callback, *, only_visible=False): def find_css(self, selector, callback, error_cb, *, only_visible=False):
utils.unused(error_cb)
mainframe = self._widget.page().mainFrame() mainframe = self._widget.page().mainFrame()
if mainframe is None: if mainframe is None:
raise browsertab.WebTabError("No frame focused!") raise browsertab.WebTabError("No frame focused!")
@ -586,7 +601,7 @@ class WebKitElements(browsertab.AbstractElements):
# Escape non-alphanumeric characters in the selector # Escape non-alphanumeric characters in the selector
# https://www.w3.org/TR/CSS2/syndata.html#value-def-identifier # https://www.w3.org/TR/CSS2/syndata.html#value-def-identifier
elem_id = re.sub(r'[^a-zA-Z0-9_-]', r'\\\g<0>', elem_id) elem_id = re.sub(r'[^a-zA-Z0-9_-]', r'\\\g<0>', elem_id)
self.find_css('#' + elem_id, find_id_cb) self.find_css('#' + elem_id, find_id_cb, error_cb=lambda exc: None)
def find_focused(self, callback): def find_focused(self, callback):
frame = self._widget.page().currentFrame() frame = self._widget.page().currentFrame()
@ -641,7 +656,7 @@ class WebKitAudio(browsertab.AbstractAudio):
"""Dummy handling of audio status for QtWebKit.""" """Dummy handling of audio status for QtWebKit."""
def set_muted(self, muted: bool, override: bool = False): def set_muted(self, muted: bool, override: bool = False) -> None:
raise browsertab.WebTabError('Muting is not supported on QtWebKit!') raise browsertab.WebTabError('Muting is not supported on QtWebKit!')
def is_muted(self): def is_muted(self):
@ -651,13 +666,33 @@ class WebKitAudio(browsertab.AbstractAudio):
return False return False
class WebKitTabPrivate(browsertab.AbstractTabPrivate):
"""QtWebKit-related methods which aren't part of the public API."""
def networkaccessmanager(self):
return self._widget.page().networkAccessManager()
def user_agent(self):
page = self._widget.page()
return page.userAgentForUrl(self._tab.url())
def clear_ssl_errors(self):
self.networkaccessmanager().clear_all_ssl_errors()
def event_target(self):
return self._widget
def shutdown(self):
self._widget.shutdown()
class WebKitTab(browsertab.AbstractTab): class WebKitTab(browsertab.AbstractTab):
"""A QtWebKit tab in the browser.""" """A QtWebKit tab in the browser."""
def __init__(self, *, win_id, mode_manager, private, parent=None): def __init__(self, *, win_id, mode_manager, private, parent=None):
super().__init__(win_id=win_id, mode_manager=mode_manager, super().__init__(win_id=win_id, private=private, parent=parent)
private=private, parent=parent)
widget = webview.WebView(win_id=win_id, tab_id=self.tab_id, widget = webview.WebView(win_id=win_id, tab_id=self.tab_id,
private=private, tab=self) private=private, tab=self)
if private: if private:
@ -672,6 +707,8 @@ class WebKitTab(browsertab.AbstractTab):
self.elements = WebKitElements(tab=self) self.elements = WebKitElements(tab=self)
self.action = WebKitAction(tab=self) self.action = WebKitAction(tab=self)
self.audio = WebKitAudio(tab=self, parent=self) self.audio = WebKitAudio(tab=self, parent=self)
self.private_api = WebKitTabPrivate(mode_manager=mode_manager,
tab=self)
# We're assigning settings in _set_widget # We're assigning settings in _set_widget
self.settings = webkitsettings.WebKitSettings(settings=None) self.settings = webkitsettings.WebKitSettings(settings=None)
self._set_widget(widget) self._set_widget(widget)
@ -685,11 +722,12 @@ class WebKitTab(browsertab.AbstractTab):
settings = widget.settings() settings = widget.settings()
settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True) settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True)
def openurl(self, url, *, predict=True): def load_url(self, url, *, emit_before_load_started=True):
self._openurl_prepare(url, predict=predict) self._load_url_prepare(
self._widget.openurl(url) url, emit_before_load_started=emit_before_load_started)
self._widget.load(url)
def url(self, requested=False): def url(self, *, requested=False):
frame = self._widget.page().mainFrame() frame = self._widget.page().mainFrame()
if requested: if requested:
return frame.requestedUrl() return frame.requestedUrl()
@ -714,9 +752,6 @@ class WebKitTab(browsertab.AbstractTab):
def icon(self): def icon(self):
return self._widget.icon() return self._widget.icon()
def shutdown(self):
self._widget.shutdown()
def reload(self, *, force=False): def reload(self, *, force=False):
if force: if force:
action = QWebPage.ReloadAndBypassCache action = QWebPage.ReloadAndBypassCache
@ -730,36 +765,20 @@ class WebKitTab(browsertab.AbstractTab):
def title(self): def title(self):
return self._widget.title() return self._widget.title()
def clear_ssl_errors(self):
self.networkaccessmanager().clear_all_ssl_errors()
def key_press(self, key, modifier=Qt.NoModifier):
press_evt = QKeyEvent(QEvent.KeyPress, key, modifier, 0, 0, 0)
release_evt = QKeyEvent(QEvent.KeyRelease, key, modifier,
0, 0, 0)
self.send_event(press_evt)
self.send_event(release_evt)
@pyqtSlot() @pyqtSlot()
def _on_history_trigger(self): def _on_history_trigger(self):
url = self.url() url = self.url()
requested_url = self.url(requested=True) requested_url = self.url(requested=True)
self.add_history_item.emit(url, requested_url, self.title()) self.history_item_triggered.emit(url, requested_url, self.title())
def set_html(self, html, base_url=QUrl()): def set_html(self, html, base_url=QUrl()):
self._widget.setHtml(html, base_url) self._widget.setHtml(html, base_url)
def networkaccessmanager(self):
return self._widget.page().networkAccessManager()
def user_agent(self):
page = self._widget.page()
return page.userAgentForUrl(self.url())
@pyqtSlot() @pyqtSlot()
def _on_load_started(self): def _on_load_started(self):
super()._on_load_started() super()._on_load_started()
self.networkaccessmanager().netrc_used = False nam = self._widget.page().networkAccessManager()
nam.netrc_used = False
# Make sure the icon is cleared when navigating to a page without one. # Make sure the icon is cleared when navigating to a page without one.
self.icon_changed.emit(QIcon()) self.icon_changed.emit(QIcon())
@ -811,7 +830,7 @@ class WebKitTab(browsertab.AbstractTab):
if (navigation.navigation_type == navigation.Type.link_clicked and if (navigation.navigation_type == navigation.Type.link_clicked and
target != usertypes.ClickTarget.normal): target != usertypes.ClickTarget.normal):
tab = shared.get_tab(self.win_id, target) tab = shared.get_tab(self.win_id, target)
tab.openurl(navigation.url) tab.load_url(navigation.url)
self.data.open_target = usertypes.ClickTarget.normal self.data.open_target = usertypes.ClickTarget.normal
navigation.accepted = False navigation.accepted = False
@ -841,6 +860,3 @@ class WebKitTab(browsertab.AbstractTab):
frame.contentsSizeChanged.connect(self._on_contents_size_changed) frame.contentsSizeChanged.connect(self._on_contents_size_changed)
frame.initialLayoutCompleted.connect(self._on_history_trigger) frame.initialLayoutCompleted.connect(self._on_history_trigger)
page.navigation_request.connect(self._on_navigation_request) page.navigation_request.connect(self._on_navigation_request)
def event_target(self):
return self._widget

View File

@ -469,7 +469,7 @@ class BrowserPage(QWebPage):
def acceptNavigationRequest(self, def acceptNavigationRequest(self,
frame: QWebFrame, frame: QWebFrame,
request: QNetworkRequest, request: QNetworkRequest,
typ: QWebPage.NavigationType): typ: QWebPage.NavigationType) -> bool:
"""Override acceptNavigationRequest to handle clicked links. """Override acceptNavigationRequest to handle clicked links.
Setting linkDelegationPolicy to DelegateAllLinks and using a slot bound Setting linkDelegationPolicy to DelegateAllLinks and using a slot bound

View File

@ -118,14 +118,6 @@ class WebView(QWebView):
self.stop() self.stop()
self.page().shutdown() self.page().shutdown()
def openurl(self, url):
"""Open a URL in the browser.
Args:
url: The URL to load as QUrl
"""
self.load(url)
def createWindow(self, wintype): def createWindow(self, wintype):
"""Called by Qt when a page wants to create a new window. """Called by Qt when a page wants to create a new window.

View File

@ -28,26 +28,15 @@ class Error(Exception):
"""Base class for all cmdexc errors.""" """Base class for all cmdexc errors."""
class CommandError(Error):
"""Raised when a command encounters an error while running."""
pass
class NoSuchCommandError(Error): class NoSuchCommandError(Error):
"""Raised when a command wasn't found.""" """Raised when a command wasn't found."""
pass
class ArgumentTypeError(Error): class ArgumentTypeError(Error):
"""Raised when an argument had an invalid type.""" """Raised when an argument had an invalid type."""
pass
class PrerequisitesError(Error): class PrerequisitesError(Error):
@ -56,5 +45,3 @@ class PrerequisitesError(Error):
This is raised for example when we're in the wrong mode while executing the This is raised for example when we're in the wrong mode while executing the
command, or we need javascript enabled but don't have done so. command, or we need javascript enabled but don't have done so.
""" """
pass

View File

@ -1,147 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-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/>.
"""Contains various command utils and a global command dict.
Module attributes:
cmd_dict: A mapping from command-strings to command objects.
"""
import inspect
from qutebrowser.utils import qtutils, log
from qutebrowser.commands import command, cmdexc
cmd_dict = {}
def check_overflow(arg, ctype):
"""Check if the given argument is in bounds for the given type.
Args:
arg: The argument to check
ctype: The C/Qt type to check as a string.
"""
try:
qtutils.check_overflow(arg, ctype)
except OverflowError:
raise cmdexc.CommandError(
"Numeric argument is too large for internal {} "
"representation.".format(ctype))
def check_exclusive(flags, names):
"""Check if only one flag is set with exclusive flags.
Raise a CommandError if not.
Args:
flags: An iterable of booleans to check.
names: An iterable of flag names for the error message.
"""
if sum(1 for e in flags if e) > 1:
argstr = '/'.join('-' + e for e in names)
raise cmdexc.CommandError("Only one of {} can be given!".format(
argstr))
class register: # noqa: N801,N806 pylint: disable=invalid-name
"""Decorator to register a new command handler.
This could also be a function, but as a class (with a "wrong" name) it's
much cleaner to implement.
Attributes:
_instance: The object from the object registry to be used as "self".
_name: The name (as string) or names (as list) of the command.
_kwargs: The arguments to pass to Command.
"""
def __init__(self, *, instance=None, name=None, **kwargs):
"""Save decorator arguments.
Gets called on parse-time with the decorator arguments.
Args:
See class attributes.
"""
self._instance = instance
self._name = name
self._kwargs = kwargs
def __call__(self, func):
"""Register the command before running the function.
Gets called when a function should be decorated.
Doesn't actually decorate anything, but creates a Command object and
registers it in the cmd_dict.
Args:
func: The function to be decorated.
Return:
The original function (unmodified).
"""
if self._name is None:
name = func.__name__.lower().replace('_', '-')
else:
assert isinstance(self._name, str), self._name
name = self._name
log.commands.vdebug("Registering command {} (from {}:{})".format(
name, func.__module__, func.__qualname__))
if name in cmd_dict:
raise ValueError("{} is already registered!".format(name))
cmd = command.Command(name=name, instance=self._instance,
handler=func, **self._kwargs)
cmd_dict[name] = cmd
return func
class argument: # noqa: N801,N806 pylint: disable=invalid-name
"""Decorator to customize an argument for @cmdutils.register.
This could also be a function, but as a class (with a "wrong" name) it's
much cleaner to implement.
Attributes:
_argname: The name of the argument to handle.
_kwargs: Keyword arguments, valid ArgInfo members
"""
def __init__(self, argname, **kwargs):
self._argname = argname
self._kwargs = kwargs
def __call__(self, func):
funcname = func.__name__
if self._argname not in inspect.signature(func).parameters:
raise ValueError("{} has no argument {}!".format(funcname,
self._argname))
if not hasattr(func, 'qute_args'):
func.qute_args = {}
elif func.qute_args is None:
raise ValueError("@cmdutils.argument got called above (after) "
"@cmdutils.register for {}!".format(funcname))
func.qute_args[self._argname] = command.ArgInfo(**self._kwargs)
return func

View File

@ -26,8 +26,9 @@ import typing
import attr import attr
from qutebrowser.api import cmdutils
from qutebrowser.commands import cmdexc, argparser from qutebrowser.commands import cmdexc, argparser
from qutebrowser.utils import log, message, docutils, objreg, usertypes from qutebrowser.utils import log, message, docutils, objreg, usertypes, utils
from qutebrowser.utils import debug as debug_utils from qutebrowser.utils import debug as debug_utils
from qutebrowser.misc import objects from qutebrowser.misc import objects
@ -37,18 +38,13 @@ class ArgInfo:
"""Information about an argument.""" """Information about an argument."""
win_id = attr.ib(False) value = attr.ib(None)
count = attr.ib(False)
hide = attr.ib(False) hide = attr.ib(False)
metavar = attr.ib(None) metavar = attr.ib(None)
flag = attr.ib(None) flag = attr.ib(None)
completion = attr.ib(None) completion = attr.ib(None)
choices = attr.ib(None) choices = attr.ib(None)
def __attrs_post_init__(self):
if self.win_id and self.count:
raise TypeError("Argument marked as both count/win_id!")
class Command: class Command:
@ -75,6 +71,10 @@ class Command:
_scope: The scope to get _instance for in the object registry. _scope: The scope to get _instance for in the object registry.
""" """
# CommandValue values which need a count
COUNT_COMMAND_VALUES = [usertypes.CommandValue.count,
usertypes.CommandValue.count_tab]
def __init__(self, *, handler, name, instance=None, maxsplit=None, def __init__(self, *, handler, name, instance=None, maxsplit=None,
modes=None, not_modes=None, debug=False, deprecated=False, modes=None, not_modes=None, debug=False, deprecated=False,
no_cmd_split=False, star_args_optional=False, scope='global', no_cmd_split=False, star_args_optional=False, scope='global',
@ -116,7 +116,6 @@ class Command:
self.parser.add_argument('-h', '--help', action=argparser.HelpAction, self.parser.add_argument('-h', '--help', action=argparser.HelpAction,
default=argparser.SUPPRESS, nargs=0, default=argparser.SUPPRESS, nargs=0,
help=argparser.SUPPRESS) help=argparser.SUPPRESS)
self._check_func()
self.opt_args = collections.OrderedDict() self.opt_args = collections.OrderedDict()
self.namespace = None self.namespace = None
self._count = None self._count = None
@ -130,6 +129,7 @@ class Command:
self._qute_args = getattr(self.handler, 'qute_args', {}) self._qute_args = getattr(self.handler, 'qute_args', {})
self.handler.qute_args = None self.handler.qute_args = None
self._check_func()
self._inspect_func() self._inspect_func()
def _check_prerequisites(self, win_id): def _check_prerequisites(self, win_id):
@ -154,16 +154,21 @@ class Command:
def _check_func(self): def _check_func(self):
"""Make sure the function parameters don't violate any rules.""" """Make sure the function parameters don't violate any rules."""
signature = inspect.signature(self.handler) signature = inspect.signature(self.handler)
if 'self' in signature.parameters and self._instance is None: if 'self' in signature.parameters:
if self._instance is None:
raise TypeError("{} is a class method, but instance was not " raise TypeError("{} is a class method, but instance was not "
"given!".format(self.name[0])) "given!".format(self.name))
arg_info = self.get_arg_info(signature.parameters['self'])
if arg_info.value is not None:
raise TypeError("{}: Can't fill 'self' with value!"
.format(self.name))
elif 'self' not in signature.parameters and self._instance is not None: elif 'self' not in signature.parameters and self._instance is not None:
raise TypeError("{} is not a class method, but instance was " raise TypeError("{} is not a class method, but instance was "
"given!".format(self.name[0])) "given!".format(self.name))
elif any(param.kind == inspect.Parameter.VAR_KEYWORD elif any(param.kind == inspect.Parameter.VAR_KEYWORD
for param in signature.parameters.values()): for param in signature.parameters.values()):
raise TypeError("{}: functions with varkw arguments are not " raise TypeError("{}: functions with varkw arguments are not "
"supported!".format(self.name[0])) "supported!".format(self.name))
def get_arg_info(self, param): def get_arg_info(self, param):
"""Get an ArgInfo tuple for the given inspect.Parameter.""" """Get an ArgInfo tuple for the given inspect.Parameter."""
@ -186,13 +191,18 @@ class Command:
True if the parameter is special, False otherwise. True if the parameter is special, False otherwise.
""" """
arg_info = self.get_arg_info(param) arg_info = self.get_arg_info(param)
if arg_info.count: if arg_info.value is None:
return False
elif arg_info.value == usertypes.CommandValue.count:
if param.default is inspect.Parameter.empty: if param.default is inspect.Parameter.empty:
raise TypeError("{}: handler has count parameter " raise TypeError("{}: handler has count parameter "
"without default!".format(self.name)) "without default!".format(self.name))
return True return True
elif arg_info.win_id: elif isinstance(arg_info.value, usertypes.CommandValue):
return True return True
else:
raise TypeError("{}: Invalid value={!r} for argument '{}'!"
.format(self.name, arg_info.value, param.name))
return False return False
def _inspect_func(self): def _inspect_func(self):
@ -292,6 +302,8 @@ class Command:
name = argparser.arg_name(param.name) name = argparser.arg_name(param.name)
arg_info = self.get_arg_info(param) arg_info = self.get_arg_info(param)
assert not arg_info.value, name
if arg_info.flag is not None: if arg_info.flag is not None:
shortname = arg_info.flag shortname = arg_info.flag
else: else:
@ -321,75 +333,66 @@ class Command:
param: The inspect.Parameter to look at. param: The inspect.Parameter to look at.
""" """
arginfo = self.get_arg_info(param) arginfo = self.get_arg_info(param)
if param.annotation is not inspect.Parameter.empty: if arginfo.value:
# Filled values are passed 1:1
return None
elif param.kind in [inspect.Parameter.VAR_POSITIONAL,
inspect.Parameter.VAR_KEYWORD]:
# For *args/**kwargs we only support strings
assert param.annotation in [inspect.Parameter.empty, str], param
return None
elif param.annotation is not inspect.Parameter.empty:
return param.annotation return param.annotation
elif param.default not in [None, inspect.Parameter.empty]: elif param.default not in [None, inspect.Parameter.empty]:
return type(param.default) return type(param.default)
elif arginfo.count or arginfo.win_id or param.kind in [
inspect.Parameter.VAR_POSITIONAL,
inspect.Parameter.VAR_KEYWORD]:
return None
else: else:
return str return str
def _get_self_arg(self, win_id, param, args): def _get_objreg(self, *, win_id, name, scope):
"""Get the self argument for a function call. """Get an object from the objreg."""
if scope == 'global':
Arguments:
win_id: The window id this command should be executed in.
param: The count parameter.
args: The positional argument list. Gets modified directly.
"""
assert param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
if self._scope == 'global':
tab_id = None tab_id = None
win_id = None win_id = None
elif self._scope == 'tab': elif scope == 'tab':
tab_id = 'current' tab_id = 'current'
elif self._scope == 'window': elif scope == 'window':
tab_id = None tab_id = None
else: else:
raise ValueError("Invalid scope {}!".format(self._scope)) raise ValueError("Invalid scope {}!".format(scope))
obj = objreg.get(self._instance, scope=self._scope, window=win_id, return objreg.get(name, scope=scope, window=win_id, tab=tab_id)
tab=tab_id)
args.append(obj)
def _get_count_arg(self, param, args, kwargs): def _add_special_arg(self, *, value, param, args, kwargs):
"""Add the count argument to a function call. """Add a special argument value to a function call.
Arguments: Arguments:
param: The count parameter. value: The value to add.
param: The parameter being filled.
args: The positional argument list. Gets modified directly. args: The positional argument list. Gets modified directly.
kwargs: The keyword argument dict. Gets modified directly. kwargs: The keyword argument dict. Gets modified directly.
""" """
if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
if self._count is not None: args.append(value)
args.append(self._count)
else:
args.append(param.default)
elif param.kind == inspect.Parameter.KEYWORD_ONLY: elif param.kind == inspect.Parameter.KEYWORD_ONLY:
if self._count is not None: kwargs[param.name] = value
kwargs[param.name] = self._count
else: else:
raise TypeError("{}: invalid parameter type {} for argument " raise TypeError("{}: invalid parameter type {} for argument "
"{!r}!".format(self.name, param.kind, param.name)) "{!r}!".format(self.name, param.kind, param.name))
def _get_win_id_arg(self, win_id, param, args, kwargs): def _add_count_tab(self, *, win_id, param, args, kwargs):
"""Add the win_id argument to a function call. """Add the count_tab widget argument."""
tabbed_browser = self._get_objreg(
win_id=win_id, name='tabbed-browser', scope='window')
Arguments: if self._count is None:
win_id: The window ID to add. tab = tabbed_browser.widget.currentWidget()
param: The count parameter. elif 1 <= self._count <= tabbed_browser.widget.count():
args: The positional argument list. Gets modified directly. cmdutils.check_overflow(self._count + 1, 'int')
kwargs: The keyword argument dict. Gets modified directly. tab = tabbed_browser.widget.widget(self._count - 1)
"""
if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
args.append(win_id)
elif param.kind == inspect.Parameter.KEYWORD_ONLY:
kwargs[param.name] = win_id
else: else:
raise TypeError("{}: invalid parameter type {} for argument " tab = None
"{!r}!".format(self.name, param.kind, param.name))
self._add_special_arg(value=tab, param=param, args=args,
kwargs=kwargs)
def _get_param_value(self, param): def _get_param_value(self, param):
"""Get the converted value for an inspect.Parameter.""" """Get the converted value for an inspect.Parameter."""
@ -428,6 +431,55 @@ class Command:
return value return value
def _handle_special_call_arg(self, *, pos, param, win_id, args, kwargs):
"""Check whether the argument is special, and if so, fill it in.
Args:
pos: The position of the argument.
param: The argparse.Parameter.
win_id: The window ID the command is run in.
args/kwargs: The args/kwargs to fill.
Return:
True if it was a special arg, False otherwise.
"""
arg_info = self.get_arg_info(param)
if pos == 0 and self._instance is not None:
assert param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD
self_value = self._get_objreg(win_id=win_id, name=self._instance,
scope=self._scope)
self._add_special_arg(value=self_value, param=param,
args=args, kwargs=kwargs)
return True
elif arg_info.value == usertypes.CommandValue.count:
if self._count is None:
assert param.default is not inspect.Parameter.empty
value = param.default
else:
value = self._count
self._add_special_arg(value=value, param=param,
args=args, kwargs=kwargs)
return True
elif arg_info.value == usertypes.CommandValue.win_id:
self._add_special_arg(value=win_id, param=param,
args=args, kwargs=kwargs)
return True
elif arg_info.value == usertypes.CommandValue.cur_tab:
tab = self._get_objreg(win_id=win_id, name='tab', scope='tab')
self._add_special_arg(value=tab, param=param,
args=args, kwargs=kwargs)
return True
elif arg_info.value == usertypes.CommandValue.count_tab:
self._add_count_tab(win_id=win_id, param=param, args=args,
kwargs=kwargs)
return True
elif arg_info.value is None:
pass
else:
raise utils.Unreachable(arg_info)
return False
def _get_call_args(self, win_id): def _get_call_args(self, win_id):
"""Get arguments for a function call. """Get arguments for a function call.
@ -442,20 +494,11 @@ class Command:
signature = inspect.signature(self.handler) signature = inspect.signature(self.handler)
for i, param in enumerate(signature.parameters.values()): for i, param in enumerate(signature.parameters.values()):
arg_info = self.get_arg_info(param) if self._handle_special_call_arg(pos=i, param=param,
if i == 0 and self._instance is not None: win_id=win_id, args=args,
# Special case for 'self'. kwargs=kwargs):
self._get_self_arg(win_id, param, args)
continue
elif arg_info.count:
# Special case for count parameter.
self._get_count_arg(param, args, kwargs)
continue
# elif arg_info.win_id:
elif arg_info.win_id:
# Special case for win_id parameter.
self._get_win_id_arg(win_id, param, args, kwargs)
continue continue
value = self._get_param_value(param) value = self._get_param_value(param)
if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
args.append(value) args.append(value)
@ -520,4 +563,14 @@ class Command:
def takes_count(self): def takes_count(self):
"""Return true iff this command can take a count argument.""" """Return true iff this command can take a count argument."""
return any(arg.count for arg in self._qute_args) return any(info.value in self.COUNT_COMMAND_VALUES
for info in self._qute_args.values())
def register(self):
"""Register this command in objects.commands."""
log.commands.vdebug(
"Registering command {} (from {}:{})".format(
self.name, self.handler.__module__, self.handler.__qualname__))
if self.name in objects.commands:
raise ValueError("{} is already registered!".format(self.name))
objects.commands[self.name] = self

View File

@ -25,10 +25,11 @@ import re
import attr import attr
from PyQt5.QtCore import pyqtSlot, QUrl, QObject from PyQt5.QtCore import pyqtSlot, QUrl, QObject
from qutebrowser.api import cmdutils
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.commands import cmdexc
from qutebrowser.utils import message, objreg, qtutils, usertypes, utils from qutebrowser.utils import message, objreg, qtutils, usertypes, utils
from qutebrowser.misc import split from qutebrowser.misc import split, objects
last_command = {} last_command = {}
@ -53,11 +54,14 @@ def _current_url(tabbed_browser):
if e.reason: if e.reason:
msg += " ({})".format(e.reason) msg += " ({})".format(e.reason)
msg += "!" msg += "!"
raise cmdexc.CommandError(msg) raise cmdutils.CommandError(msg)
def replace_variables(win_id, arglist): def replace_variables(win_id, arglist):
"""Utility function to replace variables like {url} in a list of args.""" """Utility function to replace variables like {url} in a list of args."""
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
variables = { variables = {
'url': lambda: _current_url(tabbed_browser).toString( 'url': lambda: _current_url(tabbed_browser).toString(
QUrl.FullyEncoded | QUrl.RemovePassword), QUrl.FullyEncoded | QUrl.RemovePassword),
@ -67,13 +71,13 @@ def replace_variables(win_id, arglist):
'clipboard': utils.get_clipboard, 'clipboard': utils.get_clipboard,
'primary': lambda: utils.get_clipboard(selection=True), 'primary': lambda: utils.get_clipboard(selection=True),
} }
for key in list(variables): for key in list(variables):
modified_key = '{' + key + '}' modified_key = '{' + key + '}'
variables[modified_key] = lambda x=modified_key: x variables[modified_key] = lambda x=modified_key: x
values = {} values = {}
args = [] args = []
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
def repl_cb(matchobj): def repl_cb(matchobj):
"""Return replacement for given match.""" """Return replacement for given match."""
@ -90,7 +94,7 @@ def replace_variables(win_id, arglist):
# "{url}" from clipboard is not expanded) # "{url}" from clipboard is not expanded)
args.append(repl_pattern.sub(repl_cb, arg)) args.append(repl_pattern.sub(repl_cb, arg))
except utils.ClipboardError as e: except utils.ClipboardError as e:
raise cmdexc.CommandError(e) raise cmdutils.CommandError(e)
return args return args
@ -190,7 +194,7 @@ class CommandParser:
cmdstr = self._completion_match(cmdstr) cmdstr = self._completion_match(cmdstr)
try: try:
cmd = cmdutils.cmd_dict[cmdstr] cmd = objects.commands[cmdstr]
except KeyError: except KeyError:
if not fallback: if not fallback:
raise cmdexc.NoSuchCommandError( raise cmdexc.NoSuchCommandError(
@ -217,7 +221,7 @@ class CommandParser:
Return: Return:
cmdstr modified to the matching completion or unmodified cmdstr modified to the matching completion or unmodified
""" """
matches = [cmd for cmd in sorted(cmdutils.cmd_dict, key=len) matches = [cmd for cmd in sorted(objects.commands, key=len)
if cmdstr in cmd] if cmdstr in cmd]
if len(matches) == 1: if len(matches) == 1:
cmdstr = matches[0] cmdstr = matches[0]

View File

@ -23,7 +23,8 @@ import attr
from PyQt5.QtCore import pyqtSlot, QObject, QTimer from PyQt5.QtCore import pyqtSlot, QObject, QTimer
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.commands import cmdutils, runners from qutebrowser.commands import runners
from qutebrowser.misc import objects
from qutebrowser.utils import log, utils, debug from qutebrowser.utils import log, utils, debug
from qutebrowser.completion.models import miscmodels from qutebrowser.completion.models import miscmodels
@ -92,7 +93,7 @@ class Completer(QObject):
log.completion.debug('Starting command completion') log.completion.debug('Starting command completion')
return miscmodels.command return miscmodels.command
try: try:
cmd = cmdutils.cmd_dict[before_cursor[0]] cmd = objects.commands[before_cursor[0]]
except KeyError: except KeyError:
log.completion.debug("No completion for unknown command: {}" log.completion.debug("No completion for unknown command: {}"
.format(before_cursor[0])) .format(before_cursor[0]))
@ -170,7 +171,7 @@ class Completer(QObject):
before, center, after = self._partition() before, center, after = self._partition()
log.completion.debug("Changing {} to '{}'".format(center, text)) log.completion.debug("Changing {} to '{}'".format(center, text))
try: try:
maxsplit = cmdutils.cmd_dict[before[0]].maxsplit maxsplit = objects.commands[before[0]].maxsplit
except (KeyError, IndexError): except (KeyError, IndexError):
maxsplit = None maxsplit = None
if maxsplit is None: if maxsplit is None:

View File

@ -29,7 +29,7 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, QSize
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.completion import completiondelegate from qutebrowser.completion import completiondelegate
from qutebrowser.utils import utils, usertypes, debug, log, objreg from qutebrowser.utils import utils, usertypes, debug, log, objreg
from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.api import cmdutils
class CompletionView(QTreeView): class CompletionView(QTreeView):
@ -251,7 +251,7 @@ class CompletionView(QTreeView):
status.command_history_prev() status.command_history_prev()
return return
else: else:
raise cmdexc.CommandError("Can't combine --history with " raise cmdutils.CommandError("Can't combine --history with "
"{}!".format(which)) "{}!".format(which))
if not self._active: if not self._active:
@ -394,7 +394,7 @@ class CompletionView(QTreeView):
"""Delete the current completion item.""" """Delete the current completion item."""
index = self.currentIndex() index = self.currentIndex()
if not index.isValid(): if not index.isValid():
raise cmdexc.CommandError("No item selected!") raise cmdutils.CommandError("No item selected!")
self.model().delete_cur_item(index) self.model().delete_cur_item(index)
@cmdutils.register(instance='completion', @cmdutils.register(instance='completion',
@ -411,6 +411,6 @@ class CompletionView(QTreeView):
if not text: if not text:
index = self.currentIndex() index = self.currentIndex()
if not index.isValid(): if not index.isValid():
raise cmdexc.CommandError("No item selected!") raise cmdutils.CommandError("No item selected!")
text = self.model().data(index) text = self.model().data(index)
utils.set_clipboard(text, selection=sel) utils.set_clipboard(text, selection=sel)

View File

@ -22,7 +22,7 @@
from PyQt5.QtCore import Qt, QModelIndex, QAbstractItemModel from PyQt5.QtCore import Qt, QModelIndex, QAbstractItemModel
from qutebrowser.utils import log, qtutils from qutebrowser.utils import log, qtutils
from qutebrowser.commands import cmdexc from qutebrowser.api import cmdutils
class CompletionModel(QAbstractItemModel): class CompletionModel(QAbstractItemModel):
@ -224,7 +224,7 @@ class CompletionModel(QAbstractItemModel):
cat = self._cat_from_idx(parent) cat = self._cat_from_idx(parent)
assert cat, "CompletionView sent invalid index for deletion" assert cat, "CompletionView sent invalid index for deletion"
if not cat.delete_func: if not cat.delete_func:
raise cmdexc.CommandError("Cannot delete this item.") raise cmdutils.CommandError("Cannot delete this item.")
data = [cat.data(cat.index(index.row(), i)) data = [cat.data(cat.index(index.row(), i))
for i in range(cat.columnCount())] for i in range(cat.columnCount())]

View File

@ -74,9 +74,10 @@ class HistoryCategory(QSqlQueryModel):
# build a where clause to match all of the words in any order # build a where clause to match all of the words in any order
# given the search term "a b", the WHERE clause would be: # given the search term "a b", the WHERE clause would be:
# ((url || title) LIKE '%a%') AND ((url || title) LIKE '%b%') # ((url || ' ' || title) LIKE '%a%') AND
# ((url || ' ' || title) LIKE '%b%')
where_clause = ' AND '.join( where_clause = ' AND '.join(
"(url || title) LIKE :{} escape '\\'".format(i) "(url || ' ' || title) LIKE :{} escape '\\'".format(i)
for i in range(len(words))) for i in range(len(words)))
# replace ' in timestamp-format to avoid breaking the query # replace ' in timestamp-format to avoid breaking the query

View File

@ -20,7 +20,7 @@
"""Utility functions for completion models.""" """Utility functions for completion models."""
from qutebrowser.utils import objreg, usertypes from qutebrowser.utils import objreg, usertypes
from qutebrowser.commands import cmdutils from qutebrowser.misc import objects
def get_cmd_completions(info, include_hidden, include_aliases, prefix=''): def get_cmd_completions(info, include_hidden, include_aliases, prefix=''):
@ -34,10 +34,10 @@ def get_cmd_completions(info, include_hidden, include_aliases, prefix=''):
Return: A list of tuples of form (name, description, bindings). Return: A list of tuples of form (name, description, bindings).
""" """
assert cmdutils.cmd_dict assert objects.commands
cmdlist = [] cmdlist = []
cmd_to_keys = info.keyconf.get_reverse_bindings_for('normal') cmd_to_keys = info.keyconf.get_reverse_bindings_for('normal')
for obj in set(cmdutils.cmd_dict.values()): for obj in set(objects.commands.values()):
hide_debug = obj.debug and not objreg.get('args').debug hide_debug = obj.debug and not objreg.get('args').debug
hide_mode = (usertypes.KeyMode.normal not in obj.modes and hide_mode = (usertypes.KeyMode.normal not in obj.modes and
not include_hidden) not include_hidden)

View File

@ -0,0 +1,20 @@
# 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/>.
"""qutebrowser "extensions" which only use the qutebrowser.api API."""

View File

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

View File

@ -0,0 +1,211 @@
# 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/>.
"""Commands related to caret browsing."""
from qutebrowser.api import cmdutils, apitypes
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
@cmdutils.argument('count', value=cmdutils.Value.count)
def move_to_next_line(tab: apitypes.Tab, count: int = 1) -> None:
"""Move the cursor or selection to the next line.
Args:
count: How many lines to move.
"""
tab.caret.move_to_next_line(count)
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
@cmdutils.argument('count', value=cmdutils.Value.count)
def move_to_prev_line(tab: apitypes.Tab, count: int = 1) -> None:
"""Move the cursor or selection to the prev line.
Args:
count: How many lines to move.
"""
tab.caret.move_to_prev_line(count)
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
@cmdutils.argument('count', value=cmdutils.Value.count)
def move_to_next_char(tab: apitypes.Tab, count: int = 1) -> None:
"""Move the cursor or selection to the next char.
Args:
count: How many lines to move.
"""
tab.caret.move_to_next_char(count)
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
@cmdutils.argument('count', value=cmdutils.Value.count)
def move_to_prev_char(tab: apitypes.Tab, count: int = 1) -> None:
"""Move the cursor or selection to the previous char.
Args:
count: How many chars to move.
"""
tab.caret.move_to_prev_char(count)
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
@cmdutils.argument('count', value=cmdutils.Value.count)
def move_to_end_of_word(tab: apitypes.Tab, count: int = 1) -> None:
"""Move the cursor or selection to the end of the word.
Args:
count: How many words to move.
"""
tab.caret.move_to_end_of_word(count)
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
@cmdutils.argument('count', value=cmdutils.Value.count)
def move_to_next_word(tab: apitypes.Tab, count: int = 1) -> None:
"""Move the cursor or selection to the next word.
Args:
count: How many words to move.
"""
tab.caret.move_to_next_word(count)
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
@cmdutils.argument('count', value=cmdutils.Value.count)
def move_to_prev_word(tab: apitypes.Tab, count: int = 1) -> None:
"""Move the cursor or selection to the previous word.
Args:
count: How many words to move.
"""
tab.caret.move_to_prev_word(count)
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
def move_to_start_of_line(tab: apitypes.Tab) -> None:
"""Move the cursor or selection to the start of the line."""
tab.caret.move_to_start_of_line()
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
def move_to_end_of_line(tab: apitypes.Tab) -> None:
"""Move the cursor or selection to the end of line."""
tab.caret.move_to_end_of_line()
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
@cmdutils.argument('count', value=cmdutils.Value.count)
def move_to_start_of_next_block(tab: apitypes.Tab, count: int = 1) -> None:
"""Move the cursor or selection to the start of next block.
Args:
count: How many blocks to move.
"""
tab.caret.move_to_start_of_next_block(count)
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
@cmdutils.argument('count', value=cmdutils.Value.count)
def move_to_start_of_prev_block(tab: apitypes.Tab, count: int = 1) -> None:
"""Move the cursor or selection to the start of previous block.
Args:
count: How many blocks to move.
"""
tab.caret.move_to_start_of_prev_block(count)
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
@cmdutils.argument('count', value=cmdutils.Value.count)
def move_to_end_of_next_block(tab: apitypes.Tab, count: int = 1) -> None:
"""Move the cursor or selection to the end of next block.
Args:
count: How many blocks to move.
"""
tab.caret.move_to_end_of_next_block(count)
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
@cmdutils.argument('count', value=cmdutils.Value.count)
def move_to_end_of_prev_block(tab: apitypes.Tab, count: int = 1) -> None:
"""Move the cursor or selection to the end of previous block.
Args:
count: How many blocks to move.
"""
tab.caret.move_to_end_of_prev_block(count)
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
def move_to_start_of_document(tab: apitypes.Tab) -> None:
"""Move the cursor or selection to the start of the document."""
tab.caret.move_to_start_of_document()
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
def move_to_end_of_document(tab: apitypes.Tab) -> None:
"""Move the cursor or selection to the end of the document."""
tab.caret.move_to_end_of_document()
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
def toggle_selection(tab: apitypes.Tab) -> None:
"""Toggle caret selection mode."""
tab.caret.toggle_selection()
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
def drop_selection(tab: apitypes.Tab) -> None:
"""Drop selection and keep selection mode enabled."""
tab.caret.drop_selection()
@cmdutils.register()
@cmdutils.argument('tab_obj', value=cmdutils.Value.cur_tab)
def follow_selected(tab_obj: apitypes.Tab, *, tab: bool = False) -> None:
"""Follow the selected text.
Args:
tab: Load the selected link in a new tab.
"""
try:
tab_obj.caret.follow_selected(tab=tab)
except apitypes.WebTabError as e:
raise cmdutils.CommandError(str(e))

View File

@ -0,0 +1,312 @@
# 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/>.
"""Various commands."""
import os
import signal
import functools
import logging
try:
import hunter
except ImportError:
hunter = None
from PyQt5.QtCore import Qt
from PyQt5.QtPrintSupport import QPrintPreviewDialog
from qutebrowser.api import cmdutils, apitypes, message, config
@cmdutils.register(name='reload')
@cmdutils.argument('tab', value=cmdutils.Value.count_tab)
def reloadpage(tab: apitypes.Tab, force: bool = False) -> None:
"""Reload the current/[count]th tab.
Args:
count: The tab index to reload, or None.
force: Bypass the page cache.
"""
if tab is not None:
tab.reload(force=force)
@cmdutils.register()
@cmdutils.argument('tab', value=cmdutils.Value.count_tab)
def stop(tab: apitypes.Tab) -> None:
"""Stop loading in the current/[count]th tab.
Args:
count: The tab index to stop, or None.
"""
if tab is not None:
tab.stop()
def _print_preview(tab: apitypes.Tab) -> None:
"""Show a print preview."""
def print_callback(ok: bool) -> None:
if not ok:
message.error("Printing failed!")
tab.printing.check_preview_support()
diag = QPrintPreviewDialog(tab)
diag.setAttribute(Qt.WA_DeleteOnClose)
diag.setWindowFlags(diag.windowFlags() | Qt.WindowMaximizeButtonHint |
Qt.WindowMinimizeButtonHint)
diag.paintRequested.connect(functools.partial(
tab.printing.to_printer, callback=print_callback))
diag.exec_()
def _print_pdf(tab: apitypes.Tab, filename: str) -> None:
"""Print to the given PDF file."""
tab.printing.check_pdf_support()
filename = os.path.expanduser(filename)
directory = os.path.dirname(filename)
if directory and not os.path.exists(directory):
os.mkdir(directory)
tab.printing.to_pdf(filename)
logging.getLogger('misc').debug("Print to file: {}".format(filename))
@cmdutils.register(name='print')
@cmdutils.argument('tab', value=cmdutils.Value.count_tab)
@cmdutils.argument('pdf', flag='f', metavar='file')
def printpage(tab: apitypes.Tab,
preview: bool = False, *,
pdf: str = None) -> None:
"""Print the current/[count]th tab.
Args:
preview: Show preview instead of printing.
count: The tab index to print, or None.
pdf: The file path to write the PDF to.
"""
if tab is None:
return
try:
if preview:
_print_preview(tab)
elif pdf:
_print_pdf(tab, pdf)
else:
tab.printing.show_dialog()
except apitypes.WebTabError as e:
raise cmdutils.CommandError(e)
@cmdutils.register()
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
def home(tab: apitypes.Tab) -> None:
"""Open main startpage in current tab."""
if tab.data.pinned:
message.info("Tab is pinned!")
else:
tab.load_url(config.val.url.start_pages[0])
@cmdutils.register(debug=True)
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
def debug_dump_page(tab: apitypes.Tab, dest: str, plain: bool = False) -> None:
"""Dump the current page's content to a file.
Args:
dest: Where to write the file to.
plain: Write plain text instead of HTML.
"""
dest = os.path.expanduser(dest)
def callback(data: str) -> None:
"""Write the data to disk."""
try:
with open(dest, 'w', encoding='utf-8') as f:
f.write(data)
except OSError as e:
message.error('Could not write page: {}'.format(e))
else:
message.info("Dumped page to {}.".format(dest))
tab.dump_async(callback, plain=plain)
@cmdutils.register(maxsplit=0)
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
def insert_text(tab: apitypes.Tab, text: str) -> None:
"""Insert text at cursor position.
Args:
text: The text to insert.
"""
def _insert_text_cb(elem: apitypes.WebElement) -> None:
if elem is None:
message.error("No element focused!")
return
try:
elem.insert_text(text)
except apitypes.WebElemError as e:
message.error(str(e))
return
tab.elements.find_focused(_insert_text_cb)
@cmdutils.register()
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
@cmdutils.argument('filter_', choices=['id'])
def click_element(tab: apitypes.Tab, filter_: str, value: str, *,
target: apitypes.ClickTarget =
apitypes.ClickTarget.normal,
force_event: bool = False) -> None:
"""Click the element matching the given filter.
The given filter needs to result in exactly one element, otherwise, an
error is shown.
Args:
filter_: How to filter the elements.
id: Get an element based on its ID.
value: The value to filter for.
target: How to open the clicked element (normal/tab/tab-bg/window).
force_event: Force generating a fake click event.
"""
def single_cb(elem: apitypes.WebElement) -> None:
"""Click a single element."""
if elem is None:
message.error("No element found with id {}!".format(value))
return
try:
elem.click(target, force_event=force_event)
except apitypes.WebElemError as e:
message.error(str(e))
return
handlers = {
'id': (tab.elements.find_id, single_cb),
}
handler, callback = handlers[filter_]
handler(value, callback)
@cmdutils.register(debug=True)
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
@cmdutils.argument('count', value=cmdutils.Value.count)
def debug_webaction(tab: apitypes.Tab, action: str, count: int = 1) -> None:
"""Execute a webaction.
Available actions:
http://doc.qt.io/archives/qt-5.5/qwebpage.html#WebAction-enum (WebKit)
http://doc.qt.io/qt-5/qwebenginepage.html#WebAction-enum (WebEngine)
Args:
action: The action to execute, e.g. MoveToNextChar.
count: How many times to repeat the action.
"""
for _ in range(count):
try:
tab.action.run_string(action)
except apitypes.WebTabError as e:
raise cmdutils.CommandError(str(e))
@cmdutils.register()
@cmdutils.argument('tab', value=cmdutils.Value.count_tab)
def tab_mute(tab: apitypes.Tab) -> None:
"""Mute/Unmute the current/[count]th tab.
Args:
count: The tab index to mute or unmute, or None
"""
if tab is None:
return
try:
tab.audio.set_muted(not tab.audio.is_muted(), override=True)
except apitypes.WebTabError as e:
raise cmdutils.CommandError(e)
@cmdutils.register()
def nop() -> None:
"""Do nothing."""
@cmdutils.register()
def message_error(text: str) -> None:
"""Show an error message in the statusbar.
Args:
text: The text to show.
"""
message.error(text)
@cmdutils.register()
@cmdutils.argument('count', value=cmdutils.Value.count)
def message_info(text: str, count: int = 1) -> None:
"""Show an info message in the statusbar.
Args:
text: The text to show.
count: How many times to show the message
"""
for _ in range(count):
message.info(text)
@cmdutils.register()
def message_warning(text: str) -> None:
"""Show a warning message in the statusbar.
Args:
text: The text to show.
"""
message.warning(text)
@cmdutils.register(debug=True)
@cmdutils.argument('typ', choices=['exception', 'segfault'])
def debug_crash(typ: str = 'exception') -> None:
"""Crash for debugging purposes.
Args:
typ: either 'exception' or 'segfault'.
"""
if typ == 'segfault':
os.kill(os.getpid(), signal.SIGSEGV)
raise Exception("Segfault failed (wat.)")
else:
raise Exception("Forced crash")
@cmdutils.register(debug=True, maxsplit=0, no_cmd_split=True)
def debug_trace(expr: str = "") -> None:
"""Trace executed code via hunter.
Args:
expr: What to trace, passed to hunter.
"""
if hunter is None:
raise cmdutils.CommandError("You need to install 'hunter' to use this "
"command!")
try:
eval('hunter.trace({})'.format(expr))
except Exception as e:
raise cmdutils.CommandError("{}: {}".format(e.__class__.__name__, e))

View File

@ -0,0 +1,122 @@
# 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/>.
"""Scrolling-related commands."""
from qutebrowser.api import cmdutils, apitypes
@cmdutils.register()
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
@cmdutils.argument('count', value=cmdutils.Value.count)
def scroll_px(tab: apitypes.Tab, dx: int, dy: int, count: int = 1) -> None:
"""Scroll the current tab by 'count * dx/dy' pixels.
Args:
dx: How much to scroll in x-direction.
dy: How much to scroll in y-direction.
count: multiplier
"""
dx *= count
dy *= count
cmdutils.check_overflow(dx, 'int')
cmdutils.check_overflow(dy, 'int')
tab.scroller.delta(dx, dy)
@cmdutils.register()
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
@cmdutils.argument('count', value=cmdutils.Value.count)
def scroll(tab: apitypes.Tab, direction: str, count: int = 1) -> None:
"""Scroll the current tab in the given direction.
Note you can use `:run-with-count` to have a keybinding with a bigger
scroll increment.
Args:
direction: In which direction to scroll
(up/down/left/right/top/bottom).
count: multiplier
"""
funcs = {
'up': tab.scroller.up,
'down': tab.scroller.down,
'left': tab.scroller.left,
'right': tab.scroller.right,
'top': tab.scroller.top,
'bottom': tab.scroller.bottom,
'page-up': tab.scroller.page_up,
'page-down': tab.scroller.page_down,
}
try:
func = funcs[direction]
except KeyError:
expected_values = ', '.join(sorted(funcs))
raise cmdutils.CommandError("Invalid value {!r} for direction - "
"expected one of: {}".format(
direction, expected_values))
if direction in ['top', 'bottom']:
func()
else:
func(count=count)
@cmdutils.register()
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
@cmdutils.argument('count', value=cmdutils.Value.count)
@cmdutils.argument('horizontal', flag='x')
def scroll_to_perc(tab: apitypes.Tab, count: int = None,
perc: float = None, horizontal: bool = False) -> None:
"""Scroll to a specific percentage of the page.
The percentage can be given either as argument or as count.
If no percentage is given, the page is scrolled to the end.
Args:
perc: Percentage to scroll.
horizontal: Scroll horizontally instead of vertically.
count: Percentage to scroll.
"""
if perc is None and count is None:
perc = 100
elif count is not None:
perc = count
if horizontal:
x = perc
y = None
else:
x = None
y = perc
tab.scroller.before_jump_requested.emit()
tab.scroller.to_perc(x, y)
@cmdutils.register()
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
def scroll_to_anchor(tab: apitypes.Tab, name: str) -> None:
"""Scroll to the given anchor in the document.
Args:
name: The anchor to scroll to.
"""
tab.scroller.before_jump_requested.emit()
tab.scroller.to_anchor(name)

View File

@ -0,0 +1,95 @@
# 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/>.
"""Zooming-related commands."""
from qutebrowser.api import cmdutils, apitypes, message, config
@cmdutils.register()
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
@cmdutils.argument('count', value=cmdutils.Value.count)
def zoom_in(tab: apitypes.Tab, count: int = 1, quiet: bool = False) -> None:
"""Increase the zoom level for the current tab.
Args:
count: How many steps to zoom in.
quiet: Don't show a zoom level message.
"""
try:
perc = tab.zoom.apply_offset(count)
except ValueError as e:
raise cmdutils.CommandError(e)
if not quiet:
message.info("Zoom level: {}%".format(int(perc)), replace=True)
@cmdutils.register()
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
@cmdutils.argument('count', value=cmdutils.Value.count)
def zoom_out(tab: apitypes.Tab, count: int = 1, quiet: bool = False) -> None:
"""Decrease the zoom level for the current tab.
Args:
count: How many steps to zoom out.
quiet: Don't show a zoom level message.
"""
try:
perc = tab.zoom.apply_offset(-count)
except ValueError as e:
raise cmdutils.CommandError(e)
if not quiet:
message.info("Zoom level: {}%".format(int(perc)), replace=True)
@cmdutils.register()
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
@cmdutils.argument('count', value=cmdutils.Value.count)
def zoom(tab: apitypes.Tab,
level: str = None,
count: int = None,
quiet: bool = False) -> None:
"""Set the zoom level for the current tab.
The zoom can be given as argument or as [count]. If neither is
given, the zoom is set to the default zoom. If both are given,
use [count].
Args:
level: The zoom percentage to set.
count: The zoom percentage to set.
quiet: Don't show a zoom level message.
"""
if count is not None:
int_level = count
elif level is not None:
try:
int_level = int(level.rstrip('%'))
except ValueError:
raise cmdutils.CommandError("zoom: Invalid int value {}"
.format(level))
else:
int_level = int(config.val.zoom.default)
try:
tab.zoom.set_factor(int_level / 100)
except ValueError:
raise cmdutils.CommandError("Can't zoom {}%!".format(int_level))
if not quiet:
message.info("Zoom level: {}%".format(int_level), replace=True)

View File

@ -22,19 +22,28 @@
import copy import copy
import contextlib import contextlib
import functools import functools
import typing
from typing import Any
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl
from qutebrowser.config import configdata, configexc, configutils from qutebrowser.config import configdata, configexc, configutils
from qutebrowser.utils import utils, log, jinja from qutebrowser.utils import utils, log, jinja, urlmatch
from qutebrowser.misc import objects from qutebrowser.misc import objects
from qutebrowser.keyinput import keyutils from qutebrowser.keyinput import keyutils
MYPY = False
if MYPY:
# pylint: disable=unused-import,useless-suppression
from typing import Tuple, MutableMapping
from qutebrowser.config import configcache, configfiles
from qutebrowser.misc import savemanager
# An easy way to access the config from other code via config.val.foo # An easy way to access the config from other code via config.val.foo
val = None val = typing.cast('ConfigContainer', None)
instance = None instance = typing.cast('Config', None)
key_instance = None key_instance = typing.cast('KeyConfig', None)
cache = None cache = typing.cast('configcache.ConfigCache', None)
# Keeping track of all change filters to validate them later. # Keeping track of all change filters to validate them later.
change_filters = [] change_filters = []
@ -55,7 +64,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name
_function: Whether a function rather than a method is decorated. _function: Whether a function rather than a method is decorated.
""" """
def __init__(self, option, function=False): def __init__(self, option: str, function: bool = False) -> None:
"""Save decorator arguments. """Save decorator arguments.
Gets called on parse-time with the decorator arguments. Gets called on parse-time with the decorator arguments.
@ -68,7 +77,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name
self._function = function self._function = function
change_filters.append(self) change_filters.append(self)
def validate(self): def validate(self) -> None:
"""Make sure the configured option or prefix exists. """Make sure the configured option or prefix exists.
We can't do this in __init__ as configdata isn't ready yet. We can't do this in __init__ as configdata isn't ready yet.
@ -77,7 +86,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name
not configdata.is_valid_prefix(self._option)): not configdata.is_valid_prefix(self._option)):
raise configexc.NoOptionError(self._option) raise configexc.NoOptionError(self._option)
def _check_match(self, option): def check_match(self, option: typing.Optional[str]) -> bool:
"""Check if the given option matches the filter.""" """Check if the given option matches the filter."""
if option is None: if option is None:
# Called directly, not from a config change event. # Called directly, not from a config change event.
@ -90,7 +99,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name
else: else:
return False return False
def __call__(self, func): def __call__(self, func: typing.Callable) -> typing.Callable:
"""Filter calls to the decorated function. """Filter calls to the decorated function.
Gets called when a function should be decorated. Gets called when a function should be decorated.
@ -108,20 +117,21 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name
""" """
if self._function: if self._function:
@functools.wraps(func) @functools.wraps(func)
def wrapper(option=None): def func_wrapper(option: str = None) -> typing.Any:
"""Call the underlying function.""" """Call the underlying function."""
if self._check_match(option): if self.check_match(option):
return func() return func()
return None return None
return func_wrapper
else: else:
@functools.wraps(func) @functools.wraps(func)
def wrapper(wrapper_self, option=None): def meth_wrapper(wrapper_self: typing.Any,
option: str = None) -> typing.Any:
"""Call the underlying function.""" """Call the underlying function."""
if self._check_match(option): if self.check_match(option):
return func(wrapper_self) return func(wrapper_self)
return None return None
return meth_wrapper
return wrapper
class KeyConfig: class KeyConfig:
@ -134,17 +144,22 @@ class KeyConfig:
_config: The Config object to be used. _config: The Config object to be used.
""" """
def __init__(self, config): _ReverseBindings = typing.Dict[str, typing.MutableSequence[str]]
def __init__(self, config: 'Config') -> None:
self._config = config self._config = config
def _validate(self, key, mode): def _validate(self, key: keyutils.KeySequence, mode: str) -> None:
"""Validate the given key and mode.""" """Validate the given key and mode."""
# Catch old usage of this code # Catch old usage of this code
assert isinstance(key, keyutils.KeySequence), key assert isinstance(key, keyutils.KeySequence), key
if mode not in configdata.DATA['bindings.default'].default: if mode not in configdata.DATA['bindings.default'].default:
raise configexc.KeybindingError("Invalid mode {}!".format(mode)) raise configexc.KeybindingError("Invalid mode {}!".format(mode))
def get_bindings_for(self, mode): def get_bindings_for(
self,
mode: str
) -> typing.Dict[keyutils.KeySequence, str]:
"""Get the combined bindings for the given mode.""" """Get the combined bindings for the given mode."""
bindings = dict(val.bindings.default[mode]) bindings = dict(val.bindings.default[mode])
for key, binding in val.bindings.commands[mode].items(): for key, binding in val.bindings.commands[mode].items():
@ -154,9 +169,9 @@ class KeyConfig:
bindings[key] = binding bindings[key] = binding
return bindings return bindings
def get_reverse_bindings_for(self, mode): def get_reverse_bindings_for(self, mode: str) -> '_ReverseBindings':
"""Get a dict of commands to a list of bindings for the mode.""" """Get a dict of commands to a list of bindings for the mode."""
cmd_to_keys = {} cmd_to_keys = {} # type: KeyConfig._ReverseBindings
bindings = self.get_bindings_for(mode) bindings = self.get_bindings_for(mode)
for seq, full_cmd in sorted(bindings.items()): for seq, full_cmd in sorted(bindings.items()):
for cmd in full_cmd.split(';;'): for cmd in full_cmd.split(';;'):
@ -169,7 +184,10 @@ class KeyConfig:
cmd_to_keys[cmd].insert(0, str(seq)) cmd_to_keys[cmd].insert(0, str(seq))
return cmd_to_keys return cmd_to_keys
def get_command(self, key, mode, default=False): def get_command(self,
key: keyutils.KeySequence,
mode: str,
default: bool = False) -> str:
"""Get the command for a given key (or None).""" """Get the command for a given key (or None)."""
self._validate(key, mode) self._validate(key, mode)
if default: if default:
@ -178,7 +196,11 @@ class KeyConfig:
bindings = self.get_bindings_for(mode) bindings = self.get_bindings_for(mode)
return bindings.get(key, None) return bindings.get(key, None)
def bind(self, key, command, *, mode, save_yaml=False): def bind(self,
key: keyutils.KeySequence,
command: str, *,
mode: str,
save_yaml: bool = False) -> None:
"""Add a new binding from key to command.""" """Add a new binding from key to command."""
if command is not None and not command.strip(): if command is not None and not command.strip():
raise configexc.KeybindingError( raise configexc.KeybindingError(
@ -186,8 +208,8 @@ class KeyConfig:
'mode'.format(key, mode)) 'mode'.format(key, mode))
self._validate(key, mode) self._validate(key, mode)
log.keyboard.vdebug("Adding binding {} -> {} in mode {}.".format( log.keyboard.vdebug( # type: ignore
key, command, mode)) "Adding binding {} -> {} in mode {}.".format(key, command, mode))
bindings = self._config.get_mutable_obj('bindings.commands') bindings = self._config.get_mutable_obj('bindings.commands')
if mode not in bindings: if mode not in bindings:
@ -195,7 +217,10 @@ class KeyConfig:
bindings[mode][str(key)] = command bindings[mode][str(key)] = command
self._config.update_mutables(save_yaml=save_yaml) self._config.update_mutables(save_yaml=save_yaml)
def bind_default(self, key, *, mode='normal', save_yaml=False): def bind_default(self,
key: keyutils.KeySequence, *,
mode: str = 'normal',
save_yaml: bool = False) -> None:
"""Restore a default keybinding.""" """Restore a default keybinding."""
self._validate(key, mode) self._validate(key, mode)
@ -207,7 +232,10 @@ class KeyConfig:
"Can't find binding '{}' in {} mode".format(key, mode)) "Can't find binding '{}' in {} mode".format(key, mode))
self._config.update_mutables(save_yaml=save_yaml) self._config.update_mutables(save_yaml=save_yaml)
def unbind(self, key, *, mode='normal', save_yaml=False): def unbind(self,
key: keyutils.KeySequence, *,
mode: str = 'normal',
save_yaml: bool = False) -> None:
"""Unbind the given key in the given mode.""" """Unbind the given key in the given mode."""
self._validate(key, mode) self._validate(key, mode)
@ -248,24 +276,27 @@ class Config(QObject):
MUTABLE_TYPES = (dict, list) MUTABLE_TYPES = (dict, list)
changed = pyqtSignal(str) changed = pyqtSignal(str)
def __init__(self, yaml_config, parent=None): def __init__(self,
yaml_config: 'configfiles.YamlConfig',
parent: QObject = None) -> None:
super().__init__(parent) super().__init__(parent)
self.changed.connect(_render_stylesheet.cache_clear) self.changed.connect(_render_stylesheet.cache_clear)
self._mutables = {} self._mutables = {} # type: MutableMapping[str, Tuple[Any, Any]]
self._yaml = yaml_config self._yaml = yaml_config
self._init_values() self._init_values()
def _init_values(self): def _init_values(self) -> None:
"""Populate the self._values dict.""" """Populate the self._values dict."""
self._values = {} self._values = {} # type: typing.Mapping
for name, opt in configdata.DATA.items(): for name, opt in configdata.DATA.items():
self._values[name] = configutils.Values(opt) self._values[name] = configutils.Values(opt)
def __iter__(self): def __iter__(self) -> typing.Iterator[configutils.Values]:
"""Iterate over configutils.Values items.""" """Iterate over configutils.Values items."""
yield from self._values.values() yield from self._values.values()
def init_save_manager(self, save_manager): def init_save_manager(self,
save_manager: 'savemanager.SaveManager') -> None:
"""Make sure the config gets saved properly. """Make sure the config gets saved properly.
We do this outside of __init__ because the config gets created before We do this outside of __init__ because the config gets created before
@ -273,7 +304,10 @@ class Config(QObject):
""" """
self._yaml.init_save_manager(save_manager) self._yaml.init_save_manager(save_manager)
def _set_value(self, opt, value, pattern=None): def _set_value(self,
opt: 'configdata.Option',
value: Any,
pattern: urlmatch.UrlPattern = None) -> None:
"""Set the given option to the given value.""" """Set the given option to the given value."""
if not isinstance(objects.backend, objects.NoBackend): if not isinstance(objects.backend, objects.NoBackend):
if objects.backend not in opt.backends: if objects.backend not in opt.backends:
@ -288,12 +322,12 @@ class Config(QObject):
log.config.debug("Config option changed: {} = {}".format( log.config.debug("Config option changed: {} = {}".format(
opt.name, value)) opt.name, value))
def _check_yaml(self, opt, save_yaml): def _check_yaml(self, opt: 'configdata.Option', save_yaml: bool) -> None:
"""Make sure the given option may be set in autoconfig.yml.""" """Make sure the given option may be set in autoconfig.yml."""
if save_yaml and opt.no_autoconfig: if save_yaml and opt.no_autoconfig:
raise configexc.NoAutoconfigError(opt.name) raise configexc.NoAutoconfigError(opt.name)
def read_yaml(self): def read_yaml(self) -> None:
"""Read the YAML settings from self._yaml.""" """Read the YAML settings from self._yaml."""
self._yaml.load() self._yaml.load()
for values in self._yaml: for values in self._yaml:
@ -301,7 +335,7 @@ class Config(QObject):
self._set_value(values.opt, scoped.value, self._set_value(values.opt, scoped.value,
pattern=scoped.pattern) pattern=scoped.pattern)
def get_opt(self, name): def get_opt(self, name: str) -> 'configdata.Option':
"""Get a configdata.Option object for the given setting.""" """Get a configdata.Option object for the given setting."""
try: try:
return configdata.DATA[name] return configdata.DATA[name]
@ -312,7 +346,10 @@ class Config(QObject):
name, deleted=deleted, renamed=renamed) name, deleted=deleted, renamed=renamed)
raise exception from None raise exception from None
def get(self, name, url=None, *, fallback=True): def get(self,
name: str,
url: QUrl = None, *,
fallback: bool = True) -> Any:
"""Get the given setting converted for Python code. """Get the given setting converted for Python code.
Args: Args:
@ -322,7 +359,7 @@ class Config(QObject):
obj = self.get_obj(name, url=url, fallback=fallback) obj = self.get_obj(name, url=url, fallback=fallback)
return opt.typ.to_py(obj) return opt.typ.to_py(obj)
def _maybe_copy(self, value): def _maybe_copy(self, value: Any) -> Any:
"""Copy the value if it could potentially be mutated.""" """Copy the value if it could potentially be mutated."""
if isinstance(value, self.MUTABLE_TYPES): if isinstance(value, self.MUTABLE_TYPES):
# For mutable objects, create a copy so we don't accidentally # For mutable objects, create a copy so we don't accidentally
@ -333,7 +370,10 @@ class Config(QObject):
assert value.__hash__ is not None, value assert value.__hash__ is not None, value
return value return value
def get_obj(self, name, *, url=None, fallback=True): def get_obj(self,
name: str, *,
url: QUrl = None,
fallback: bool = True) -> Any:
"""Get the given setting as object (for YAML/config.py). """Get the given setting as object (for YAML/config.py).
Note that the returned values are not watched for mutation. Note that the returned values are not watched for mutation.
@ -343,7 +383,10 @@ class Config(QObject):
value = self._values[name].get_for_url(url, fallback=fallback) value = self._values[name].get_for_url(url, fallback=fallback)
return self._maybe_copy(value) return self._maybe_copy(value)
def get_obj_for_pattern(self, name, *, pattern): def get_obj_for_pattern(
self, name: str, *,
pattern: typing.Optional[urlmatch.UrlPattern]
) -> Any:
"""Get the given setting as object (for YAML/config.py). """Get the given setting as object (for YAML/config.py).
This gets the overridden value for a given pattern, or This gets the overridden value for a given pattern, or
@ -353,11 +396,12 @@ class Config(QObject):
value = self._values[name].get_for_pattern(pattern, fallback=False) value = self._values[name].get_for_pattern(pattern, fallback=False)
return self._maybe_copy(value) return self._maybe_copy(value)
def get_mutable_obj(self, name, *, pattern=None): def get_mutable_obj(self, name: str, *,
pattern: urlmatch.UrlPattern = None) -> Any:
"""Get an object which can be mutated, e.g. in a config.py. """Get an object which can be mutated, e.g. in a config.py.
If a pattern is given, return the value for that pattern. If a pattern is given, return the value for that pattern.
Note that it's impossible to get a mutable object for an URL as we Note that it's impossible to get a mutable object for a URL as we
wouldn't know what pattern to apply. wouldn't know what pattern to apply.
""" """
self.get_opt(name) # To make sure it exists self.get_opt(name) # To make sure it exists
@ -378,7 +422,8 @@ class Config(QObject):
return copy_value return copy_value
def get_str(self, name, *, pattern=None): def get_str(self, name: str, *,
pattern: urlmatch.UrlPattern = None) -> str:
"""Get the given setting as string. """Get the given setting as string.
If a pattern is given, get the setting for the given pattern or If a pattern is given, get the setting for the given pattern or
@ -389,7 +434,10 @@ class Config(QObject):
value = values.get_for_pattern(pattern) value = values.get_for_pattern(pattern)
return opt.typ.to_str(value) return opt.typ.to_str(value)
def set_obj(self, name, value, *, pattern=None, save_yaml=False): def set_obj(self, name: str,
value: Any, *,
pattern: urlmatch.UrlPattern = None,
save_yaml: bool = False) -> None:
"""Set the given setting from a YAML/config.py object. """Set the given setting from a YAML/config.py object.
If save_yaml=True is given, store the new value to YAML. If save_yaml=True is given, store the new value to YAML.
@ -400,7 +448,10 @@ class Config(QObject):
if save_yaml: if save_yaml:
self._yaml.set_obj(name, value, pattern=pattern) self._yaml.set_obj(name, value, pattern=pattern)
def set_str(self, name, value, *, pattern=None, save_yaml=False): def set_str(self, name: str,
value: str, *,
pattern: urlmatch.UrlPattern = None,
save_yaml: bool = False) -> None:
"""Set the given setting from a string. """Set the given setting from a string.
If save_yaml=True is given, store the new value to YAML. If save_yaml=True is given, store the new value to YAML.
@ -415,7 +466,9 @@ class Config(QObject):
if save_yaml: if save_yaml:
self._yaml.set_obj(name, converted, pattern=pattern) self._yaml.set_obj(name, converted, pattern=pattern)
def unset(self, name, *, save_yaml=False, pattern=None): def unset(self, name: str, *,
save_yaml: bool = False,
pattern: urlmatch.UrlPattern = None) -> None:
"""Set the given setting back to its default.""" """Set the given setting back to its default."""
opt = self.get_opt(name) opt = self.get_opt(name)
self._check_yaml(opt, save_yaml) self._check_yaml(opt, save_yaml)
@ -426,7 +479,7 @@ class Config(QObject):
if save_yaml: if save_yaml:
self._yaml.unset(name, pattern=pattern) self._yaml.unset(name, pattern=pattern)
def clear(self, *, save_yaml=False): def clear(self, *, save_yaml: bool = False) -> None:
"""Clear all settings in the config. """Clear all settings in the config.
If save_yaml=True is given, also remove all customization from the YAML If save_yaml=True is given, also remove all customization from the YAML
@ -440,7 +493,7 @@ class Config(QObject):
if save_yaml: if save_yaml:
self._yaml.clear() self._yaml.clear()
def update_mutables(self, *, save_yaml=False): def update_mutables(self, *, save_yaml: bool = False) -> None:
"""Update mutable settings if they changed. """Update mutable settings if they changed.
Every time someone calls get_obj() on a mutable object, we save a Every time someone calls get_obj() on a mutable object, we save a
@ -455,7 +508,7 @@ class Config(QObject):
self.set_obj(name, new_value, save_yaml=save_yaml) self.set_obj(name, new_value, save_yaml=save_yaml)
self._mutables = {} self._mutables = {}
def dump_userconfig(self): def dump_userconfig(self) -> str:
"""Get the part of the config which was changed by the user. """Get the part of the config which was changed by the user.
Return: Return:
@ -484,7 +537,10 @@ class ConfigContainer:
_pattern: The URL pattern to be used. _pattern: The URL pattern to be used.
""" """
def __init__(self, config, configapi=None, prefix='', pattern=None): def __init__(self, config: Config,
configapi: 'configfiles.ConfigAPI' = None,
prefix: str = '',
pattern: urlmatch.UrlPattern = None) -> None:
self._config = config self._config = config
self._prefix = prefix self._prefix = prefix
self._configapi = configapi self._configapi = configapi
@ -492,13 +548,13 @@ class ConfigContainer:
if configapi is None and pattern is not None: if configapi is None and pattern is not None:
raise TypeError("Can't use pattern without configapi!") raise TypeError("Can't use pattern without configapi!")
def __repr__(self): def __repr__(self) -> str:
return utils.get_repr(self, constructor=True, config=self._config, return utils.get_repr(self, constructor=True, config=self._config,
configapi=self._configapi, prefix=self._prefix, configapi=self._configapi, prefix=self._prefix,
pattern=self._pattern) pattern=self._pattern)
@contextlib.contextmanager @contextlib.contextmanager
def _handle_error(self, action, name): def _handle_error(self, action: str, name: str) -> typing.Iterator[None]:
try: try:
yield yield
except configexc.Error as e: except configexc.Error as e:
@ -507,7 +563,7 @@ class ConfigContainer:
text = "While {} '{}'".format(action, name) text = "While {} '{}'".format(action, name)
self._configapi.errors.append(configexc.ConfigErrorDesc(text, e)) self._configapi.errors.append(configexc.ConfigErrorDesc(text, e))
def __getattr__(self, attr): def __getattr__(self, attr: str) -> Any:
"""Get an option or a new ConfigContainer with the added prefix. """Get an option or a new ConfigContainer with the added prefix.
If we get an option which exists, we return the value for it. If we get an option which exists, we return the value for it.
@ -534,7 +590,7 @@ class ConfigContainer:
return self._config.get_mutable_obj( return self._config.get_mutable_obj(
name, pattern=self._pattern) name, pattern=self._pattern)
def __setattr__(self, attr, value): def __setattr__(self, attr: str, value: Any) -> None:
"""Set the given option in the config.""" """Set the given option in the config."""
if attr.startswith('_'): if attr.startswith('_'):
super().__setattr__(attr, value) super().__setattr__(attr, value)
@ -544,7 +600,7 @@ class ConfigContainer:
with self._handle_error('setting', name): with self._handle_error('setting', name):
self._config.set_obj(name, value, pattern=self._pattern) self._config.set_obj(name, value, pattern=self._pattern)
def _join(self, attr): def _join(self, attr: str) -> str:
"""Get the prefix joined with the given attribute.""" """Get the prefix joined with the given attribute."""
if self._prefix: if self._prefix:
return '{}.{}'.format(self._prefix, attr) return '{}.{}'.format(self._prefix, attr)
@ -552,8 +608,10 @@ class ConfigContainer:
return attr return attr
def set_register_stylesheet(obj, *, stylesheet=None, update=True): def set_register_stylesheet(obj: QObject, *,
"""Set the stylesheet for an object based on it's STYLESHEET attribute. stylesheet: str = None,
update: bool = True) -> None:
"""Set the stylesheet for an object.
Also, register an update when the config is changed. Also, register an update when the config is changed.
@ -568,7 +626,7 @@ def set_register_stylesheet(obj, *, stylesheet=None, update=True):
@functools.lru_cache() @functools.lru_cache()
def _render_stylesheet(stylesheet): def _render_stylesheet(stylesheet: str) -> str:
"""Render the given stylesheet jinja template.""" """Render the given stylesheet jinja template."""
with jinja.environment.no_autoescape(): with jinja.environment.no_autoescape():
template = jinja.environment.from_string(stylesheet) template = jinja.environment.from_string(stylesheet)
@ -584,7 +642,9 @@ class StyleSheetObserver(QObject):
_stylesheet: The stylesheet template to use. _stylesheet: The stylesheet template to use.
""" """
def __init__(self, obj, stylesheet, update): def __init__(self, obj: QObject,
stylesheet: typing.Optional[str],
update: bool) -> None:
super().__init__() super().__init__()
self._obj = obj self._obj = obj
self._update = update self._update = update
@ -593,11 +653,11 @@ class StyleSheetObserver(QObject):
if self._update: if self._update:
self.setParent(self._obj) self.setParent(self._obj)
if stylesheet is None: if stylesheet is None:
self._stylesheet = obj.STYLESHEET self._stylesheet = obj.STYLESHEET # type: str
else: else:
self._stylesheet = stylesheet self._stylesheet = stylesheet
def _get_stylesheet(self): def _get_stylesheet(self) -> str:
"""Format a stylesheet based on a template. """Format a stylesheet based on a template.
Return: Return:
@ -606,19 +666,15 @@ class StyleSheetObserver(QObject):
return _render_stylesheet(self._stylesheet) return _render_stylesheet(self._stylesheet)
@pyqtSlot() @pyqtSlot()
def _update_stylesheet(self): def _update_stylesheet(self) -> None:
"""Update the stylesheet for obj.""" """Update the stylesheet for obj."""
self._obj.setStyleSheet(self._get_stylesheet()) self._obj.setStyleSheet(self._get_stylesheet())
def register(self): def register(self) -> None:
"""Do a first update and listen for more. """Do a first update and listen for more."""
Args:
update: if False, don't listen for future updates.
"""
qss = self._get_stylesheet() qss = self._get_stylesheet()
log.config.vdebug("stylesheet for {}: {}".format( log.config.vdebug( # type: ignore
self._obj.__class__.__name__, qss)) "stylesheet for {}: {}".format(self._obj.__class__.__name__, qss))
self._obj.setStyleSheet(qss) self._obj.setStyleSheet(qss)
if self._update: if self._update:
instance.changed.connect(self._update_stylesheet) instance.changed.connect(self._update_stylesheet)

View File

@ -20,6 +20,8 @@
"""Implementation of a basic config cache.""" """Implementation of a basic config cache."""
import typing
from qutebrowser.config import config from qutebrowser.config import config
@ -36,14 +38,14 @@ class ConfigCache:
""" """
def __init__(self) -> None: def __init__(self) -> None:
self._cache = {} self._cache = {} # type: typing.Dict[str, typing.Any]
config.instance.changed.connect(self._on_config_changed) config.instance.changed.connect(self._on_config_changed)
def _on_config_changed(self, attr: str) -> None: def _on_config_changed(self, attr: str) -> None:
if attr in self._cache: if attr in self._cache:
self._cache[attr] = config.instance.get(attr) self._cache[attr] = config.instance.get(attr)
def __getitem__(self, attr: str): def __getitem__(self, attr: str) -> typing.Any:
if attr not in self._cache: if attr not in self._cache:
assert not config.instance.get_opt(attr).supports_pattern assert not config.instance.get_opt(attr).supports_pattern
self._cache[attr] = config.instance.get(attr) self._cache[attr] = config.instance.get(attr)

View File

@ -19,36 +19,47 @@
"""Commands related to the configuration.""" """Commands related to the configuration."""
import typing
import os.path import os.path
import contextlib import contextlib
from PyQt5.QtCore import QUrl from PyQt5.QtCore import QUrl
from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.api import cmdutils
from qutebrowser.completion.models import configmodel from qutebrowser.completion.models import configmodel
from qutebrowser.utils import objreg, message, standarddir, urlmatch from qutebrowser.utils import objreg, message, standarddir, urlmatch
from qutebrowser.config import configtypes, configexc, configfiles, configdata from qutebrowser.config import configtypes, configexc, configfiles, configdata
from qutebrowser.misc import editor from qutebrowser.misc import editor
from qutebrowser.keyinput import keyutils from qutebrowser.keyinput import keyutils
MYPY = False
if MYPY:
# pylint: disable=unused-import,useless-suppression
from qutebrowser.config.config import Config, KeyConfig
class ConfigCommands: class ConfigCommands:
"""qutebrowser commands related to the configuration.""" """qutebrowser commands related to the configuration."""
def __init__(self, config, keyconfig): def __init__(self,
config: 'Config',
keyconfig: 'KeyConfig') -> None:
self._config = config self._config = config
self._keyconfig = keyconfig self._keyconfig = keyconfig
@contextlib.contextmanager @contextlib.contextmanager
def _handle_config_error(self): def _handle_config_error(self) -> typing.Iterator[None]:
"""Catch errors in set_command and raise CommandError.""" """Catch errors in set_command and raise CommandError."""
try: try:
yield yield
except configexc.Error as e: except configexc.Error as e:
raise cmdexc.CommandError(str(e)) raise cmdutils.CommandError(str(e))
def _parse_pattern(self, pattern): def _parse_pattern(
self,
pattern: typing.Optional[str]
) -> typing.Optional[urlmatch.UrlPattern]:
"""Parse a pattern string argument to a pattern.""" """Parse a pattern string argument to a pattern."""
if pattern is None: if pattern is None:
return None return None
@ -56,17 +67,18 @@ class ConfigCommands:
try: try:
return urlmatch.UrlPattern(pattern) return urlmatch.UrlPattern(pattern)
except urlmatch.ParseError as e: except urlmatch.ParseError as e:
raise cmdexc.CommandError("Error while parsing {}: {}" raise cmdutils.CommandError("Error while parsing {}: {}"
.format(pattern, str(e))) .format(pattern, str(e)))
def _parse_key(self, key): def _parse_key(self, key: str) -> keyutils.KeySequence:
"""Parse a key argument.""" """Parse a key argument."""
try: try:
return keyutils.KeySequence.parse(key) return keyutils.KeySequence.parse(key)
except keyutils.KeyParseError as e: except keyutils.KeyParseError as e:
raise cmdexc.CommandError(str(e)) raise cmdutils.CommandError(str(e))
def _print_value(self, option, pattern): def _print_value(self, option: str,
pattern: typing.Optional[urlmatch.UrlPattern]) -> None:
"""Print the value of the given option.""" """Print the value of the given option."""
with self._handle_config_error(): with self._handle_config_error():
value = self._config.get_str(option, pattern=pattern) value = self._config.get_str(option, pattern=pattern)
@ -79,10 +91,11 @@ class ConfigCommands:
@cmdutils.register(instance='config-commands') @cmdutils.register(instance='config-commands')
@cmdutils.argument('option', completion=configmodel.option) @cmdutils.argument('option', completion=configmodel.option)
@cmdutils.argument('value', completion=configmodel.value) @cmdutils.argument('value', completion=configmodel.value)
@cmdutils.argument('win_id', win_id=True) @cmdutils.argument('win_id', value=cmdutils.Value.win_id)
@cmdutils.argument('pattern', flag='u') @cmdutils.argument('pattern', flag='u')
def set(self, win_id, option=None, value=None, temp=False, print_=False, def set(self, win_id: int, option: str = None, value: str = None,
*, pattern=None): temp: bool = False, print_: bool = False,
*, pattern: str = None) -> None:
"""Set an option. """Set an option.
If the option name ends with '?' or no value is provided, the If the option name ends with '?' or no value is provided, the
@ -101,35 +114,35 @@ class ConfigCommands:
if option is None: if option is None:
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id) window=win_id)
tabbed_browser.openurl(QUrl('qute://settings'), newtab=False) tabbed_browser.load_url(QUrl('qute://settings'), newtab=False)
return return
if option.endswith('!'): if option.endswith('!'):
raise cmdexc.CommandError("Toggling values was moved to the " raise cmdutils.CommandError("Toggling values was moved to the "
":config-cycle command") ":config-cycle command")
pattern = self._parse_pattern(pattern) parsed_pattern = self._parse_pattern(pattern)
if option.endswith('?') and option != '?': if option.endswith('?') and option != '?':
self._print_value(option[:-1], pattern=pattern) self._print_value(option[:-1], pattern=parsed_pattern)
return return
with self._handle_config_error(): with self._handle_config_error():
if value is None: if value is None:
self._print_value(option, pattern=pattern) self._print_value(option, pattern=parsed_pattern)
else: else:
self._config.set_str(option, value, pattern=pattern, self._config.set_str(option, value, pattern=parsed_pattern,
save_yaml=not temp) save_yaml=not temp)
if print_: if print_:
self._print_value(option, pattern=pattern) self._print_value(option, pattern=parsed_pattern)
@cmdutils.register(instance='config-commands', maxsplit=1, @cmdutils.register(instance='config-commands', maxsplit=1,
no_cmd_split=True, no_replace_variables=True) no_cmd_split=True, no_replace_variables=True)
@cmdutils.argument('command', completion=configmodel.bind) @cmdutils.argument('command', completion=configmodel.bind)
@cmdutils.argument('win_id', win_id=True) @cmdutils.argument('win_id', value=cmdutils.Value.win_id)
def bind(self, win_id, key=None, command=None, *, mode='normal', def bind(self, win_id: str, key: str = None, command: str = None, *,
default=False): mode: str = 'normal', default: bool = False) -> None:
"""Bind a key to a command. """Bind a key to a command.
If no command is given, show the current binding for the given key. If no command is given, show the current binding for the given key.
@ -147,7 +160,7 @@ class ConfigCommands:
if key is None: if key is None:
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id) window=win_id)
tabbed_browser.openurl(QUrl('qute://bindings'), newtab=True) tabbed_browser.load_url(QUrl('qute://bindings'), newtab=True)
return return
seq = self._parse_key(key) seq = self._parse_key(key)
@ -174,7 +187,7 @@ class ConfigCommands:
self._keyconfig.bind(seq, command, mode=mode, save_yaml=True) self._keyconfig.bind(seq, command, mode=mode, save_yaml=True)
@cmdutils.register(instance='config-commands') @cmdutils.register(instance='config-commands')
def unbind(self, key, *, mode='normal'): def unbind(self, key: str, *, mode: str = 'normal') -> None:
"""Unbind a keychain. """Unbind a keychain.
Args: Args:
@ -191,8 +204,9 @@ class ConfigCommands:
@cmdutils.argument('option', completion=configmodel.option) @cmdutils.argument('option', completion=configmodel.option)
@cmdutils.argument('values', completion=configmodel.value) @cmdutils.argument('values', completion=configmodel.value)
@cmdutils.argument('pattern', flag='u') @cmdutils.argument('pattern', flag='u')
def config_cycle(self, option, *values, pattern=None, temp=False, def config_cycle(self, option: str, *values: str,
print_=False): pattern: str = None,
temp: bool = False, print_: bool = False) -> None:
"""Cycle an option between multiple values. """Cycle an option between multiple values.
Args: Args:
@ -202,42 +216,42 @@ class ConfigCommands:
temp: Set value temporarily until qutebrowser is closed. temp: Set value temporarily until qutebrowser is closed.
print_: Print the value after setting. print_: Print the value after setting.
""" """
pattern = self._parse_pattern(pattern) parsed_pattern = self._parse_pattern(pattern)
with self._handle_config_error(): with self._handle_config_error():
opt = self._config.get_opt(option) opt = self._config.get_opt(option)
old_value = self._config.get_obj_for_pattern(option, old_value = self._config.get_obj_for_pattern(
pattern=pattern) option, pattern=parsed_pattern)
if not values and isinstance(opt.typ, configtypes.Bool): if not values and isinstance(opt.typ, configtypes.Bool):
values = ['true', 'false'] values = ('true', 'false')
if len(values) < 2: if len(values) < 2:
raise cmdexc.CommandError("Need at least two values for " raise cmdutils.CommandError("Need at least two values for "
"non-boolean settings.") "non-boolean settings.")
# Use the next valid value from values, or the first if the current # Use the next valid value from values, or the first if the current
# value does not appear in the list # value does not appear in the list
with self._handle_config_error(): with self._handle_config_error():
values = [opt.typ.from_str(val) for val in values] cycle_values = [opt.typ.from_str(val) for val in values]
try: try:
idx = values.index(old_value) idx = cycle_values.index(old_value)
idx = (idx + 1) % len(values) idx = (idx + 1) % len(cycle_values)
value = values[idx] value = cycle_values[idx]
except ValueError: except ValueError:
value = values[0] value = cycle_values[0]
with self._handle_config_error(): with self._handle_config_error():
self._config.set_obj(option, value, pattern=pattern, self._config.set_obj(option, value, pattern=parsed_pattern,
save_yaml=not temp) save_yaml=not temp)
if print_: if print_:
self._print_value(option, pattern=pattern) self._print_value(option, pattern=parsed_pattern)
@cmdutils.register(instance='config-commands') @cmdutils.register(instance='config-commands')
@cmdutils.argument('option', completion=configmodel.customized_option) @cmdutils.argument('option', completion=configmodel.customized_option)
def config_unset(self, option, temp=False): def config_unset(self, option: str, temp: bool = False) -> None:
"""Unset an option. """Unset an option.
This sets an option back to its default and removes it from This sets an option back to its default and removes it from
@ -252,7 +266,8 @@ class ConfigCommands:
@cmdutils.register(instance='config-commands') @cmdutils.register(instance='config-commands')
@cmdutils.argument('option', completion=configmodel.list_option) @cmdutils.argument('option', completion=configmodel.list_option)
def config_list_add(self, option, value, temp=False): def config_list_add(self, option: str, value: str,
temp: bool = False) -> None:
"""Append a value to a config option that is a list. """Append a value to a config option that is a list.
Args: Args:
@ -263,8 +278,8 @@ class ConfigCommands:
opt = self._config.get_opt(option) opt = self._config.get_opt(option)
valid_list_types = (configtypes.List, configtypes.ListOrValue) valid_list_types = (configtypes.List, configtypes.ListOrValue)
if not isinstance(opt.typ, valid_list_types): if not isinstance(opt.typ, valid_list_types):
raise cmdexc.CommandError(":config-list-add can only be used for " raise cmdutils.CommandError(":config-list-add can only be used "
"lists") "for lists")
with self._handle_config_error(): with self._handle_config_error():
option_value = self._config.get_mutable_obj(option) option_value = self._config.get_mutable_obj(option)
@ -273,7 +288,8 @@ class ConfigCommands:
@cmdutils.register(instance='config-commands') @cmdutils.register(instance='config-commands')
@cmdutils.argument('option', completion=configmodel.dict_option) @cmdutils.argument('option', completion=configmodel.dict_option)
def config_dict_add(self, option, key, value, temp=False, replace=False): def config_dict_add(self, option: str, key: str, value: str,
temp: bool = False, replace: bool = False) -> None:
"""Add a key/value pair to a dictionary option. """Add a key/value pair to a dictionary option.
Args: Args:
@ -286,14 +302,14 @@ class ConfigCommands:
""" """
opt = self._config.get_opt(option) opt = self._config.get_opt(option)
if not isinstance(opt.typ, configtypes.Dict): if not isinstance(opt.typ, configtypes.Dict):
raise cmdexc.CommandError(":config-dict-add can only be used for " raise cmdutils.CommandError(":config-dict-add can only be used "
"dicts") "for dicts")
with self._handle_config_error(): with self._handle_config_error():
option_value = self._config.get_mutable_obj(option) option_value = self._config.get_mutable_obj(option)
if key in option_value and not replace: if key in option_value and not replace:
raise cmdexc.CommandError("{} already exists in {} - use " raise cmdutils.CommandError("{} already exists in {} - use "
"--replace to overwrite!" "--replace to overwrite!"
.format(key, option)) .format(key, option))
@ -302,7 +318,8 @@ class ConfigCommands:
@cmdutils.register(instance='config-commands') @cmdutils.register(instance='config-commands')
@cmdutils.argument('option', completion=configmodel.list_option) @cmdutils.argument('option', completion=configmodel.list_option)
def config_list_remove(self, option, value, temp=False): def config_list_remove(self, option: str, value: str,
temp: bool = False) -> None:
"""Remove a value from a list. """Remove a value from a list.
Args: Args:
@ -313,15 +330,15 @@ class ConfigCommands:
opt = self._config.get_opt(option) opt = self._config.get_opt(option)
valid_list_types = (configtypes.List, configtypes.ListOrValue) valid_list_types = (configtypes.List, configtypes.ListOrValue)
if not isinstance(opt.typ, valid_list_types): if not isinstance(opt.typ, valid_list_types):
raise cmdexc.CommandError(":config-list-remove can only be used " raise cmdutils.CommandError(":config-list-remove can only be used "
"for lists") "for lists")
with self._handle_config_error(): with self._handle_config_error():
option_value = self._config.get_mutable_obj(option) option_value = self._config.get_mutable_obj(option)
if value not in option_value: if value not in option_value:
raise cmdexc.CommandError("{} is not in {}!".format(value, raise cmdutils.CommandError("{} is not in {}!".format(
option)) value, option))
option_value.remove(value) option_value.remove(value)
@ -329,7 +346,8 @@ class ConfigCommands:
@cmdutils.register(instance='config-commands') @cmdutils.register(instance='config-commands')
@cmdutils.argument('option', completion=configmodel.dict_option) @cmdutils.argument('option', completion=configmodel.dict_option)
def config_dict_remove(self, option, key, temp=False): def config_dict_remove(self, option: str, key: str,
temp: bool = False) -> None:
"""Remove a key from a dict. """Remove a key from a dict.
Args: Args:
@ -339,22 +357,22 @@ class ConfigCommands:
""" """
opt = self._config.get_opt(option) opt = self._config.get_opt(option)
if not isinstance(opt.typ, configtypes.Dict): if not isinstance(opt.typ, configtypes.Dict):
raise cmdexc.CommandError(":config-dict-remove can only be used " raise cmdutils.CommandError(":config-dict-remove can only be used "
"for dicts") "for dicts")
with self._handle_config_error(): with self._handle_config_error():
option_value = self._config.get_mutable_obj(option) option_value = self._config.get_mutable_obj(option)
if key not in option_value: if key not in option_value:
raise cmdexc.CommandError("{} is not in {}!".format(key, raise cmdutils.CommandError("{} is not in {}!".format(
option)) key, option))
del option_value[key] del option_value[key]
self._config.update_mutables(save_yaml=not temp) self._config.update_mutables(save_yaml=not temp)
@cmdutils.register(instance='config-commands') @cmdutils.register(instance='config-commands')
def config_clear(self, save=False): def config_clear(self, save: bool = False) -> None:
"""Set all settings back to their default. """Set all settings back to their default.
Args: Args:
@ -364,7 +382,7 @@ class ConfigCommands:
self._config.clear(save_yaml=save) self._config.clear(save_yaml=save)
@cmdutils.register(instance='config-commands') @cmdutils.register(instance='config-commands')
def config_source(self, filename=None, clear=False): def config_source(self, filename: str = None, clear: bool = False) -> None:
"""Read a config.py file. """Read a config.py file.
Args: Args:
@ -383,19 +401,19 @@ class ConfigCommands:
try: try:
configfiles.read_config_py(filename) configfiles.read_config_py(filename)
except configexc.ConfigFileErrors as e: except configexc.ConfigFileErrors as e:
raise cmdexc.CommandError(e) raise cmdutils.CommandError(e)
@cmdutils.register(instance='config-commands') @cmdutils.register(instance='config-commands')
def config_edit(self, no_source=False): def config_edit(self, no_source: bool = False) -> None:
"""Open the config.py file in the editor. """Open the config.py file in the editor.
Args: Args:
no_source: Don't re-source the config file after editing. no_source: Don't re-source the config file after editing.
""" """
def on_file_updated(): def on_file_updated() -> None:
"""Source the new config when editing finished. """Source the new config when editing finished.
This can't use cmdexc.CommandError as it's run async. This can't use cmdutils.CommandError as it's run async.
""" """
try: try:
configfiles.read_config_py(filename) configfiles.read_config_py(filename)
@ -410,7 +428,8 @@ class ConfigCommands:
ed.edit_file(filename) ed.edit_file(filename)
@cmdutils.register(instance='config-commands') @cmdutils.register(instance='config-commands')
def config_write_py(self, filename=None, force=False, defaults=False): def config_write_py(self, filename: str = None,
force: bool = False, defaults: bool = False) -> None:
"""Write the current configuration to a config.py file. """Write the current configuration to a config.py file.
Args: Args:
@ -426,16 +445,16 @@ class ConfigCommands:
filename = os.path.expanduser(filename) filename = os.path.expanduser(filename)
if os.path.exists(filename) and not force: if os.path.exists(filename) and not force:
raise cmdexc.CommandError("{} already exists - use --force to " raise cmdutils.CommandError("{} already exists - use --force to "
"overwrite!".format(filename)) "overwrite!".format(filename))
options = [] # type: typing.List
if defaults: if defaults:
options = [(None, opt, opt.default) options = [(None, opt, opt.default)
for _name, opt in sorted(configdata.DATA.items())] for _name, opt in sorted(configdata.DATA.items())]
bindings = dict(configdata.DATA['bindings.default'].default) bindings = dict(configdata.DATA['bindings.default'].default)
commented = True commented = True
else: else:
options = []
for values in self._config: for values in self._config:
for scoped in values: for scoped in values:
options.append((scoped.pattern, values.opt, scoped.value)) options.append((scoped.pattern, values.opt, scoped.value))
@ -447,4 +466,4 @@ class ConfigCommands:
try: try:
writer.write(filename) writer.write(filename)
except OSError as e: except OSError as e:
raise cmdexc.CommandError(str(e)) raise cmdutils.CommandError(str(e))

View File

@ -24,14 +24,18 @@ Module attributes:
DATA: A dict of Option objects after init() has been called. DATA: A dict of Option objects after init() has been called.
""" """
import typing
from typing import Optional # pylint: disable=unused-import,useless-suppression
import functools import functools
import attr import attr
from qutebrowser.config import configtypes from qutebrowser.config import configtypes
from qutebrowser.utils import usertypes, qtutils, utils from qutebrowser.utils import usertypes, qtutils, utils
DATA = None DATA = typing.cast(typing.Mapping[str, 'Option'], None)
MIGRATIONS = None MIGRATIONS = typing.cast('Migrations', None)
_BackendDict = typing.Mapping[str, typing.Union[str, bool]]
@attr.s @attr.s
@ -42,15 +46,15 @@ class Option:
Note that this is just an option which exists, with no value associated. Note that this is just an option which exists, with no value associated.
""" """
name = attr.ib() name = attr.ib() # type: str
typ = attr.ib() typ = attr.ib() # type: configtypes.BaseType
default = attr.ib() default = attr.ib() # type: typing.Any
backends = attr.ib() backends = attr.ib() # type: typing.Iterable[usertypes.Backend]
raw_backends = attr.ib() raw_backends = attr.ib() # type: Optional[typing.Mapping[str, bool]]
description = attr.ib() description = attr.ib() # type: str
supports_pattern = attr.ib(default=False) supports_pattern = attr.ib(default=False) # type: bool
restart = attr.ib(default=False) restart = attr.ib(default=False) # type: bool
no_autoconfig = attr.ib(default=False) no_autoconfig = attr.ib(default=False) # type: bool
@attr.s @attr.s
@ -63,11 +67,13 @@ class Migrations:
deleted: A list of option names which have been removed. deleted: A list of option names which have been removed.
""" """
renamed = attr.ib(default=attr.Factory(dict)) renamed = attr.ib(
deleted = attr.ib(default=attr.Factory(list)) default=attr.Factory(dict)) # type: typing.Dict[str, str]
deleted = attr.ib(
default=attr.Factory(list)) # type: typing.List[str]
def _raise_invalid_node(name, what, node): def _raise_invalid_node(name: str, what: str, node: typing.Any) -> None:
"""Raise an exception for an invalid configdata YAML node. """Raise an exception for an invalid configdata YAML node.
Args: Args:
@ -79,13 +85,16 @@ def _raise_invalid_node(name, what, node):
name, what, node)) name, what, node))
def _parse_yaml_type(name, node): def _parse_yaml_type(
name: str,
node: typing.Union[str, typing.Mapping[str, typing.Any]],
) -> configtypes.BaseType:
if isinstance(node, str): if isinstance(node, str):
# e.g: # e.g:
# type: Bool # type: Bool
# -> create the type object without any arguments # -> create the type object without any arguments
type_name = node type_name = node
kwargs = {} kwargs = {} # type: typing.MutableMapping[str, typing.Any]
elif isinstance(node, dict): elif isinstance(node, dict):
# e.g: # e.g:
# type: # type:
@ -123,7 +132,10 @@ def _parse_yaml_type(name, node):
type_name, node, e)) type_name, node, e))
def _parse_yaml_backends_dict(name, node): def _parse_yaml_backends_dict(
name: str,
node: _BackendDict,
) -> typing.Sequence[usertypes.Backend]:
"""Parse a dict definition for backends. """Parse a dict definition for backends.
Example: Example:
@ -160,7 +172,10 @@ def _parse_yaml_backends_dict(name, node):
return backends return backends
def _parse_yaml_backends(name, node): def _parse_yaml_backends(
name: str,
node: typing.Union[None, str, _BackendDict],
) -> typing.Sequence[usertypes.Backend]:
"""Parse a backend node in the yaml. """Parse a backend node in the yaml.
It can have one of those four forms: It can have one of those four forms:
@ -187,7 +202,9 @@ def _parse_yaml_backends(name, node):
raise utils.Unreachable raise utils.Unreachable
def _read_yaml(yaml_data): def _read_yaml(
yaml_data: str,
) -> typing.Tuple[typing.Mapping[str, Option], Migrations]:
"""Read config data from a YAML file. """Read config data from a YAML file.
Args: Args:
@ -249,12 +266,12 @@ def _read_yaml(yaml_data):
@functools.lru_cache(maxsize=256) @functools.lru_cache(maxsize=256)
def is_valid_prefix(prefix): def is_valid_prefix(prefix: str) -> bool:
"""Check whether the given prefix is a valid prefix for some option.""" """Check whether the given prefix is a valid prefix for some option."""
return any(key.startswith(prefix + '.') for key in DATA) return any(key.startswith(prefix + '.') for key in DATA)
def init(): def init() -> None:
"""Initialize configdata from the YAML file.""" """Initialize configdata from the YAML file."""
global DATA, MIGRATIONS global DATA, MIGRATIONS
DATA, MIGRATIONS = _read_yaml(utils.read_file('config/configdata.yml')) DATA, MIGRATIONS = _read_yaml(utils.read_file('config/configdata.yml'))

View File

@ -39,12 +39,7 @@ ignore_case:
renamed: search.ignore_case renamed: search.ignore_case
search.ignore_case: search.ignore_case:
type: type: IgnoreCase
name: String
valid_values:
- always: Search case-insensitively.
- never: Search case-sensitively.
- smart: Search case-sensitively if there are capital characters.
default: smart default: smart
desc: When to find text on a page case-insensitively. desc: When to find text on a page case-insensitively.

View File

@ -19,6 +19,7 @@
"""Code to show a diff of the legacy config format.""" """Code to show a diff of the legacy config format."""
import typing # pylint: disable=unused-import,useless-suppression
import difflib import difflib
import os.path import os.path
@ -727,10 +728,10 @@ scroll right
""" """
def get_diff(): def get_diff() -> str:
"""Get a HTML diff for the old config files.""" """Get a HTML diff for the old config files."""
old_conf_lines = [] old_conf_lines = [] # type: typing.MutableSequence[str]
old_key_lines = [] old_key_lines = [] # type: typing.MutableSequence[str]
for filename, dest in [('qutebrowser.conf', old_conf_lines), for filename, dest in [('qutebrowser.conf', old_conf_lines),
('keys.conf', old_key_lines)]: ('keys.conf', old_key_lines)]:

View File

@ -19,23 +19,22 @@
"""Exceptions related to config parsing.""" """Exceptions related to config parsing."""
import typing
import attr import attr
from qutebrowser.utils import jinja from qutebrowser.utils import jinja, usertypes
class Error(Exception): class Error(Exception):
"""Base exception for config-related errors.""" """Base exception for config-related errors."""
pass
class NoAutoconfigError(Error): class NoAutoconfigError(Error):
"""Raised when this option can't be set in autoconfig.yml.""" """Raised when this option can't be set in autoconfig.yml."""
def __init__(self, name): def __init__(self, name: str) -> None:
super().__init__("The {} setting can only be set in config.py!" super().__init__("The {} setting can only be set in config.py!"
.format(name)) .format(name))
@ -44,7 +43,11 @@ class BackendError(Error):
"""Raised when this setting is unavailable with the current backend.""" """Raised when this setting is unavailable with the current backend."""
def __init__(self, name, backend, raw_backends): def __init__(
self, name: str,
backend: usertypes.Backend,
raw_backends: typing.Optional[typing.Mapping[str, bool]]
) -> None:
if raw_backends is None or not raw_backends[backend.name]: if raw_backends is None or not raw_backends[backend.name]:
msg = ("The {} setting is not available with the {} backend!" msg = ("The {} setting is not available with the {} backend!"
.format(name, backend.name)) .format(name, backend.name))
@ -59,7 +62,7 @@ class NoPatternError(Error):
"""Raised when the given setting does not support URL patterns.""" """Raised when the given setting does not support URL patterns."""
def __init__(self, name): def __init__(self, name: str) -> None:
super().__init__("The {} setting does not support URL patterns!" super().__init__("The {} setting does not support URL patterns!"
.format(name)) .format(name))
@ -73,7 +76,8 @@ class ValidationError(Error):
msg: Additional error message. msg: Additional error message.
""" """
def __init__(self, value, msg): def __init__(self, value: typing.Any,
msg: typing.Union[str, Exception]) -> None:
super().__init__("Invalid value '{}' - {}".format(value, msg)) super().__init__("Invalid value '{}' - {}".format(value, msg))
self.option = None self.option = None
@ -87,7 +91,9 @@ class NoOptionError(Error):
"""Raised when an option was not found.""" """Raised when an option was not found."""
def __init__(self, option, *, deleted=False, renamed=None): def __init__(self, option: str, *,
deleted: bool = False,
renamed: str = None) -> None:
if deleted: if deleted:
assert renamed is None assert renamed is None
suffix = ' (this option was removed from qutebrowser)' suffix = ' (this option was removed from qutebrowser)'
@ -111,18 +117,18 @@ class ConfigErrorDesc:
traceback: The formatted traceback of the exception. traceback: The formatted traceback of the exception.
""" """
text = attr.ib() text = attr.ib() # type: str
exception = attr.ib() exception = attr.ib() # type: typing.Union[str, Exception]
traceback = attr.ib(None) traceback = attr.ib(None) # type: str
def __str__(self): def __str__(self) -> str:
if self.traceback: if self.traceback:
return '{} - {}: {}'.format(self.text, return '{} - {}: {}'.format(self.text,
self.exception.__class__.__name__, self.exception.__class__.__name__,
self.exception) self.exception)
return '{}: {}'.format(self.text, self.exception) return '{}: {}'.format(self.text, self.exception)
def with_text(self, text): def with_text(self, text: str) -> 'ConfigErrorDesc':
"""Get a new ConfigErrorDesc with the given text appended.""" """Get a new ConfigErrorDesc with the given text appended."""
return self.__class__(text='{} ({})'.format(self.text, text), return self.__class__(text='{} ({})'.format(self.text, text),
exception=self.exception, exception=self.exception,
@ -133,13 +139,15 @@ class ConfigFileErrors(Error):
"""Raised when multiple errors occurred inside the config.""" """Raised when multiple errors occurred inside the config."""
def __init__(self, basename, errors): def __init__(self,
basename: str,
errors: typing.Sequence[ConfigErrorDesc]) -> None:
super().__init__("Errors occurred while reading {}:\n{}".format( super().__init__("Errors occurred while reading {}:\n{}".format(
basename, '\n'.join(' {}'.format(e) for e in errors))) basename, '\n'.join(' {}'.format(e) for e in errors)))
self.basename = basename self.basename = basename
self.errors = errors self.errors = errors
def to_html(self): def to_html(self) -> str:
"""Get the error texts as a HTML snippet.""" """Get the error texts as a HTML snippet."""
template = jinja.environment.from_string(""" template = jinja.environment.from_string("""
Errors occurred while reading {{ basename }}: Errors occurred while reading {{ basename }}:

View File

@ -27,6 +27,7 @@ import textwrap
import traceback import traceback
import configparser import configparser
import contextlib import contextlib
import typing
import yaml import yaml
from PyQt5.QtCore import pyqtSignal, QObject, QSettings from PyQt5.QtCore import pyqtSignal, QObject, QSettings
@ -36,16 +37,21 @@ from qutebrowser.config import configexc, config, configdata, configutils
from qutebrowser.keyinput import keyutils from qutebrowser.keyinput import keyutils
from qutebrowser.utils import standarddir, utils, qtutils, log, urlmatch from qutebrowser.utils import standarddir, utils, qtutils, log, urlmatch
MYPY = False
if MYPY:
# pylint: disable=unused-import, useless-suppression
from qutebrowser.misc import savemanager
# The StateConfig instance # The StateConfig instance
state = None state = typing.cast('StateConfig', None)
class StateConfig(configparser.ConfigParser): class StateConfig(configparser.ConfigParser):
"""The "state" file saving various application state.""" """The "state" file saving various application state."""
def __init__(self): def __init__(self) -> None:
super().__init__() super().__init__()
self._filename = os.path.join(standarddir.data(), 'state') self._filename = os.path.join(standarddir.data(), 'state')
self.read(self._filename, encoding='utf-8') self.read(self._filename, encoding='utf-8')
@ -59,7 +65,8 @@ class StateConfig(configparser.ConfigParser):
for key in deleted_keys: for key in deleted_keys:
self['general'].pop(key, None) self['general'].pop(key, None)
def init_save_manager(self, save_manager): def init_save_manager(self,
save_manager: 'savemanager.SaveManager') -> None:
"""Make sure the config gets saved properly. """Make sure the config gets saved properly.
We do this outside of __init__ because the config gets created before We do this outside of __init__ because the config gets created before
@ -67,7 +74,7 @@ class StateConfig(configparser.ConfigParser):
""" """
save_manager.add_saveable('state-config', self._save) save_manager.add_saveable('state-config', self._save)
def _save(self): def _save(self) -> None:
"""Save the state file to the configured location.""" """Save the state file to the configured location."""
with open(self._filename, 'w', encoding='utf-8') as f: with open(self._filename, 'w', encoding='utf-8') as f:
self.write(f) self.write(f)
@ -84,17 +91,20 @@ class YamlConfig(QObject):
VERSION = 2 VERSION = 2
changed = pyqtSignal() changed = pyqtSignal()
def __init__(self, parent=None): _SettingsType = typing.Dict[str, typing.Dict[str, typing.Any]]
def __init__(self, parent: QObject = None) -> None:
super().__init__(parent) super().__init__(parent)
self._filename = os.path.join(standarddir.config(auto=True), self._filename = os.path.join(standarddir.config(auto=True),
'autoconfig.yml') 'autoconfig.yml')
self._dirty = None self._dirty = False
self._values = {} self._values = {} # type: typing.Dict[str, configutils.Values]
for name, opt in configdata.DATA.items(): for name, opt in configdata.DATA.items():
self._values[name] = configutils.Values(opt) self._values[name] = configutils.Values(opt)
def init_save_manager(self, save_manager): def init_save_manager(self,
save_manager: 'savemanager.SaveManager') -> None:
"""Make sure the config gets saved properly. """Make sure the config gets saved properly.
We do this outside of __init__ because the config gets created before We do this outside of __init__ because the config gets created before
@ -102,21 +112,21 @@ class YamlConfig(QObject):
""" """
save_manager.add_saveable('yaml-config', self._save, self.changed) save_manager.add_saveable('yaml-config', self._save, self.changed)
def __iter__(self): def __iter__(self) -> typing.Iterator[configutils.Values]:
"""Iterate over configutils.Values items.""" """Iterate over configutils.Values items."""
yield from self._values.values() yield from self._values.values()
def _mark_changed(self): def _mark_changed(self) -> None:
"""Mark the YAML config as changed.""" """Mark the YAML config as changed."""
self._dirty = True self._dirty = True
self.changed.emit() self.changed.emit()
def _save(self): def _save(self) -> None:
"""Save the settings to the YAML file if they've changed.""" """Save the settings to the YAML file if they've changed."""
if not self._dirty: if not self._dirty:
return return
settings = {} settings = {} # type: YamlConfig._SettingsType
for name, values in sorted(self._values.items()): for name, values in sorted(self._values.items()):
if not values: if not values:
continue continue
@ -135,7 +145,10 @@ class YamlConfig(QObject):
""".lstrip('\n'))) """.lstrip('\n')))
utils.yaml_dump(data, f) utils.yaml_dump(data, f)
def _pop_object(self, yaml_data, key, typ): def _pop_object(self,
yaml_data: typing.Any,
key: str,
typ: type) -> typing.Any:
"""Get a global object from the given data.""" """Get a global object from the given data."""
if not isinstance(yaml_data, dict): if not isinstance(yaml_data, dict):
desc = configexc.ConfigErrorDesc("While loading data", desc = configexc.ConfigErrorDesc("While loading data",
@ -158,7 +171,7 @@ class YamlConfig(QObject):
return data return data
def load(self): def load(self) -> None:
"""Load configuration from the configured YAML file.""" """Load configuration from the configured YAML file."""
try: try:
with open(self._filename, 'r', encoding='utf-8') as f: with open(self._filename, 'r', encoding='utf-8') as f:
@ -189,18 +202,19 @@ class YamlConfig(QObject):
self._validate(settings) self._validate(settings)
self._build_values(settings) self._build_values(settings)
def _load_settings_object(self, yaml_data): def _load_settings_object(self, yaml_data: typing.Any) -> '_SettingsType':
"""Load the settings from the settings: key.""" """Load the settings from the settings: key."""
return self._pop_object(yaml_data, 'settings', dict) return self._pop_object(yaml_data, 'settings', dict)
def _load_legacy_settings_object(self, yaml_data): def _load_legacy_settings_object(self,
yaml_data: typing.Any) -> '_SettingsType':
data = self._pop_object(yaml_data, 'global', dict) data = self._pop_object(yaml_data, 'global', dict)
settings = {} settings = {}
for name, value in data.items(): for name, value in data.items():
settings[name] = {'global': value} settings[name] = {'global': value}
return settings return settings
def _build_values(self, settings): def _build_values(self, settings: typing.Mapping) -> None:
"""Build up self._values from the values in the given dict.""" """Build up self._values from the values in the given dict."""
errors = [] errors = []
for name, yaml_values in settings.items(): for name, yaml_values in settings.items():
@ -233,7 +247,8 @@ class YamlConfig(QObject):
if errors: if errors:
raise configexc.ConfigFileErrors('autoconfig.yml', errors) raise configexc.ConfigFileErrors('autoconfig.yml', errors)
def _migrate_bool(self, settings, name, true_value, false_value): def _migrate_bool(self, settings: _SettingsType, name: str,
true_value: str, false_value: str) -> None:
"""Migrate a boolean in the settings.""" """Migrate a boolean in the settings."""
if name in settings: if name in settings:
for scope, val in settings[name].items(): for scope, val in settings[name].items():
@ -241,7 +256,7 @@ class YamlConfig(QObject):
settings[name][scope] = true_value if val else false_value settings[name][scope] = true_value if val else false_value
self._mark_changed() self._mark_changed()
def _handle_migrations(self, settings): def _handle_migrations(self, settings: _SettingsType) -> '_SettingsType':
"""Migrate older configs to the newest format.""" """Migrate older configs to the newest format."""
# Simple renamed/deleted options # Simple renamed/deleted options
for name in list(settings): for name in list(settings):
@ -299,7 +314,7 @@ class YamlConfig(QObject):
return settings return settings
def _validate(self, settings): def _validate(self, settings: _SettingsType) -> None:
"""Make sure all settings exist.""" """Make sure all settings exist."""
unknown = [] unknown = []
for name in settings: for name in settings:
@ -312,18 +327,19 @@ class YamlConfig(QObject):
for e in sorted(unknown)] for e in sorted(unknown)]
raise configexc.ConfigFileErrors('autoconfig.yml', errors) raise configexc.ConfigFileErrors('autoconfig.yml', errors)
def set_obj(self, name, value, *, pattern=None): def set_obj(self, name: str, value: typing.Any, *,
pattern: urlmatch.UrlPattern = None) -> None:
"""Set the given setting to the given value.""" """Set the given setting to the given value."""
self._values[name].add(value, pattern) self._values[name].add(value, pattern)
self._mark_changed() self._mark_changed()
def unset(self, name, *, pattern=None): def unset(self, name: str, *, pattern: urlmatch.UrlPattern = None) -> None:
"""Remove the given option name if it's configured.""" """Remove the given option name if it's configured."""
changed = self._values[name].remove(pattern) changed = self._values[name].remove(pattern)
if changed: if changed:
self._mark_changed() self._mark_changed()
def clear(self): def clear(self) -> None:
"""Clear all values from the YAML file.""" """Clear all values from the YAML file."""
for values in self._values.values(): for values in self._values.values():
values.clear() values.clear()
@ -346,15 +362,15 @@ class ConfigAPI:
datadir: The qutebrowser data directory, as pathlib.Path. datadir: The qutebrowser data directory, as pathlib.Path.
""" """
def __init__(self, conf, keyconfig): def __init__(self, conf: config.Config, keyconfig: config.KeyConfig):
self._config = conf self._config = conf
self._keyconfig = keyconfig self._keyconfig = keyconfig
self.errors = [] self.errors = [] # type: typing.List[configexc.ConfigErrorDesc]
self.configdir = pathlib.Path(standarddir.config()) self.configdir = pathlib.Path(standarddir.config())
self.datadir = pathlib.Path(standarddir.data()) self.datadir = pathlib.Path(standarddir.data())
@contextlib.contextmanager @contextlib.contextmanager
def _handle_error(self, action, name): def _handle_error(self, action: str, name: str) -> typing.Iterator[None]:
"""Catch config-related exceptions and save them in self.errors.""" """Catch config-related exceptions and save them in self.errors."""
try: try:
yield yield
@ -372,40 +388,40 @@ class ConfigAPI:
text = "While {} '{}' and parsing key".format(action, name) text = "While {} '{}' and parsing key".format(action, name)
self.errors.append(configexc.ConfigErrorDesc(text, e)) self.errors.append(configexc.ConfigErrorDesc(text, e))
def finalize(self): def finalize(self) -> None:
"""Do work which needs to be done after reading config.py.""" """Do work which needs to be done after reading config.py."""
self._config.update_mutables() self._config.update_mutables()
def load_autoconfig(self): def load_autoconfig(self) -> None:
"""Load the autoconfig.yml file which is used for :set/:bind/etc.""" """Load the autoconfig.yml file which is used for :set/:bind/etc."""
with self._handle_error('reading', 'autoconfig.yml'): with self._handle_error('reading', 'autoconfig.yml'):
read_autoconfig() read_autoconfig()
def get(self, name, pattern=None): def get(self, name: str, pattern: str = None) -> typing.Any:
"""Get a setting value from the config, optionally with a pattern.""" """Get a setting value from the config, optionally with a pattern."""
with self._handle_error('getting', name): with self._handle_error('getting', name):
urlpattern = urlmatch.UrlPattern(pattern) if pattern else None urlpattern = urlmatch.UrlPattern(pattern) if pattern else None
return self._config.get_mutable_obj(name, pattern=urlpattern) return self._config.get_mutable_obj(name, pattern=urlpattern)
def set(self, name, value, pattern=None): def set(self, name: str, value: typing.Any, pattern: str = None) -> None:
"""Set a setting value in the config, optionally with a pattern.""" """Set a setting value in the config, optionally with a pattern."""
with self._handle_error('setting', name): with self._handle_error('setting', name):
urlpattern = urlmatch.UrlPattern(pattern) if pattern else None urlpattern = urlmatch.UrlPattern(pattern) if pattern else None
self._config.set_obj(name, value, pattern=urlpattern) self._config.set_obj(name, value, pattern=urlpattern)
def bind(self, key, command, mode='normal'): def bind(self, key: str, command: str, mode: str = 'normal') -> None:
"""Bind a key to a command, with an optional key mode.""" """Bind a key to a command, with an optional key mode."""
with self._handle_error('binding', key): with self._handle_error('binding', key):
seq = keyutils.KeySequence.parse(key) seq = keyutils.KeySequence.parse(key)
self._keyconfig.bind(seq, command, mode=mode) self._keyconfig.bind(seq, command, mode=mode)
def unbind(self, key, mode='normal'): def unbind(self, key: str, mode: str = 'normal') -> None:
"""Unbind a key from a command, with an optional key mode.""" """Unbind a key from a command, with an optional key mode."""
with self._handle_error('unbinding', key): with self._handle_error('unbinding', key):
seq = keyutils.KeySequence.parse(key) seq = keyutils.KeySequence.parse(key)
self._keyconfig.unbind(seq, mode=mode) self._keyconfig.unbind(seq, mode=mode)
def source(self, filename): def source(self, filename: str) -> None:
"""Read the given config file from disk.""" """Read the given config file from disk."""
if not os.path.isabs(filename): if not os.path.isabs(filename):
filename = str(self.configdir / filename) filename = str(self.configdir / filename)
@ -416,7 +432,7 @@ class ConfigAPI:
self.errors += e.errors self.errors += e.errors
@contextlib.contextmanager @contextlib.contextmanager
def pattern(self, pattern): def pattern(self, pattern: str) -> typing.Iterator[config.ConfigContainer]:
"""Get a ConfigContainer for the given pattern.""" """Get a ConfigContainer for the given pattern."""
# We need to propagate the exception so we don't need to return # We need to propagate the exception so we don't need to return
# something. # something.
@ -430,17 +446,21 @@ class ConfigPyWriter:
"""Writer for config.py files from given settings.""" """Writer for config.py files from given settings."""
def __init__(self, options, bindings, *, commented): def __init__(
self,
options: typing.List,
bindings: typing.MutableMapping[str, typing.Mapping[str, str]], *,
commented: bool) -> None:
self._options = options self._options = options
self._bindings = bindings self._bindings = bindings
self._commented = commented self._commented = commented
def write(self, filename): def write(self, filename: str) -> None:
"""Write the config to the given file.""" """Write the config to the given file."""
with open(filename, 'w', encoding='utf-8') as f: with open(filename, 'w', encoding='utf-8') as f:
f.write('\n'.join(self._gen_lines())) f.write('\n'.join(self._gen_lines()))
def _line(self, line): def _line(self, line: str) -> str:
"""Get an (optionally commented) line.""" """Get an (optionally commented) line."""
if self._commented: if self._commented:
if line.startswith('#'): if line.startswith('#'):
@ -450,7 +470,7 @@ class ConfigPyWriter:
else: else:
return line return line
def _gen_lines(self): def _gen_lines(self) -> typing.Iterator[str]:
"""Generate a config.py with the given settings/bindings. """Generate a config.py with the given settings/bindings.
Yields individual lines. Yields individual lines.
@ -459,7 +479,7 @@ class ConfigPyWriter:
yield from self._gen_options() yield from self._gen_options()
yield from self._gen_bindings() yield from self._gen_bindings()
def _gen_header(self): def _gen_header(self) -> typing.Iterator[str]:
"""Generate the initial header of the config.""" """Generate the initial header of the config."""
yield self._line("# Autogenerated config.py") yield self._line("# Autogenerated config.py")
yield self._line("# Documentation:") yield self._line("# Documentation:")
@ -481,7 +501,7 @@ class ConfigPyWriter:
yield self._line("# config.load_autoconfig()") yield self._line("# config.load_autoconfig()")
yield '' yield ''
def _gen_options(self): def _gen_options(self) -> typing.Iterator[str]:
"""Generate the options part of the config.""" """Generate the options part of the config."""
for pattern, opt, value in self._options: for pattern, opt, value in self._options:
if opt.name in ['bindings.commands', 'bindings.default']: if opt.name in ['bindings.commands', 'bindings.default']:
@ -509,7 +529,7 @@ class ConfigPyWriter:
opt.name, value, str(pattern))) opt.name, value, str(pattern)))
yield '' yield ''
def _gen_bindings(self): def _gen_bindings(self) -> typing.Iterator[str]:
"""Generate the bindings part of the config.""" """Generate the bindings part of the config."""
normal_bindings = self._bindings.pop('normal', {}) normal_bindings = self._bindings.pop('normal', {})
if normal_bindings: if normal_bindings:
@ -527,7 +547,7 @@ class ConfigPyWriter:
yield '' yield ''
def read_config_py(filename, raising=False): def read_config_py(filename: str, raising: bool = False) -> None:
"""Read a config.py file. """Read a config.py file.
Arguments; Arguments;
@ -543,8 +563,8 @@ def read_config_py(filename, raising=False):
basename = os.path.basename(filename) basename = os.path.basename(filename)
module = types.ModuleType('config') module = types.ModuleType('config')
module.config = api module.config = api # type: ignore
module.c = container module.c = container # type: ignore
module.__file__ = filename module.__file__ = filename
try: try:
@ -589,7 +609,7 @@ def read_config_py(filename, raising=False):
raise configexc.ConfigFileErrors('config.py', api.errors) raise configexc.ConfigFileErrors('config.py', api.errors)
def read_autoconfig(): def read_autoconfig() -> None:
"""Read the autoconfig.yml file.""" """Read the autoconfig.yml file."""
try: try:
config.instance.read_yaml() config.instance.read_yaml()
@ -601,7 +621,7 @@ def read_autoconfig():
@contextlib.contextmanager @contextlib.contextmanager
def saved_sys_properties(): def saved_sys_properties() -> typing.Iterator[None]:
"""Save various sys properties such as sys.path and sys.modules.""" """Save various sys properties such as sys.path and sys.modules."""
old_path = sys.path.copy() old_path = sys.path.copy()
old_modules = sys.modules.copy() old_modules = sys.modules.copy()
@ -614,7 +634,7 @@ def saved_sys_properties():
del sys.modules[module] del sys.modules[module]
def init(): def init() -> None:
"""Initialize config storage not related to the main config.""" """Initialize config storage not related to the main config."""
global state global state
state = StateConfig() state = StateConfig()

View File

@ -19,24 +19,27 @@
"""Initialization of the configuration.""" """Initialization of the configuration."""
import argparse
import os.path import os.path
import sys import sys
import typing
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
from qutebrowser.api import config as configapi
from qutebrowser.config import (config, configdata, configfiles, configtypes, from qutebrowser.config import (config, configdata, configfiles, configtypes,
configexc, configcommands) configexc, configcommands)
from qutebrowser.utils import (objreg, usertypes, log, standarddir, message, from qutebrowser.utils import (objreg, usertypes, log, standarddir, message,
qtutils) qtutils)
from qutebrowser.config import configcache from qutebrowser.config import configcache
from qutebrowser.misc import msgbox, objects from qutebrowser.misc import msgbox, objects, savemanager
# Error which happened during init, so we can show a message box. # Error which happened during init, so we can show a message box.
_init_errors = None _init_errors = None
def early_init(args): def early_init(args: argparse.Namespace) -> None:
"""Initialize the part of the config which works without a QApplication.""" """Initialize the part of the config which works without a QApplication."""
configdata.init() configdata.init()
@ -44,6 +47,7 @@ def early_init(args):
config.instance = config.Config(yaml_config=yaml_config) config.instance = config.Config(yaml_config=yaml_config)
config.val = config.ConfigContainer(config.instance) config.val = config.ConfigContainer(config.instance)
configapi.val = config.ConfigContainer(config.instance)
config.key_instance = config.KeyConfig(config.instance) config.key_instance = config.KeyConfig(config.instance)
config.cache = configcache.ConfigCache() config.cache = configcache.ConfigCache()
yaml_config.setParent(config.instance) yaml_config.setParent(config.instance)
@ -83,7 +87,7 @@ def early_init(args):
_init_envvars() _init_envvars()
def _init_envvars(): def _init_envvars() -> None:
"""Initialize environment variables which need to be set early.""" """Initialize environment variables which need to be set early."""
if objects.backend == usertypes.Backend.QtWebEngine: if objects.backend == usertypes.Backend.QtWebEngine:
software_rendering = config.val.qt.force_software_rendering software_rendering = config.val.qt.force_software_rendering
@ -105,7 +109,7 @@ def _init_envvars():
@config.change_filter('fonts.monospace', function=True) @config.change_filter('fonts.monospace', function=True)
def _update_monospace_fonts(): def _update_monospace_fonts() -> None:
"""Update all fonts if fonts.monospace was set.""" """Update all fonts if fonts.monospace was set."""
configtypes.Font.monospace_fonts = config.val.fonts.monospace configtypes.Font.monospace_fonts = config.val.fonts.monospace
for name, opt in configdata.DATA.items(): for name, opt in configdata.DATA.items():
@ -121,7 +125,7 @@ def _update_monospace_fonts():
config.instance.changed.emit(name) config.instance.changed.emit(name)
def get_backend(args): def get_backend(args: argparse.Namespace) -> usertypes.Backend:
"""Find out what backend to use based on available libraries.""" """Find out what backend to use based on available libraries."""
str_to_backend = { str_to_backend = {
'webkit': usertypes.Backend.QtWebKit, 'webkit': usertypes.Backend.QtWebKit,
@ -134,7 +138,7 @@ def get_backend(args):
return str_to_backend[config.val.backend] return str_to_backend[config.val.backend]
def late_init(save_manager): def late_init(save_manager: savemanager.SaveManager) -> None:
"""Initialize the rest of the config after the QApplication is created.""" """Initialize the rest of the config after the QApplication is created."""
global _init_errors global _init_errors
if _init_errors is not None: if _init_errors is not None:
@ -150,7 +154,7 @@ def late_init(save_manager):
configfiles.state.init_save_manager(save_manager) configfiles.state.init_save_manager(save_manager)
def qt_args(namespace): def qt_args(namespace: argparse.Namespace) -> typing.List[str]:
"""Get the Qt QApplication arguments based on an argparse namespace. """Get the Qt QApplication arguments based on an argparse namespace.
Args: Args:
@ -176,7 +180,7 @@ def qt_args(namespace):
return argv return argv
def _qtwebengine_args(): def _qtwebengine_args() -> typing.Iterator[str]:
"""Get the QtWebEngine arguments to use based on the config.""" """Get the QtWebEngine arguments to use based on the config."""
if not qtutils.version_check('5.11', compiled=False): if not qtutils.version_check('5.11', compiled=False):
# WORKAROUND equivalent to # WORKAROUND equivalent to
@ -222,7 +226,7 @@ def _qtwebengine_args():
'never': '--no-referrers', 'never': '--no-referrers',
'same-domain': '--reduced-referrer-granularity', 'same-domain': '--reduced-referrer-granularity',
} }
} } # type: typing.Dict[str, typing.Dict[typing.Any, typing.Optional[str]]]
if not qtutils.version_check('5.11'): if not qtutils.version_check('5.11'):
# On Qt 5.11, we can control this via QWebEngineSettings # On Qt 5.11, we can control this via QWebEngineSettings

File diff suppressed because it is too large Load Diff

View File

@ -21,23 +21,31 @@
"""Utilities and data structures used by various config code.""" """Utilities and data structures used by various config code."""
import attr import typing
from qutebrowser.utils import utils import attr
from PyQt5.QtCore import QUrl
from qutebrowser.utils import utils, urlmatch
from qutebrowser.config import configexc from qutebrowser.config import configexc
MYPY = False
if MYPY:
# pylint: disable=unused-import,useless-suppression
from qutebrowser.config import configdata
class _UnsetObject:
class Unset:
"""Sentinel object.""" """Sentinel object."""
__slots__ = () __slots__ = ()
def __repr__(self): def __repr__(self) -> str:
return '<UNSET>' return '<UNSET>'
UNSET = _UnsetObject() UNSET = Unset()
@attr.s @attr.s
@ -50,8 +58,8 @@ class ScopedValue:
pattern: The UrlPattern for the value, or None for global values. pattern: The UrlPattern for the value, or None for global values.
""" """
value = attr.ib() value = attr.ib() # type: typing.Any
pattern = attr.ib() pattern = attr.ib() # type: typing.Optional[urlmatch.UrlPattern]
class Values: class Values:
@ -73,15 +81,17 @@ class Values:
opt: The Option being customized. opt: The Option being customized.
""" """
def __init__(self, opt, values=None): def __init__(self,
opt: 'configdata.Option',
values: typing.MutableSequence = None) -> None:
self.opt = opt self.opt = opt
self._values = values or [] self._values = values or []
def __repr__(self): def __repr__(self) -> str:
return utils.get_repr(self, opt=self.opt, values=self._values, return utils.get_repr(self, opt=self.opt, values=self._values,
constructor=True) constructor=True)
def __str__(self): def __str__(self) -> str:
"""Get the values as human-readable string.""" """Get the values as human-readable string."""
if not self: if not self:
return '{}: <unchanged>'.format(self.opt.name) return '{}: <unchanged>'.format(self.opt.name)
@ -96,7 +106,7 @@ class Values:
scoped.pattern, self.opt.name, str_value)) scoped.pattern, self.opt.name, str_value))
return '\n'.join(lines) return '\n'.join(lines)
def __iter__(self): def __iter__(self) -> typing.Iterator['ScopedValue']:
"""Yield ScopedValue elements. """Yield ScopedValue elements.
This yields in "normal" order, i.e. global and then first-set settings This yields in "normal" order, i.e. global and then first-set settings
@ -104,23 +114,25 @@ class Values:
""" """
yield from self._values yield from self._values
def __bool__(self): def __bool__(self) -> bool:
"""Check whether this value is customized.""" """Check whether this value is customized."""
return bool(self._values) return bool(self._values)
def _check_pattern_support(self, arg): def _check_pattern_support(
self, arg: typing.Optional[urlmatch.UrlPattern]) -> None:
"""Make sure patterns are supported if one was given.""" """Make sure patterns are supported if one was given."""
if arg is not None and not self.opt.supports_pattern: if arg is not None and not self.opt.supports_pattern:
raise configexc.NoPatternError(self.opt.name) raise configexc.NoPatternError(self.opt.name)
def add(self, value, pattern=None): def add(self, value: typing.Any,
pattern: urlmatch.UrlPattern = None) -> None:
"""Add a value with the given pattern to the list of values.""" """Add a value with the given pattern to the list of values."""
self._check_pattern_support(pattern) self._check_pattern_support(pattern)
self.remove(pattern) self.remove(pattern)
scoped = ScopedValue(value, pattern) scoped = ScopedValue(value, pattern)
self._values.append(scoped) self._values.append(scoped)
def remove(self, pattern=None): def remove(self, pattern: urlmatch.UrlPattern = None) -> bool:
"""Remove the value with the given pattern. """Remove the value with the given pattern.
If a matching pattern was removed, True is returned. If a matching pattern was removed, True is returned.
@ -131,11 +143,11 @@ class Values:
self._values = [v for v in self._values if v.pattern != pattern] self._values = [v for v in self._values if v.pattern != pattern]
return old_len != len(self._values) return old_len != len(self._values)
def clear(self): def clear(self) -> None:
"""Clear all customization for this value.""" """Clear all customization for this value."""
self._values = [] self._values = []
def _get_fallback(self, fallback): def _get_fallback(self, fallback: typing.Any) -> typing.Any:
"""Get the fallback global/default value.""" """Get the fallback global/default value."""
for scoped in self._values: for scoped in self._values:
if scoped.pattern is None: if scoped.pattern is None:
@ -146,7 +158,8 @@ class Values:
else: else:
return UNSET return UNSET
def get_for_url(self, url=None, *, fallback=True): def get_for_url(self, url: QUrl = None, *,
fallback: bool = True) -> typing.Any:
"""Get a config value, falling back when needed. """Get a config value, falling back when needed.
This first tries to find a value matching the URL (if given). This first tries to find a value matching the URL (if given).
@ -165,7 +178,9 @@ class Values:
return self._get_fallback(fallback) return self._get_fallback(fallback)
def get_for_pattern(self, pattern, *, fallback=True): def get_for_pattern(self,
pattern: typing.Optional[urlmatch.UrlPattern], *,
fallback: bool = True) -> typing.Any:
"""Get a value only if it's been overridden for the given pattern. """Get a value only if it's been overridden for the given pattern.
This is useful when showing values to the user. This is useful when showing values to the user.

View File

@ -19,6 +19,10 @@
"""Bridge from QWeb(Engine)Settings to our own settings.""" """Bridge from QWeb(Engine)Settings to our own settings."""
import typing
import argparse
from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QFont from PyQt5.QtGui import QFont
from qutebrowser.config import config, configutils from qutebrowser.config import config, configutils
@ -32,7 +36,8 @@ class AttributeInfo:
"""Info about a settings attribute.""" """Info about a settings attribute."""
def __init__(self, *attributes, converter=None): def __init__(self, *attributes: typing.Any,
converter: typing.Callable = None) -> None:
self.attributes = attributes self.attributes = attributes
if converter is None: if converter is None:
self.converter = lambda val: val self.converter = lambda val: val
@ -44,15 +49,15 @@ class AbstractSettings:
"""Abstract base class for settings set via QWeb(Engine)Settings.""" """Abstract base class for settings set via QWeb(Engine)Settings."""
_ATTRIBUTES = None _ATTRIBUTES = {} # type: typing.Dict[str, AttributeInfo]
_FONT_SIZES = None _FONT_SIZES = {} # type: typing.Dict[str, typing.Any]
_FONT_FAMILIES = None _FONT_FAMILIES = {} # type: typing.Dict[str, typing.Any]
_FONT_TO_QFONT = None _FONT_TO_QFONT = {} # type: typing.Dict[typing.Any, QFont.StyleHint]
def __init__(self, settings): def __init__(self, settings: typing.Any) -> None:
self._settings = settings self._settings = settings
def set_attribute(self, name, value): def set_attribute(self, name: str, value: typing.Any) -> bool:
"""Set the given QWebSettings/QWebEngineSettings attribute. """Set the given QWebSettings/QWebEngineSettings attribute.
If the value is configutils.UNSET, the value is reset instead. If the value is configutils.UNSET, the value is reset instead.
@ -73,7 +78,7 @@ class AbstractSettings:
return old_value != new_value return old_value != new_value
def test_attribute(self, name): def test_attribute(self, name: str) -> bool:
"""Get the value for the given attribute. """Get the value for the given attribute.
If the setting resolves to a list of attributes, only the first If the setting resolves to a list of attributes, only the first
@ -82,7 +87,7 @@ class AbstractSettings:
info = self._ATTRIBUTES[name] info = self._ATTRIBUTES[name]
return self._settings.testAttribute(info.attributes[0]) return self._settings.testAttribute(info.attributes[0])
def set_font_size(self, name, value): def set_font_size(self, name: str, value: int) -> bool:
"""Set the given QWebSettings/QWebEngineSettings font size. """Set the given QWebSettings/QWebEngineSettings font size.
Return: Return:
@ -94,7 +99,7 @@ class AbstractSettings:
self._settings.setFontSize(family, value) self._settings.setFontSize(family, value)
return old_value != value return old_value != value
def set_font_family(self, name, value): def set_font_family(self, name: str, value: typing.Optional[str]) -> bool:
"""Set the given QWebSettings/QWebEngineSettings font family. """Set the given QWebSettings/QWebEngineSettings font family.
With None (the default), QFont is used to get the default font for the With None (the default), QFont is used to get the default font for the
@ -115,7 +120,7 @@ class AbstractSettings:
return value != old_value return value != old_value
def set_default_text_encoding(self, encoding): def set_default_text_encoding(self, encoding: str) -> bool:
"""Set the default text encoding to use. """Set the default text encoding to use.
Return: Return:
@ -126,7 +131,7 @@ class AbstractSettings:
self._settings.setDefaultTextEncoding(encoding) self._settings.setDefaultTextEncoding(encoding)
return old_value != encoding return old_value != encoding
def _update_setting(self, setting, value): def _update_setting(self, setting: str, value: typing.Any) -> bool:
"""Update the given setting/value. """Update the given setting/value.
Unknown settings are ignored. Unknown settings are ignored.
@ -144,12 +149,12 @@ class AbstractSettings:
return self.set_default_text_encoding(value) return self.set_default_text_encoding(value)
return False return False
def update_setting(self, setting): def update_setting(self, setting: str) -> None:
"""Update the given setting.""" """Update the given setting."""
value = config.instance.get(setting) value = config.instance.get(setting)
self._update_setting(setting, value) self._update_setting(setting, value)
def update_for_url(self, url): def update_for_url(self, url: QUrl) -> typing.Set[str]:
"""Update settings customized for the given tab. """Update settings customized for the given tab.
Return: Return:
@ -171,14 +176,14 @@ class AbstractSettings:
return changed_settings return changed_settings
def init_settings(self): def init_settings(self) -> None:
"""Set all supported settings correctly.""" """Set all supported settings correctly."""
for setting in (list(self._ATTRIBUTES) + list(self._FONT_SIZES) + for setting in (list(self._ATTRIBUTES) + list(self._FONT_SIZES) +
list(self._FONT_FAMILIES)): list(self._FONT_FAMILIES)):
self.update_setting(setting) self.update_setting(setting)
def init(args): def init(args: argparse.Namespace) -> None:
"""Initialize all QWeb(Engine)Settings.""" """Initialize all QWeb(Engine)Settings."""
if objects.backend == usertypes.Backend.QtWebEngine: if objects.backend == usertypes.Backend.QtWebEngine:
from qutebrowser.browser.webengine import webenginesettings from qutebrowser.browser.webengine import webenginesettings
@ -193,7 +198,7 @@ def init(args):
pattern=urlmatch.UrlPattern(pattern)) pattern=urlmatch.UrlPattern(pattern))
def shutdown(): def shutdown() -> None:
"""Shut down QWeb(Engine)Settings.""" """Shut down QWeb(Engine)Settings."""
if objects.backend == usertypes.Backend.QtWebEngine: if objects.backend == usertypes.Backend.QtWebEngine:
from qutebrowser.browser.webengine import webenginesettings from qutebrowser.browser.webengine import webenginesettings

View File

Some files were not shown because too many files have changed in this diff Show More