Merge branch 'master' into mutable-dict

This commit is contained in:
Ryan Farley 2017-09-19 14:07:46 -05:00
commit cc540bb166
124 changed files with 1207 additions and 1838 deletions

9
.github/CONTRIBUTING.asciidoc vendored Normal file
View File

@ -0,0 +1,9 @@
- Before you start to work on something, please leave a comment on the relevant
issue (or open one). This makes sure there is no duplicate work done.
- Either run the testsuite locally, or keep an eye on Travis CI / AppVeyor
after pushing changes.
See the full contribution docs for details:
include::../doc/contributing.asciidoc[]

5
.gitignore vendored
View File

@ -15,11 +15,8 @@ __pycache__
/qutebrowser/3rdparty /qutebrowser/3rdparty
/doc/*.html /doc/*.html
/README.html /README.html
/CHANGELOG.html
/CONTRIBUTING.html
/FAQ.html
/INSTALL.html
/qutebrowser/html/doc/ /qutebrowser/html/doc/
/qutebrowser/html/*.html
/.venv* /.venv*
/.coverage /.coverage
/htmlcov /htmlcov

View File

@ -6,18 +6,12 @@ python: 3.6
matrix: matrix:
include: include:
- os: linux
env: DOCKER=debian-jessie
services: docker
- os: linux - os: linux
env: DOCKER=archlinux env: DOCKER=archlinux
services: docker services: docker
- os: linux - 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: DOCKER=ubuntu-xenial
services: docker
- os: linux - os: linux
env: TESTENV=py36-pyqt571 env: TESTENV=py36-pyqt571
- os: linux - os: linux

View File

View File

@ -11,8 +11,8 @@ graft misc/userscripts
recursive-include scripts *.py *.sh recursive-include scripts *.py *.sh
include qutebrowser/utils/testfile include qutebrowser/utils/testfile
include qutebrowser/git-commit-id include qutebrowser/git-commit-id
include COPYING doc/* README.asciidoc CONTRIBUTING.asciidoc FAQ.asciidoc INSTALL.asciidoc CHANGELOG.asciidoc include LICENSE doc/* README.asciidoc
include qutebrowser.desktop include misc/qutebrowser.desktop
include requirements.txt include requirements.txt
include tox.ini include tox.ini
include qutebrowser.py include qutebrowser.py
@ -37,7 +37,6 @@ exclude qutebrowser/javascript/.eslintrc.yaml
exclude qutebrowser/javascript/.eslintignore exclude qutebrowser/javascript/.eslintignore
exclude doc/help exclude doc/help
exclude .* exclude .*
exclude codecov.yml
exclude misc/appveyor_install.py exclude misc/appveyor_install.py
exclude misc/qutebrowser.spec exclude misc/qutebrowser.spec
exclude misc/qutebrowser.nsi exclude misc/qutebrowser.nsi

View File

@ -9,7 +9,7 @@ qutebrowser
// QUTE_WEB_HIDE // QUTE_WEB_HIDE
image:icons/qutebrowser-64x64.png[qutebrowser logo] *A keyboard-driven, vim-like browser based on PyQt5 and Qt.* image:icons/qutebrowser-64x64.png[qutebrowser logo] *A keyboard-driven, vim-like browser based on PyQt5 and Qt.*
image:https://img.shields.io/pypi/l/qutebrowser.svg?style=flat["license badge",link="https://github.com/qutebrowser/qutebrowser/blob/master/COPYING"] image:https://img.shields.io/pypi/l/qutebrowser.svg?style=flat["license badge",link="https://github.com/qutebrowser/qutebrowser/blob/master/LICENSE"]
image:https://img.shields.io/pypi/v/qutebrowser.svg?style=flat["version badge",link="https://pypi.python.org/pypi/qutebrowser/"] image:https://img.shields.io/pypi/v/qutebrowser.svg?style=flat["version badge",link="https://pypi.python.org/pypi/qutebrowser/"]
image:https://requires.io/github/qutebrowser/qutebrowser/requirements.svg?branch=master["requirements badge",link="https://requires.io/github/qutebrowser/qutebrowser/requirements/?branch=master"] image:https://requires.io/github/qutebrowser/qutebrowser/requirements.svg?branch=master["requirements badge",link="https://requires.io/github/qutebrowser/qutebrowser/requirements/?branch=master"]
image:https://travis-ci.org/qutebrowser/qutebrowser.svg?branch=master["Build Status", link="https://travis-ci.org/qutebrowser/qutebrowser"] image:https://travis-ci.org/qutebrowser/qutebrowser.svg?branch=master["Build Status", link="https://travis-ci.org/qutebrowser/qutebrowser"]
@ -36,7 +36,7 @@ Downloads
--------- ---------
See the https://github.com/qutebrowser/qutebrowser/releases[github releases See the https://github.com/qutebrowser/qutebrowser/releases[github releases
page] for available downloads and the link:INSTALL.asciidoc[INSTALL] file for page] for available downloads and the link:doc/install.asciidoc[INSTALL] file for
detailed instructions on how to get qutebrowser running on various platforms. detailed instructions on how to get qutebrowser running on various platforms.
Documentation Documentation
@ -49,11 +49,11 @@ available:
image:https://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="https://qutebrowser.org/img/cheatsheet-big.png"] image:https://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="https://qutebrowser.org/img/cheatsheet-big.png"]
* link:doc/quickstart.asciidoc[Quick start guide] * link:doc/quickstart.asciidoc[Quick start guide]
* A https://www.shortcutfoo.com/app/dojos/qutebrowser[free training course] to remember those key bindings. * A https://www.shortcutfoo.com/app/dojos/qutebrowser[free training course] to remember those key bindings.
* link:FAQ.asciidoc[Frequently asked questions] * link:doc/faq.asciidoc[Frequently asked questions]
* link:doc/help/configuring.html[Configuring qutebrowser] * link:doc/help/configuring.html[Configuring qutebrowser]
* link:CONTRIBUTING.asciidoc[Contributing to qutebrowser] * link:doc/contributing.asciidoc[Contributing to qutebrowser]
* link:INSTALL.asciidoc[INSTALL] * link:doc/install.asciidoc[Installing qutebrowser]
* link:CHANGELOG.asciidoc[Change Log] * link:doc/changelog.asciidoc[Change Log]
* link:doc/stacktrace.asciidoc[Reporting segfaults] * link:doc/stacktrace.asciidoc[Reporting segfaults]
* link:doc/userscripts.asciidoc[How to write userscripts] * link:doc/userscripts.asciidoc[How to write userscripts]
@ -79,7 +79,7 @@ Contributions / Bugs
-------------------- --------------------
You want to contribute to qutebrowser? Awesome! Please read You want to contribute to qutebrowser? Awesome! Please read
link:CONTRIBUTING.asciidoc[the contribution guidelines] for details and link:doc/contributing.asciidoc[the contribution guidelines] for details and
useful hints. useful hints.
If you found a bug or have a feature request, you can report it in several If you found a bug or have a feature request, you can report it in several
@ -99,21 +99,18 @@ Requirements
The following software and libraries are required to run qutebrowser: The following software and libraries are required to run qutebrowser:
* http://www.python.org/[Python] 3.4 or newer (3.6 recommended) - note that * http://www.python.org/[Python] 3.5 or newer (3.6 recommended)
support for Python 3.4 * http://qt.io/[Qt] 5.7.1 or newer with the following modules:
https://github.com/qutebrowser/qutebrowser/issues/2742[will be dropped soon].
* http://qt.io/[Qt] 5.2.0 or newer (5.9 recommended - note that support for Qt
< 5.7.1 will be dropped soon) with the following modules:
- QtCore / qtbase - QtCore / qtbase
- QtQuick (part of qtbase in some distributions) - QtQuick (part of qtbase in some distributions)
- QtSQL (part of qtbase in some distributions) - QtSQL (part of qtbase in some distributions)
- QtOpenGL
- QtWebEngine, or - QtWebEngine, or
- QtWebKit (old or link:https://github.com/annulen/webkit/wiki[reloaded]/NG). - QtWebKit - only the
Note that support for legacy QtWebKit (before 5.212) will be link:https://github.com/annulen/webkit/wiki[updated fork] (5.212) is
dropped soon. supported.
* http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.2.0 or newer * http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.7.0 or newer
(5.9 recommended) for Python 3. Note that support for PyQt < 5.7 will be (5.9 recommended) for Python 3.
dropped soon.
* https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools] * https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools]
* http://fdik.org/pyPEG/[pyPEG2] * http://fdik.org/pyPEG/[pyPEG2]
* http://jinja.pocoo.org/[jinja2] * http://jinja.pocoo.org/[jinja2]
@ -129,8 +126,8 @@ The following libraries are optional:
* http://asciidoc.org/[asciidoc] to generate the documentation for the `:help` * http://asciidoc.org/[asciidoc] to generate the documentation for the `:help`
command, when using the git repository (rather than a release). command, when using the git repository (rather than a release).
See link:INSTALL.asciidoc[INSTALL] for directions on how to install qutebrowser See link:doc/install.asciidoc[the documentation] for directions on how to
and its dependencies. install qutebrowser and its dependencies.
Donating Donating
-------- --------

View File

@ -20,18 +20,24 @@ v1.0.0 (unreleased)
Breaking changes Breaking changes
~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~
- Support for legacy QtWebKit (before 5.212 which is distributed - (TODO) Support for legacy QtWebKit (before 5.212 which is distributed
independently from Qt) is dropped. independently from Qt) is dropped.
- Support for Python 3.4 is dropped. - Support for Python 3.4 is dropped.
- Support for Qt before 5.7 is dropped. - Support for Qt before 5.7.1 and PyQt before 5.7 is dropped.
- (TODO) New dependency on ruamel.yaml; dropped PyYAML dependency.
- (TODO) The QtWebEngine backend is now used by default if available.
- New dependency on the QtSql module and Qt sqlite support. - New dependency on the QtSql module and Qt sqlite support.
- New dependency on ruamel.yaml; dropped PyYAML dependency.
- The QtWebEngine backend is now used by default if available.
- New config system which ignores the old config file. - New config system which ignores the old config file.
- The depedency on PyOpenGL (when using QtWebEngine) got removed. Note - The depedency on PyOpenGL (when using QtWebEngine) got removed. Note
that PyQt5.QtOpenGL is still a dependency. that PyQt5.QtOpenGL is still a dependency.
- PyQt5.QtOpenGL is now always required, even with QtWebKit.
- Migrating QtWebEngine data written by versions before 2016-11-15 (before - Migrating QtWebEngine data written by versions before 2016-11-15 (before
v0.9.0) is now not supported anymore. v0.9.0) is now not supported anymore.
- Upgrading qutebrowser with a version older than v0.4.0 still running now won't
work properly anymore.
- Various documentation files got moved to the doc/ subfolder,
`qutebrowser.desktop` got moved to misc/.
- The `--harfbuzz` commandline argument got dropped
Major changes Major changes
~~~~~~~~~~~~~ ~~~~~~~~~~~~~
@ -44,12 +50,15 @@ Added
~~~~~ ~~~~~
- New back/forward indicator in the statusbar - New back/forward indicator in the statusbar
- New `bindings.key_mappings` setting to map keys to other keys
- New `qt_args` setting to pass additional arguments to Qt/Chromium
- New `backend` setting to select the backend to use (auto/webengine/webkit).
Together with the previous setting, this should make wrapper scripts
unnecessary.
Changed Changed
~~~~~~~ ~~~~~~~
- Upgrading qutebrowser with a version older than v0.4.0 still running now won't
work properly anymore.
- Using `:download` now uses the page's title as filename. - Using `:download` now uses the page's title as filename.
- Using `:back` or `:forward` with a count now skips intermediate pages. - Using `:back` or `:forward` with a count now skips intermediate pages.
- When there are multiple messages shown, the timeout is increased. - When there are multiple messages shown, the timeout is increased.

View File

@ -5,12 +5,6 @@ The Compiler <mail@qutebrowser.org>
:data-uri: :data-uri:
:toc: :toc:
IMPORTANT: I'm currently (July 2017) more busy than usual until September,
because of exams coming up. In addition to that, a new config system is coming
which will conflict with many non-trivial contributions. Because of that, please
refrain from contributing new features until then. If you're reading this note
after mid-September, please open an issue.
I `&lt;3` footnote:[Of course, that says `<3` in HTML.] contributors! I `&lt;3` footnote:[Of course, that says `<3` in HTML.] contributors!
This document contains guidelines for contributing to qutebrowser, as well as This document contains guidelines for contributing to qutebrowser, as well as
@ -104,10 +98,9 @@ unittests and several linters/checkers.
Currently, the following tox environments are available: Currently, the following tox environments are available:
* Tests using https://www.pytest.org[pytest]: * Tests using https://www.pytest.org[pytest]:
- `py34`: Run pytest for python-3.4. - `py35`, `py36`: Run pytest for python 3.5/3.6 with the system-wide PyQt.
- `py35`: Run pytest for python-3.5. - `py36-pyqt57`, ..., `py36-pyqt59`: Run pytest with the given PyQt version (`py35-*` also works)
- `py34-cov`: Run pytest for python-3.4 with code coverage report. - `py36-pyqt59-cov`: Run with coverage support (other Python/PyQt versions work too)
- `py35-cov`: Run pytest for python-3.5 with code coverage report.
* `flake8`: Run https://pypi.python.org/pypi/flake8[flake8] checks: * `flake8`: Run https://pypi.python.org/pypi/flake8[flake8] checks:
https://pypi.python.org/pypi/pyflakes[pyflakes], https://pypi.python.org/pypi/pyflakes[pyflakes],
https://pypi.python.org/pypi/pep8[pep8], https://pypi.python.org/pypi/pep8[pep8],
@ -187,7 +180,7 @@ In the _scripts/_ subfolder there's a `run_profile.py` which profiles the code
and shows a graphical representation of what takes how much time. and shows a graphical representation of what takes how much time.
It uses the built-in Python It uses the built-in Python
https://docs.python.org/3.4/library/profile.html[cProfile] module and can show https://docs.python.org/3.6/library/profile.html[cProfile] module and can show
the output in four different ways: the output in four different ways:
* Raw profile file (`--profile-tool=none`) * Raw profile file (`--profile-tool=none`)
@ -542,11 +535,11 @@ ____
Setting up a Windows Development Environment Setting up a Windows Development Environment
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Install https://www.python.org/downloads/release/python-344/[Python 3.4] * Install https://www.python.org/downloads/release/python-362/[Python 3.6].
* Install https://sourceforge.net/projects/pyqt/files/PyQt5/PyQt-5.5.1/[PyQt 5.5] * Install PyQt via `pip install PyQt5`
* Create a file at `C:\Windows\system32\python3.bat` with the following content: * Create a file at `C:\Windows\system32\python3.bat` with the following content (adjust the path as necessary):
`@C:\Python34\python %*` `@C:\Python36\python %*`
This will make the Python 3.4 interpreter available as `python3`, which is used by various development scripts. This will make the Python 3.6 interpreter available as `python3`, which is used by various development scripts.
* Install git from the https://git-scm.com/download/win[git-scm downloads page] * Install git from the https://git-scm.com/download/win[git-scm downloads page]
Try not to enable `core.autocrlf`, since that will cause `flake8` to complain a lot. Use an editor that can deal with plain line feeds instead. Try not to enable `core.autocrlf`, since that will cause `flake8` to complain a lot. Use an editor that can deal with plain line feeds instead.
* Clone your favourite qutebrowser repository. * Clone your favourite qutebrowser repository.

View File

@ -205,26 +205,6 @@ Experiencing freezing on sites like duckduckgo and youtube.::
See https://github.com/qutebrowser/qutebrowser/issues/357[Issue #357] See https://github.com/qutebrowser/qutebrowser/issues/357[Issue #357]
for more details. for more details.
Experiencing segfaults (crashes) on Debian systems.::
For Debian it's highly recommended to install the `gstreamer0.10-plugins-base` package.
This is a workaround for a bug in Qt, it has been fixed upstream in Qt 5.4
More details can be found
https://bugs.webkit.org/show_bug.cgi?id=119951[here].
Segfaults on Facebook, Medium, Amazon, ...::
If you are on a Debian or Ubuntu based system, you might experience some crashes
visiting these sites. This is caused by various bugs in Qt which have been
fixed in Qt 5.4. However Debian and Ubuntu are slow to adopt or upgrade
some packages. On Debian Jessie, it's recommended to use the experimental
repos as described in https://github.com/qutebrowser/qutebrowser/blob/master/INSTALL.asciidoc#on-debian--ubuntu[INSTALL].
+
Since Ubuntu Trusty (using Qt 5.2.1),
https://bugreports.qt.io/browse/QTBUG-42417?jql=component%20%3D%20WebKit%20and%20resolution%20%3D%20Done%20and%20fixVersion%20in%20(5.3.0%2C%20%225.3.0%20Alpha%22%2C%20%225.3.0%20Beta1%22%2C%20%225.3.0%20RC1%22%2C%205.3.1%2C%205.3.2%2C%205.4.0%2C%20%225.4.0%20Alpha%22%2C%20%225.4.0%20Beta%22%2C%20%225.4.0%20RC%22)%20and%20priority%20in%20(%22P2%3A%20Important%22%2C%20%22P1%3A%20Critical%22%2C%20%22P0%3A%20Blocker%22)[over
70 important bugs] have been fixed in QtWebKit. For Debian Jessie (using Qt 5.3.2)
it's still
https://bugreports.qt.io/browse/QTBUG-42417?jql=component%20%3D%20WebKit%20and%20resolution%20%3D%20Done%20and%20fixVersion%20in%20(5.4.0%2C%20%225.4.0%20Alpha%22%2C%20%225.4.0%20Beta%22%2C%20%225.4.0%20RC%22)%20and%20priority%20in%20(%22P2%3A%20Important%22%2C%20%22P1%3A%20Critical%22%2C%20%22P0%3A%20Blocker%22)[nearly
20 important bugs].
When using QtWebEngine, qutebrowser reports "Render Process Crashed" and the console prints a traceback on Gentoo Linux or another Source-Based Distro:: When using QtWebEngine, qutebrowser reports "Render Process Crashed" and the console prints a traceback on Gentoo Linux or another Source-Based Distro::
As stated in https://gcc.gnu.org/gcc-6/changes.html[GCC's Website] GCC 6 has introduced some optimizations that could break non-conforming codebases, like QtWebEngine. + As stated in https://gcc.gnu.org/gcc-6/changes.html[GCC's Website] GCC 6 has introduced some optimizations that could break non-conforming codebases, like QtWebEngine. +
As a workaround, you can disable the nullpointer check optimization by adding the -fno-delete-null-pointer-checks flag while compiling. + As a workaround, you can disable the nullpointer check optimization by adding the -fno-delete-null-pointer-checks flag while compiling. +

View File

@ -66,6 +66,9 @@ link:commands.html#unbind[`:unbind`] commands:
Key chains starting with a comma are ideal for custom bindings, as the comma key Key chains starting with a comma are ideal for custom bindings, as the comma key
will never be used in a default keybinding. will never be used in a default keybinding.
See the help pages linked above (or `:help :bind`, `:help :unbind`) for more
information.
Configuring qutebrowser via config.py Configuring qutebrowser via config.py
------------------------------------- -------------------------------------
@ -105,8 +108,8 @@ accepted values depend on the type of the option. Commonly used are:
- Booleans: `c.completion.shrink = True` - Booleans: `c.completion.shrink = True`
- Integers: `c.messages.timeout = 5000` - Integers: `c.messages.timeout = 5000`
- Dictionaries: - Dictionaries:
* `c.headers.custom = {'X-Hello': 'World'}` to override any other values in the * `c.headers.custom = {'X-Hello': 'World', 'X-Awesome': 'yes'}` to override
dictionary. any other values in the dictionary.
* `c.aliases['foo'] = ':message-info foo'` to add a single value. * `c.aliases['foo'] = ':message-info foo'` to add a single value.
- Lists: - Lists:
* `c.url.start_pages = ["https://www.qutebrowser.org/"]` to override any * `c.url.start_pages = ["https://www.qutebrowser.org/"]` to override any
@ -159,26 +162,39 @@ To bind a key:
.config.py: .config.py:
[source,python] [source,python]
---- ----
config.bind(',v', 'spawn mpv {url}', mode='normal') config.bind('<Ctrl-v>', 'spawn mpv {url}')
----
To bind a key in a mode other than `'normal'`, add a `mode` argument:
[source,python]
----
config.bind('<Ctrl-y>', 'prompt-yes', mode='prompt')
---- ----
If the key is already bound, `force=True` needs to be given to rebind it: If the key is already bound, `force=True` needs to be given to rebind it:
[source,python] [source,python]
---- ----
config.bind(',v', 'message-info foo', mode='normal', force=True) config.bind('<Ctrl-v>', 'message-info foo', force=True)
---- ----
To unbind a key (either a key which has been bound before, or a default binding): To unbind a key (either a key which has been bound before, or a default binding):
[source,python] [source,python]
---- ----
config.unbind(',v', mode='normal') config.unbind('<Ctrl-v>', mode='normal')
---- ----
Key chains starting with a comma are ideal for custom bindings, as the comma key To bind keys without modifiers, specify a key chain to bind as a string. Key
chains starting with a comma are ideal for custom bindings, as the comma key
will never be used in a default keybinding. will never be used in a default keybinding.
[source,python]
----
config.bind(',v', 'spawn mpv {url}')
----
To suppress loading of any default keybindings, you can set To suppress loading of any default keybindings, you can set
`c.bindings.default = {}`. `c.bindings.default = {}`.

View File

@ -7,13 +7,13 @@ Documentation
The following help pages are currently available: The following help pages are currently available:
* link:../quickstart.html[Quick start guide] * link:../quickstart.html[Quick start guide]
* link:../../FAQ.html[Frequently asked questions] * link:../doc.html[Frequently asked questions]
* link:../../CHANGELOG.html[Change Log] * link:../changelog.html[Change Log]
* link:commands.html[Documentation of commands] * link:commands.html[Documentation of commands]
* link:configuring.html[Configuring qutebrowser] * link:configuring.html[Configuring qutebrowser]
* link:settings.html[Documentation of settings] * link:settings.html[Documentation of settings]
* link:../userscripts.html[How to write userscripts] * link:../userscripts.html[How to write userscripts]
* link:../../CONTRIBUTING.html[Contributing to qutebrowser] * link:../contributing.html[Contributing to qutebrowser]
Getting help Getting help
------------ ------------

View File

@ -12,6 +12,7 @@
|<<aliases,aliases>>|Aliases for commands. |<<aliases,aliases>>|Aliases for commands.
|<<auto_save.interval,auto_save.interval>>|How often (in milliseconds) to auto-save config/cookies/etc. |<<auto_save.interval,auto_save.interval>>|How often (in milliseconds) to auto-save config/cookies/etc.
|<<auto_save.session,auto_save.session>>|Always restore open sites when qutebrowser is reopened. |<<auto_save.session,auto_save.session>>|Always restore open sites when qutebrowser is reopened.
|<<backend,backend>>|The backend to use to display websites.
|<<bindings.commands,bindings.commands>>|Keybindings mapping keys to commands in different modes. |<<bindings.commands,bindings.commands>>|Keybindings mapping keys to commands in different modes.
|<<bindings.default,bindings.default>>|Default keybindings. If you want to add bindings, modify `bindings.commands` instead. |<<bindings.default,bindings.default>>|Default keybindings. If you want to add bindings, modify `bindings.commands` instead.
|<<bindings.key_mappings,bindings.key_mappings>>|This setting can be used to map keys to other keys. |<<bindings.key_mappings,bindings.key_mappings>>|This setting can be used to map keys to other keys.
@ -208,6 +209,7 @@
|<<new_instance_open_target_window,new_instance_open_target_window>>|Which window to choose when opening links as new tabs. |<<new_instance_open_target_window,new_instance_open_target_window>>|Which window to choose when opening links as new tabs.
|<<prompt.filebrowser,prompt.filebrowser>>|Show a filebrowser in upload/download prompts. |<<prompt.filebrowser,prompt.filebrowser>>|Show a filebrowser in upload/download prompts.
|<<prompt.radius,prompt.radius>>|The rounding radius for the edges of prompts. |<<prompt.radius,prompt.radius>>|The rounding radius for the edges of prompts.
|<<qt_args,qt_args>>|Additional arguments to pass to Qt, without leading `--`.
|<<scrolling.bar,scrolling.bar>>|Show a scrollbar. |<<scrolling.bar,scrolling.bar>>|Show a scrollbar.
|<<scrolling.smooth,scrolling.smooth>>|Enable smooth scrolling for web pages. |<<scrolling.smooth,scrolling.smooth>>|Enable smooth scrolling for web pages.
|<<session_default_name,session_default_name>>|The name of the session to save by default. |<<session_default_name,session_default_name>>|The name of the session to save by default.
@ -253,6 +255,7 @@
[[aliases]] [[aliases]]
=== aliases === aliases
Aliases for commands. Aliases for commands.
The keys of the given dictionary are the aliases, while the values are the commands they map to.
Type: <<types,Dict>> Type: <<types,Dict>>
@ -283,11 +286,29 @@ Valid values:
Default: empty Default: empty
[[backend]]
=== backend
The backend to use to display websites.
qutebrowser supports two different web rendering engines / backends, QtWebKit and QtWebEngine.
QtWebKit is based on WebKit (similar to Safari). It was discontinued by the Qt project with Qt 5.6, but picked up as a well maintained fork: https://github.com/annulen/webkit/wiki - qutebrowser only supports the fork.
QtWebEngine is Qt's official successor to QtWebKit and based on the Chromium project. It's slightly more resource hungry that QtWebKit and has a couple of missing features in qutebrowser, but is generally the preferred choice.
Type: <<types,String>>
Valid values:
* +auto+: Automatically select either QtWebEngine or QtWebKit
* +webkit+: Force QtWebKit
* +webengine+: Force QtWebEngine
Default: +pass:[auto]+
[[bindings.commands]] [[bindings.commands]]
=== bindings.commands === bindings.commands
Keybindings mapping keys to commands in different modes. Keybindings mapping keys to commands in different modes.
This setting is a dictionary containing mode names and dictionaries mapping keys to commands: This setting is a dictionary containing mode names and dictionaries mapping keys to commands:
`{mode: {key: command}}` `{mode: {key: command}}`
While it's possible to bind keys by modifying this setting, it's recommended to use the `config.bind()` function or `:bind` command, which makes modifying it easier and checks for more possible mistakes.
If you want to map a key to another key, check the `bindings.key_mappings` setting instead. If you want to map a key to another key, check the `bindings.key_mappings` setting instead.
For special keys (can't be part of a keychain), enclose them in `<`...`>`. For modifiers, you can use either `-` or `+` as delimiters, and these names: For special keys (can't be part of a keychain), enclose them in `<`...`>`. For modifiers, you can use either `-` or `+` as delimiters, and these names:
@ -344,7 +365,7 @@ Default: empty
=== bindings.default === bindings.default
Default keybindings. If you want to add bindings, modify `bindings.commands` instead. Default keybindings. If you want to add bindings, modify `bindings.commands` instead.
The main purpose of this setting is that you can set it to an empty dictionary if you want to load no default keybindings at all. The main purpose of this setting is that you can set it to an empty dictionary if you want to load no default keybindings at all.
If you want to preserve default bindings (and get new bindings when there is an update), add new bindings to `bindings.commands` (or use `:bind`) and leave this setting alone. If you want to preserve default bindings (and get new bindings when there is an update), use `config.bind()` in `config.py` or the `:bind` command, and leave this setting alone.
Type: <<types,Dict>> Type: <<types,Dict>>
@ -1693,6 +1714,7 @@ Default: +pass:[true]+
[[content.javascript.log]] [[content.javascript.log]]
=== content.javascript.log === content.javascript.log
Log levels to use for JavaScript console logging messages. Log levels to use for JavaScript console logging messages.
When a JavaScript message with the level given in the dictionary key is logged, the corresponding dictionary value selects the qutebrowser logger to use.
On QtWebKit, the "unknown" setting is always used. On QtWebKit, the "unknown" setting is always used.
Type: <<types,Dict>> Type: <<types,Dict>>
@ -2084,7 +2106,7 @@ Font used for the hints.
Type: <<types,Font>> Type: <<types,Font>>
Default: +pass:[bold 13px monospace]+ Default: +pass:[bold 10pt monospace]+
[[fonts.keyhint]] [[fonts.keyhint]]
=== fonts.keyhint === fonts.keyhint
@ -2604,6 +2626,15 @@ Type: <<types,Int>>
Default: +pass:[8]+ Default: +pass:[8]+
[[qt_args]]
=== qt_args
Additional arguments to pass to Qt, without leading `--`.
With QtWebEngine, some Chromium arguments (see https://peter.sh/experiments/chromium-command-line-switches/ for a list) will work.
Type: <<types,List>>
Default: empty
[[scrolling.bar]] [[scrolling.bar]]
=== scrolling.bar === scrolling.bar
Show a scrollbar. Show a scrollbar.
@ -3002,7 +3033,8 @@ Default:
[[url.searchengines]] [[url.searchengines]]
=== url.searchengines === url.searchengines
Definitions of search engines which can be used via the address bar. Definitions of search engines which can be used via the address bar.
The searchengine named `DEFAULT` is used when `url.auto_search` is turned on and something else than a URL was entered to be opened. Other search engines can be used by prepending the search engine name to the search term, e.g. `:open google qutebrowser`. The string `{}` will be replaced by the search term, use `{{` and `}}` for literal `{`/`}` signs. Maps a searchengine name (such as `DEFAULT`, or `ddg`) to a URL with a `{}` placeholder. The placeholder will be replaced by the search term, use `{{` and `}}` for literal `{`/`}` signs.
The searchengine named `DEFAULT` is used when `url.auto_search` is turned on and something else than a URL was entered to be opened. Other search engines can be used by prepending the search engine name to the search term, e.g. `:open google qutebrowser`.
Type: <<types,Dict>> Type: <<types,Dict>>

View File

@ -3,39 +3,50 @@ Installing qutebrowser
toc::[] toc::[]
NOTE: qutebrowser recently had some bigger dependency changes for v1.0.0, which
means those instructions might be out of date in some places.
https://github.com/qutebrowser/qutebrowser/blob/master/doc/contributing.asciidoc[Please help]
updating them if you notice something being broken!
On Debian / Ubuntu On Debian / Ubuntu
------------------ ------------------
qutebrowser should run on these systems: How to install qutebrowser depends a lot on the version of Debian/Ubuntu you're
running.
* Debian jessie or newer Debian Jessie / Ubuntu 14.04 LTS / Linux Mint < 18
* Ubuntu Trusty (14.04 LTS) or newer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Any other distribution based on these (e.g. Linux Mint 17+)
Unfortunately there is no Debian package in the official repos yet, but installing qutebrowser is Those distributions only have Python 3.4 and a too old Qt version available,
still relatively easy! while qutebrowser requires Python 3.5 and Qt 5.7.1 or newer.
You can use packages that are built for every release or build it yourself from git. It should be possible to install Python 3.5 e.g. from the
https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa[deadsnakes PPA] or via_ipca
https://github.com/pyenv/pyenv[pyenv], but nobody tried that yet.
On Ubuntu 16.04 and 16.10 it's recommended to <<tox,install qutebrowser via tox>> If you get qutebrowser running on those distributions, please
instead in order to be able to use the new QtWebEngine backend. Newer versions https://github.com/qutebrowser/qutebrowser/blob/master/doc/contributing.asciidoc[contribute]
have a QtWebEngine package in the repositories. to update this documentation!
Using the packages Ubuntu 16.04 LTS / Linux Mint 18
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Ubuntu 16.04 doesn't come with an up-to-date engine (a new enough QtWebKit, or
QtWebEngine). However, it comes with Python 3.5, so you can
<<tox,install qutebrowser via tox>>.
Debian Stretch / Ubuntu 17.04 and newer
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Those versions come with QtWebEngine in the repositories. This makes it possible
to install qutebrowser via the Debian package.
Install the dependencies via apt-get: Install the dependencies via apt-get:
---- ----
# apt-get install python3-lxml python-tox python3-pyqt5 python3-pyqt5.qtwebkit python3-pyqt5.qtquick python3-sip python3-jinja2 python3-pygments python3-yaml python3-pyqt5.qtsql libqt5sql5-sqlite # apt install python-tox python3-{lxml,pyqt5,sip,jinja2,pygments,yaml} python3-pyqt5.qt{webengine,quick,opengl,sql} libqt5sql5-sqlite
---- ----
On Debian Stretch or Ubuntu 17.04 or later, it's also recommended to use the
newer QtWebEngine backend.
To do so, install `python3-pyqt5.qtwebengine` and `python3-pyqt5.qtopengl`, then
start qutebrowser with `--backend webengine`.
Get the qutebrowser package from the Get the qutebrowser package from the
https://github.com/qutebrowser/qutebrowser/releases[release page] and download https://github.com/qutebrowser/qutebrowser/releases[release page] and download
the https://qutebrowser.org/python3-pypeg2_2.15.2-1_all.deb[PyPEG2 package]. the https://qutebrowser.org/python3-pypeg2_2.15.2-1_all.deb[PyPEG2 package].
@ -47,35 +58,27 @@ Install the packages:
# dpkg -i qutebrowser_*_all.deb # dpkg -i qutebrowser_*_all.deb
---- ----
Build it from git Some additional hints:
~~~~~~~~~~~~~~~~~
Install the dependencies via apt-get:
- Alternatively, you can <<tox,install qutebrowser via tox>> to get a newer
QtWebEngine version.
- If running from git, run the following to generate the documentation for the
`:help` command:
+
---- ----
# apt-get install python3-pyqt5 python3-pyqt5.qtwebkit python3-pyqt5.qtquick python-tox python3-sip python3-dev python3-pyqt5.qtsql libqt5sql5-sqlite # apt-get install --no-install-recommends asciidoc source-highlight
----
On Debian Stretch or Ubuntu 17.04 or later, it's also recommended to install
`python3-pyqt5.qtwebengine` and start qutebrowser with `--backend webengine` in
order to use the new backend.
To generate the documentation for the `:help` command, when using the git
repository (rather than a release):
----
# apt-get install asciidoc source-highlight
$ python3 scripts/asciidoc2html.py $ python3 scripts/asciidoc2html.py
---- ----
If video or sound don't seem to work, try installing the gstreamer plugins: - If you prefer using QtWebKit, there's an up-to-date version available in
Debian experimental, or from http://repo.paretje.be/unstable/[this repository]
for Debian Stretch.
- If video or sound don't work with QtWebKit, try installing the gstreamer plugins:
+
---- ----
# apt-get install gstreamer1.0-plugins-{bad,base,good,ugly} # apt-get install gstreamer1.0-plugins-{bad,base,good,ugly}
---- ----
Then <<tox,install qutebrowser via tox>>.
On Fedora On Fedora
--------- ---------
@ -116,7 +119,7 @@ $ rm -r qutebrowser-git
or you could use an AUR helper, e.g. `yaourt -S qutebrowser-git`. or you could use an AUR helper, e.g. `yaourt -S qutebrowser-git`.
If video or sound don't seem to work, try installing the gstreamer plugins: If video or sound don't work with QtWebKit, try installing the gstreamer plugins:
---- ----
# pacman -S gst-plugins-{base,good,bad,ugly} gst-libav # pacman -S gst-plugins-{base,good,bad,ugly} gst-libav
@ -125,6 +128,8 @@ If video or sound don't seem to work, try installing the gstreamer plugins:
On Gentoo On Gentoo
--------- ---------
WARNING: The Gentoo packages (even the live version) are lagging behind a lot, which means those instructions probably won't work anymore. Until things are looking better, it's recommended to <<tox,install qutebrowser via tox>>.
A version of qutebrowser is available in the main repository and can be installed with: A version of qutebrowser is available in the main repository and can be installed with:
---- ----
@ -161,15 +166,11 @@ To update to the last Live version, remember to do
To include qutebrowser among the updates. To include qutebrowser among the updates.
Make sure you have `python3_4` in your `PYTHON_TARGETS` You'll also need to install `dev-qt/qtwebengine` or a newer QtWebKit using
(`/etc/portage/make.conf`) and rebuild your system (`emerge -uDNav @world`) if https://gist.github.com/annulen/309569fb61e5d64a703c055c1e726f71[this ebuild].
necessary.
It's also recommended to install QtWebKit-NG via If video or sound don't work with QtWebKit, try installing the gstreamer
https://gist.github.com/annulen/309569fb61e5d64a703c055c1e726f71[this ebuild], plugins:
or install Qt >= 5.7.1 with QtWebEngine in order to use an up-to-date backend.
If video or sound don't seem to work, try installing the gstreamer plugins:
---- ----
# emerge -av gst-plugins-{base,good,bad,ugly,libav} # emerge -av gst-plugins-{base,good,bad,ugly,libav}
@ -214,6 +215,8 @@ On openSUSE
There are prebuilt RPMs available at https://software.opensuse.org/download.html?project=network&package=qutebrowser[OBS]. There are prebuilt RPMs available at https://software.opensuse.org/download.html?project=network&package=qutebrowser[OBS].
To use the QtWebEngine backend, install `libqt5-qtwebengine`.
On OpenBSD On OpenBSD
---------- ----------
@ -328,6 +331,9 @@ it as part of the packaging process.
Installing qutebrowser with tox Installing qutebrowser with tox
------------------------------- -------------------------------
Getting the repository
~~~~~~~~~~~~~~~~~~~~~~
First of all, clone the repository using http://git-scm.org/[git] and switch First of all, clone the repository using http://git-scm.org/[git] and switch
into the repository folder: into the repository folder:
@ -336,6 +342,8 @@ $ git clone https://github.com/qutebrowser/qutebrowser.git
$ cd qutebrowser $ cd qutebrowser
---- ----
Installing depdendencies (including Qt)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Then run tox inside the qutebrowser repository to set up a Then run tox inside the qutebrowser repository to set up a
https://docs.python.org/3/library/venv.html[virtual environment]: https://docs.python.org/3/library/venv.html[virtual environment]:
@ -344,20 +352,39 @@ https://docs.python.org/3/library/venv.html[virtual environment]:
$ tox -e mkvenv-pypi $ tox -e mkvenv-pypi
---- ----
If your distribution uses OpenSSL 1.1 (like Debian Stretch or Archlinux), you'll This installs all needed Python dependencies in a `.venv` subfolder.
need to set `LD_LIBRARY_PATH` to the OpenSSL 1.0 directory
(`export LD_LIBRARY_PATH=/usr/lib/openssl-1.0` on Archlinux) before starting This comes with an up-to-date Qt/PyQt including QtWebEngine, but has a few
qutebrowser. caveats:
- Make sure your `python3` is Python 3.5 or newer, otherwise you'll get a "No
matching distribution found" error. Note that qutebrowser itself also requires
this.
- It only works on 64-bit x86 systems, with other architectures you'll get the
same error.
- If your distribution uses OpenSSL 1.1 (like Debian Stretch or Archlinux),
you'll need to set `LD_LIBRARY_PATH` to the OpenSSL 1.0 directory
(`export LD_LIBRARY_PATH=/usr/lib/openssl-1.0` on Archlinux) before starting
qutebrowser if you want SSL to work in certain downloads (e.g. for
`:adblock-update` or `:download`).
- It comes with a QtWebEngine compiled without proprietary codec support (such
as h.264).
See the next section for an alternative.
Installing dependencies (system-wide Qt)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Alternatively, you can use `tox -e mkvenv` (without `-pypi`) to symlink your Alternatively, you can use `tox -e mkvenv` (without `-pypi`) to symlink your
local Qt install instead of installing PyQt in the virtualenv. However, unless local Qt install instead of installing PyQt in the virtualenv. However, unless
you have QtWebKit-NG or QtWebEngine available, qutebrowser will use the legacy you have a new QtWebKit or QtWebEngine available, qutebrowser will not work. It
QtWebKit backend. also typically means you'll be using an older release of QtWebEngine.
On Windows, run `tox -e 'mkvenv-win' instead, however make sure that ONLY On Windows, run `tox -e 'mkvenv-win' instead, however make sure that ONLY
Python3 is in your PATH before running tox. Python3 is in your PATH before running tox.
This installs all needed Python dependencies in a `.venv` subfolder. Creating a wrapper script
~~~~~~~~~~~~~~~~~~~~~~~~~
You can then create a simple wrapper script to start qutebrowser somewhere in You can then create a simple wrapper script to start qutebrowser somewhere in
your `$PATH` (e.g. `/usr/local/bin/qutebrowser` or `~/bin/qutebrowser`): your `$PATH` (e.g. `/usr/local/bin/qutebrowser` or `~/bin/qutebrowser`):

View File

@ -84,9 +84,6 @@ show it.
*--force-color*:: *--force-color*::
Force colored logging Force colored logging
*--harfbuzz* '{old,new,system,auto}'::
HarfBuzz engine version to use. Default: auto.
*--relaxed-config*:: *--relaxed-config*::
Silently remove unknown config options. Silently remove unknown config options.

View File

@ -23,7 +23,7 @@ SetCompressor /solid lzma
!define MUI_ICON "../icons/qutebrowser.ico" !define MUI_ICON "../icons/qutebrowser.ico"
!define MUI_UNICON "../icons/qutebrowser.ico" !define MUI_UNICON "../icons/qutebrowser.ico"
!insertmacro MUI_PAGE_LICENSE "..\COPYING" !insertmacro MUI_PAGE_LICENSE "..\LICENSE"
!insertmacro MUI_PAGE_DIRECTORY !insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES !insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_UNPAGE_CONFIRM !insertmacro MUI_UNPAGE_CONFIRM

View File

@ -20,4 +20,4 @@ pycodestyle==2.3.1
pydocstyle==1.1.1 # rq.filter: < 2.0.0 pydocstyle==1.1.1 # rq.filter: < 2.0.0
pyflakes==1.6.0 pyflakes==1.6.0
pyparsing==2.2.0 pyparsing==2.2.0
six==1.10.0 six==1.11.0

View File

@ -1,3 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
altgraph==0.14
future==0.16.0
macholib==1.8
pefile==2017.9.3
-e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=PyInstaller -e git+https://github.com/pyinstaller/pyinstaller.git@develop#egg=PyInstaller

View File

@ -11,7 +11,7 @@ mccabe==0.6.1
-e git+https://github.com/PyCQA/pylint.git#egg=pylint -e git+https://github.com/PyCQA/pylint.git#egg=pylint
./scripts/dev/pylint_checkers ./scripts/dev/pylint_checkers
requests==2.18.4 requests==2.18.4
six==1.10.0 six==1.11.0
uritemplate==3.0.0 uritemplate==3.0.0
uritemplate.py==3.0.2 uritemplate.py==3.0.2
urllib3==1.22 urllib3==1.22

View File

@ -11,7 +11,7 @@ mccabe==0.6.1
pylint==1.7.2 pylint==1.7.2
./scripts/dev/pylint_checkers ./scripts/dev/pylint_checkers
requests==2.18.4 requests==2.18.4
six==1.10.0 six==1.11.0
uritemplate==3.0.0 uritemplate==3.0.0
uritemplate.py==3.0.2 uritemplate.py==3.0.2
urllib3==1.22 urllib3==1.22

View File

@ -4,7 +4,6 @@ hg+https://bitbucket.org/ned/coveragepy
git+https://github.com/micheles/decorator.git git+https://github.com/micheles/decorator.git
git+https://github.com/pallets/flask.git git+https://github.com/pallets/flask.git
git+https://github.com/miracle2k/python-glob2.git git+https://github.com/miracle2k/python-glob2.git
git+https://github.com/Runscope/httpbin.git
git+https://github.com/HypothesisWorks/hypothesis-python.git git+https://github.com/HypothesisWorks/hypothesis-python.git
git+https://github.com/pallets/itsdangerous.git git+https://github.com/pallets/itsdangerous.git
git+https://bitbucket.org/zzzeek/mako.git git+https://bitbucket.org/zzzeek/mako.git

View File

@ -1,18 +1,17 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
attrs==17.2.0
beautifulsoup4==4.6.0 beautifulsoup4==4.6.0
cheroot==5.8.3 cheroot==5.8.3
click==6.7 click==6.7
# colorama==0.3.9 # colorama==0.3.9
coverage==4.4.1 coverage==4.4.1
decorator==4.1.2
EasyProcess==0.2.3 EasyProcess==0.2.3
fields==5.0.0 fields==5.0.0
Flask==0.12.2 Flask==0.12.2
glob2==0.6 glob2==0.6
httpbin==0.5.0 hunter==2.0.1
hunter==1.4.1 hypothesis==3.28.3
hypothesis==3.19.1
itsdangerous==0.24 itsdangerous==0.24
# Jinja2==2.9.6 # Jinja2==2.9.6
Mako==1.0.7 Mako==1.0.7
@ -20,20 +19,21 @@ Mako==1.0.7
parse==1.8.2 parse==1.8.2
parse-type==0.3.4 parse-type==0.3.4
py==1.4.34 py==1.4.34
pytest==3.2.1 py-cpuinfo==3.3.0
pytest==3.2.2
pytest-bdd==2.18.2 pytest-bdd==2.18.2
pytest-benchmark==3.1.1 pytest-benchmark==3.1.1
pytest-catchlog==1.2.2 pytest-catchlog==1.2.2
pytest-cov==2.5.1 pytest-cov==2.5.1
pytest-faulthandler==1.3.1 pytest-faulthandler==1.3.1
pytest-instafail==0.3.0 pytest-instafail==0.3.0
pytest-mock==1.6.2 pytest-mock==1.6.3
pytest-qt==2.1.2 pytest-qt==2.2.0
pytest-repeat==0.4.1 pytest-repeat==0.4.1
pytest-rerunfailures==3.0 pytest-rerunfailures==3.1
pytest-travis-fold==1.2.0 pytest-travis-fold==1.2.0
pytest-xvfb==1.0.0 pytest-xvfb==1.0.0
PyVirtualDisplay==0.2.1 PyVirtualDisplay==0.2.1
six==1.10.0 six==1.11.0
vulture==0.25 vulture==0.26
Werkzeug==0.12.2 Werkzeug==0.12.2

View File

@ -3,7 +3,6 @@ cheroot
coverage coverage
Flask Flask
hunter hunter
httpbin
hypothesis hypothesis
pytest pytest
pytest-bdd pytest-bdd

View File

@ -2,5 +2,5 @@
pluggy==0.4.0 pluggy==0.4.0
py==1.4.34 py==1.4.34
tox==2.7.0 tox==2.8.2
virtualenv==15.1.0 virtualenv==15.1.0

View File

@ -1,3 +1,3 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
vulture==0.25 vulture==0.26

View File

@ -19,15 +19,12 @@ markers =
qtwebengine_todo: Features still missing with QtWebEngine qtwebengine_todo: Features still missing with QtWebEngine
qtwebengine_skip: Tests not applicable with QtWebEngine qtwebengine_skip: Tests not applicable with QtWebEngine
qtwebkit_skip: Tests not applicable with QtWebKit qtwebkit_skip: Tests not applicable with QtWebKit
qtwebkit_ng_xfail: Tests failing with QtWebKit-NG
qtwebkit_ng_skip: Tests skipped with QtWebKit-NG
qtwebengine_flaky: Tests which are flaky (and currently skipped) with QtWebEngine qtwebengine_flaky: Tests which are flaky (and currently skipped) with QtWebEngine
qtwebengine_mac_xfail: Tests which fail on macOS with QtWebEngine qtwebengine_mac_xfail: Tests which fail on macOS with QtWebEngine
js_prompt: Tests needing to display a javascript prompt js_prompt: Tests needing to display a javascript prompt
this: Used to mark tests during development this: Used to mark tests during development
no_invalid_lines: Don't fail on unparseable lines in end2end tests no_invalid_lines: Don't fail on unparseable lines in end2end tests
issue2478: Tests which are broken on Windows with QtWebEngine, https://github.com/qutebrowser/qutebrowser/issues/2478 issue2478: Tests which are broken on Windows with QtWebEngine, https://github.com/qutebrowser/qutebrowser/issues/2478
qt55: Tests only running on Qt 5.5 or later
qt_log_level_fail = WARNING qt_log_level_fail = WARNING
qt_log_ignore = qt_log_ignore =
^SpellCheck: .* ^SpellCheck: .*

View File

@ -43,7 +43,7 @@ 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 cmdutils, runners, cmdexc
from qutebrowser.config import config, websettings, configexc from qutebrowser.config import config, websettings, configexc, configfiles
from qutebrowser.browser import (urlmarks, adblock, history, browsertab, from qutebrowser.browser import (urlmarks, adblock, history, browsertab,
downloads) downloads)
from qutebrowser.browser.network import proxy from qutebrowser.browser.network import proxy
@ -52,11 +52,14 @@ from qutebrowser.browser.webkit.network import networkmanager
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,
crashsignal, earlyinit, objects, sql, cmdhistory) crashsignal, earlyinit, sql, cmdhistory)
from qutebrowser.misc import utilcmds # pylint: disable=unused-import from qutebrowser.utils import (log, version, message, utils, urlutils, objreg,
from qutebrowser.utils import (log, version, message, utils, qtutils, urlutils, usertypes, standarddir, error)
objreg, usertypes, standarddir, error) # pylint: disable=unused-import
# We import utilcmds to run the cmdutils.register decorators. # We import those to run the cmdutils.register decorators.
from qutebrowser.mainwindow.statusbar import command
from qutebrowser.misc import utilcmds
# pylint: enable=unused-import
qApp = None qApp = None
@ -73,6 +76,9 @@ def run(args):
log.init.debug("Initializing directories...") log.init.debug("Initializing directories...")
standarddir.init(args) standarddir.init(args)
log.init.debug("Initializing config...")
config.early_init(args)
global qApp global qApp
qApp = Application(args) qApp = Application(args)
qApp.setOrganizationName("qutebrowser") qApp.setOrganizationName("qutebrowser")
@ -210,13 +216,12 @@ def _load_session(name):
Args: Args:
name: The name of the session to load, or None to read state file. name: The name of the session to load, or None to read state file.
""" """
state_config = objreg.get('state-config')
session_manager = objreg.get('session-manager') session_manager = objreg.get('session-manager')
if name is None and session_manager.exists('_autosave'): if name is None and session_manager.exists('_autosave'):
name = '_autosave' name = '_autosave'
elif name is None: elif name is None:
try: try:
name = state_config['general']['session'] name = configfiles.state['general']['session']
except KeyError: except KeyError:
# No session given as argument and none in the session file -> # No session given as argument and none in the session file ->
# start without loading a session # start without loading a session
@ -229,7 +234,7 @@ def _load_session(name):
except sessions.SessionError as e: except sessions.SessionError as e:
message.error("Failed to load session {}: {}".format(name, e)) message.error("Failed to load session {}: {}".format(name, e))
try: try:
del state_config['general']['session'] del configfiles.state['general']['session']
except KeyError: except KeyError:
pass pass
# If this was a _restart session, delete it. # If this was a _restart session, delete it.
@ -323,22 +328,10 @@ def _open_special_pages(args):
# With --basedir given, don't open anything. # With --basedir given, don't open anything.
return return
state_config = objreg.get('state-config') general_sect = configfiles.state['general']
general_sect = state_config['general']
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window='last-focused') window='last-focused')
# Legacy QtWebKit warning
needs_warning = (objects.backend == usertypes.Backend.QtWebKit and
not qtutils.is_qtwebkit_ng())
warning_shown = general_sect.get('backend-warning-shown') == '1'
if not warning_shown and needs_warning:
tabbed_browser.tabopen(QUrl('qute://backend-warning'),
background=False)
general_sect['backend-warning-shown'] = '1'
# Quickstart page # Quickstart page
quickstart_done = general_sect.get('quickstart-done') == '1' quickstart_done = general_sect.get('quickstart-done') == '1'
@ -360,13 +353,6 @@ def _open_special_pages(args):
general_sect['config-migration-shown'] = '1' general_sect['config-migration-shown'] = '1'
def _save_version():
"""Save the current version to the state config."""
state_config = objreg.get('state-config', None)
if state_config is not None:
state_config['general']['version'] = qutebrowser.__version__
def on_focus_changed(_old, new): def on_focus_changed(_old, new):
"""Register currently focused main window in the object registry.""" """Register currently focused main window in the object registry."""
if new is None: if new is None:
@ -407,7 +393,7 @@ def _init_modules(args, crash_handler):
log.init.debug("Initializing save manager...") log.init.debug("Initializing save manager...")
save_manager = savemanager.SaveManager(qApp) save_manager = savemanager.SaveManager(qApp)
objreg.register('save-manager', save_manager) objreg.register('save-manager', save_manager)
save_manager.add_saveable('version', _save_version) config.late_init(save_manager)
log.init.debug("Initializing network...") log.init.debug("Initializing network...")
networkmanager.init() networkmanager.init()
@ -419,10 +405,6 @@ def _init_modules(args, crash_handler):
readline_bridge = readline.ReadlineBridge() readline_bridge = readline.ReadlineBridge()
objreg.register('readline-bridge', readline_bridge) objreg.register('readline-bridge', readline_bridge)
log.init.debug("Initializing config...")
config.init(qApp)
save_manager.init_autosave()
log.init.debug("Initializing sql...") log.init.debug("Initializing sql...")
try: try:
sql.init(os.path.join(standarddir.data(), 'history.sqlite')) sql.init(os.path.join(standarddir.data(), 'history.sqlite'))
@ -780,7 +762,7 @@ class Application(QApplication):
""" """
self._last_focus_object = None self._last_focus_object = None
qt_args = qtutils.get_args(args) qt_args = config.qt_args(args)
log.init.debug("Qt arguments: {}, based on {}".format(qt_args, args)) log.init.debug("Qt arguments: {}, based on {}".format(qt_args, args))
super().__init__(qt_args) super().__init__(qt_args)

View File

@ -24,6 +24,7 @@ import sys
import os.path import os.path
import shlex import shlex
import functools import functools
import typing
from PyQt5.QtWidgets import QApplication, QTabBar, QDialog from PyQt5.QtWidgets import QApplication, QTabBar, QDialog
from PyQt5.QtCore import Qt, QUrl, QEvent, QUrlQuery from PyQt5.QtCore import Qt, QUrl, QEvent, QUrlQuery
@ -39,10 +40,11 @@ from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate,
webelem, downloads) webelem, downloads)
from qutebrowser.keyinput import modeman from qutebrowser.keyinput import modeman
from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils, from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
objreg, utils, typing, debug) objreg, utils, debug)
from qutebrowser.utils.usertypes import KeyMode from qutebrowser.utils.usertypes import KeyMode
from qutebrowser.misc import editor, guiprocess from qutebrowser.misc import editor, guiprocess
from qutebrowser.completion.models import urlmodel, miscmodels from qutebrowser.completion.models import urlmodel, miscmodels
from qutebrowser.mainwindow import mainwindow
class CommandDispatcher: class CommandDispatcher:
@ -70,7 +72,6 @@ class CommandDispatcher:
def _new_tabbed_browser(self, private): def _new_tabbed_browser(self, private):
"""Get a tabbed-browser from a new window.""" """Get a tabbed-browser from a new window."""
from qutebrowser.mainwindow import mainwindow
new_window = mainwindow.MainWindow(private=private) new_window = mainwindow.MainWindow(private=private)
new_window.show() new_window.show()
return new_window.tabbed_browser return new_window.tabbed_browser

View File

@ -24,7 +24,8 @@ import binascii
from PyQt5.QtWidgets import QWidget from PyQt5.QtWidgets import QWidget
from qutebrowser.utils import log, objreg, usertypes from qutebrowser.config import configfiles
from qutebrowser.utils import log, usertypes
from qutebrowser.misc import miscwidgets, objects from qutebrowser.misc import miscwidgets, objects
@ -67,9 +68,8 @@ class AbstractWebInspector(QWidget):
def _load_state_geometry(self): def _load_state_geometry(self):
"""Load the geometry from the state file.""" """Load the geometry from the state file."""
state_config = objreg.get('state-config')
try: try:
data = state_config['geometry']['inspector'] data = configfiles.state['geometry']['inspector']
geom = base64.b64decode(data, validate=True) geom = base64.b64decode(data, validate=True)
except KeyError: except KeyError:
# First start # First start
@ -84,10 +84,9 @@ class AbstractWebInspector(QWidget):
def closeEvent(self, e): def closeEvent(self, e):
"""Save the geometry when closed.""" """Save the geometry when closed."""
state_config = objreg.get('state-config')
data = bytes(self.saveGeometry()) data = bytes(self.saveGeometry())
geom = base64.b64encode(data).decode('ASCII') geom = base64.b64encode(data).decode('ASCII')
state_config['geometry']['inspector'] = geom configfiles.state['geometry']['inspector'] = geom
super().closeEvent(e) super().closeEvent(e)
def inspect(self, page): def inspect(self, page):

View File

@ -24,6 +24,7 @@ import posixpath
from qutebrowser.browser import webelem from qutebrowser.browser import webelem
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import objreg, urlutils, log, message, qtutils from qutebrowser.utils import objreg, urlutils, log, message, qtutils
from qutebrowser.mainwindow import mainwindow
class Error(Exception): class Error(Exception):
@ -134,7 +135,6 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False,
window=win_id) window=win_id)
if window: if window:
from qutebrowser.mainwindow import mainwindow
new_window = mainwindow.MainWindow( new_window = mainwindow.MainWindow(
private=cur_tabbed_browser.private) private=cur_tabbed_browser.private)
new_window.show() new_window.show()

View File

@ -28,7 +28,6 @@ import json
import os import os
import time import time
import urllib.parse import urllib.parse
import datetime
import textwrap import textwrap
import pkg_resources import pkg_resources
@ -37,7 +36,7 @@ from PyQt5.QtCore import QUrlQuery, QUrl
import qutebrowser import qutebrowser
from qutebrowser.config import config, configdata, configexc, configdiff from qutebrowser.config import config, configdata, configexc, configdiff
from qutebrowser.utils import (version, utils, jinja, log, message, docutils, from qutebrowser.utils import (version, utils, jinja, log, message, docutils,
objreg, usertypes, qtutils) objreg)
from qutebrowser.misc import objects from qutebrowser.misc import objects
@ -224,50 +223,13 @@ def qute_history(url):
return 'text/html', json.dumps(history_data(start_time, offset)) return 'text/html', json.dumps(history_data(start_time, offset))
else: else:
if ( if not config.val.content.javascript.enabled:
config.val.content.javascript.enabled and return 'text/plain', b'JavaScript is required for qute://history'
(objects.backend == usertypes.Backend.QtWebEngine or return 'text/html', jinja.render(
qtutils.is_qtwebkit_ng()) 'history.html',
): title='History',
return 'text/html', jinja.render( gap_interval=config.val.history_gap_interval
'history.html', )
title='History',
gap_interval=config.val.history_gap_interval
)
else:
# Get current date from query parameter, if not given choose today.
curr_date = datetime.date.today()
try:
query_date = QUrlQuery(url).queryItemValue("date")
if query_date:
curr_date = datetime.datetime.strptime(query_date,
"%Y-%m-%d").date()
except ValueError:
log.misc.debug("Invalid date passed to qute:history: " +
query_date)
one_day = datetime.timedelta(days=1)
next_date = curr_date + one_day
prev_date = curr_date - one_day
# start_time is the last second of curr_date
start_time = time.mktime(next_date.timetuple()) - 1
history = [
(i["url"], i["title"],
datetime.datetime.fromtimestamp(i["time"]),
QUrl(i["url"]).host())
for i in history_data(start_time)
]
return 'text/html', jinja.render(
'history_nojs.html',
title='History',
history=history,
curr_date=curr_date,
next_date=next_date,
prev_date=prev_date,
today=datetime.date.today(),
)
@add_handler('javascript') @add_handler('javascript')
@ -345,7 +307,7 @@ def qute_log(url):
@add_handler('gpl') @add_handler('gpl')
def qute_gpl(_url): def qute_gpl(_url):
"""Handler for qute://gpl. Return HTML content as string.""" """Handler for qute://gpl. Return HTML content as string."""
return 'text/html', utils.read_file('html/COPYING.html') return 'text/html', utils.read_file('html/LICENSE.html')
@add_handler('help') @add_handler('help')

View File

@ -23,6 +23,7 @@ import html
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import usertypes, message, log, objreg, jinja from qutebrowser.utils import usertypes, message, log, objreg, jinja
from qutebrowser.mainwindow import mainwindow
class CallSuper(Exception): class CallSuper(Exception):
@ -234,7 +235,6 @@ 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)
from qutebrowser.mainwindow import mainwindow
window = mainwindow.MainWindow(private=tabbed_browser.private) window = mainwindow.MainWindow(private=tabbed_browser.private)
window.show() window.show()
win_id = window.win_id win_id = window.win_id

View File

@ -31,6 +31,7 @@ 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.utils import log, usertypes, utils, qtutils, objreg from qutebrowser.utils import log, usertypes, utils, qtutils, objreg
@ -372,7 +373,6 @@ 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:
from qutebrowser.mainwindow import mainwindow
window = mainwindow.MainWindow(private=tabbed_browser.private) window = mainwindow.MainWindow(private=tabbed_browser.private)
window.show() window.show()
window.tabbed_browser.tabopen(url) window.tabbed_browser.tabopen(url)

View File

@ -49,7 +49,6 @@ class DiskCache(QNetworkDiskCache):
if size is None: if size is None:
size = 1024 * 1024 * 50 # default from QNetworkDiskCachePrivate size = 1024 * 1024 * 50 # default from QNetworkDiskCachePrivate
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-59909 # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-59909
if (qtutils.version_check('5.7.1') and if not qtutils.version_check('5.9'): # pragma: no cover
not qtutils.version_check('5.9')): # pragma: no cover
size = 0 size = 0
self.setMaximumCacheSize(size) self.setMaximumCacheSize(size)

View File

@ -38,12 +38,7 @@ class CertificateErrorWrapper(usertypes.AbstractCertificateErrorWrapper):
string=str(self)) string=str(self))
def __hash__(self): def __hash__(self):
try: return hash(self._error)
# Qt >= 5.4
return hash(self._error)
except TypeError:
return hash((self._error.certificate().toDer(),
self._error.error()))
def __eq__(self, other): def __eq__(self, other):
return self._error == other._error # pylint: disable=protected-access return self._error == other._error # pylint: disable=protected-access

View File

@ -24,13 +24,12 @@ import collections
import netrc import netrc
import html import html
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication, from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QCoreApplication, QUrl,
QUrl, QByteArray) QByteArray)
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QSslSocket from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QSslSocket
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils, from qutebrowser.utils import message, log, usertypes, utils, objreg, urlutils
urlutils)
from qutebrowser.browser import shared from qutebrowser.browser import shared
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,
@ -88,15 +87,9 @@ def _is_secure_cipher(cipher):
def init(): def init():
"""Disable insecure SSL ciphers on old Qt versions.""" """Disable insecure SSL ciphers on old Qt versions."""
if qtutils.version_check('5.3.0'): default_ciphers = QSslSocket.defaultCiphers()
default_ciphers = QSslSocket.defaultCiphers() log.init.debug("Default Qt ciphers: {}".format(
log.init.debug("Default Qt ciphers: {}".format( ', '.join(c.name() for c in default_ciphers)))
', '.join(c.name() for c in default_ciphers)))
else:
# https://codereview.qt-project.org/#/c/75943/
default_ciphers = QSslSocket.supportedCiphers()
log.init.debug("Supported Qt ciphers: {}".format(
', '.join(c.name() for c in default_ciphers)))
good_ciphers = [] good_ciphers = []
bad_ciphers = [] bad_ciphers = []
@ -409,24 +402,11 @@ class NetworkManager(QNetworkAccessManager):
tab = objreg.get('tab', scope='tab', window=self._win_id, tab = objreg.get('tab', scope='tab', window=self._win_id,
tab=self._tab_id) tab=self._tab_id)
current_url = tab.url() current_url = tab.url()
except (KeyError, RuntimeError, TypeError): except (KeyError, RuntimeError):
# https://github.com/qutebrowser/qutebrowser/issues/889 # https://github.com/qutebrowser/qutebrowser/issues/889
# Catching RuntimeError and TypeError because we could be in # Catching RuntimeError because we could be in the middle of
# the middle of the webpage shutdown here. # the webpage shutdown here.
current_url = QUrl() current_url = QUrl()
self.set_referer(req, current_url) self.set_referer(req, current_url)
return super().createRequest(op, req, outgoing_data)
if PYQT_VERSION < 0x050301:
# WORKAROUND (remove this when we bump the requirements to 5.3.1)
#
# If we don't disable our message handler, we get a freeze if a
# warning is printed due to a PyQt bug, e.g. when clicking a
# currency on http://ch.mouser.com/localsites/
#
# See http://www.riverbankcomputing.com/pipermail/pyqt/2014-June/034420.html
with log.disable_qt_msghandler():
reply = super().createRequest(op, req, outgoing_data)
else:
reply = super().createRequest(op, req, outgoing_data)
return reply

View File

@ -25,13 +25,7 @@ from PyQt5.QtCore import QByteArray, QDataStream, QIODevice, QUrl
from qutebrowser.utils import qtutils from qutebrowser.utils import qtutils
def _encode_url(url): def _serialize_items(items, current_idx, stream):
"""Encode a QUrl suitable to pass to QWebHistory."""
data = bytes(QUrl.toPercentEncoding(url.toString(), b':/#?&+=@%*'))
return data.decode('ascii')
def _serialize_ng(items, current_idx, stream):
# {'currentItemIndex': 0, # {'currentItemIndex': 0,
# 'history': [{'children': [], # 'history': [{'children': [],
# 'documentSequenceNumber': 1485030525573123, # 'documentSequenceNumber': 1485030525573123,
@ -47,13 +41,13 @@ def _serialize_ng(items, current_idx, stream):
# 'urlString': 'about:blank'}]} # 'urlString': 'about:blank'}]}
data = {'currentItemIndex': current_idx, 'history': []} data = {'currentItemIndex': current_idx, 'history': []}
for item in items: for item in items:
data['history'].append(_serialize_item_ng(item)) data['history'].append(_serialize_item(item))
stream.writeInt(3) # history stream version stream.writeInt(3) # history stream version
stream.writeQVariantMap(data) stream.writeQVariantMap(data)
def _serialize_item_ng(item): def _serialize_item(item):
data = { data = {
'originalURLString': item.original_url.toString(QUrl.FullyEncoded), 'originalURLString': item.original_url.toString(QUrl.FullyEncoded),
'scrollPosition': {'x': 0, 'y': 0}, 'scrollPosition': {'x': 0, 'y': 0},
@ -68,82 +62,6 @@ def _serialize_item_ng(item):
return data return data
def _serialize_old(items, current_idx, stream):
### Source/WebKit/qt/Api/qwebhistory.cpp operator<<
stream.writeInt(2) # history stream version
stream.writeInt(len(items))
stream.writeInt(current_idx)
for i, item in enumerate(items):
_serialize_item_old(i, item, stream)
def _serialize_item_old(i, item, stream):
"""Serialize a single WebHistoryItem into a QDataStream.
Args:
i: The index of the current item.
item: The WebHistoryItem to write.
stream: The QDataStream to write to.
"""
### Source/WebCore/history/qt/HistoryItemQt.cpp restoreState
## urlString
stream.writeQString(_encode_url(item.url))
## title
stream.writeQString(item.title)
## originalURLString
stream.writeQString(_encode_url(item.original_url))
### Source/WebCore/history/HistoryItem.cpp decodeBackForwardTree
## backForwardTreeEncodingVersion
stream.writeUInt32(2)
## size (recursion stack)
stream.writeUInt64(0)
## node->m_documentSequenceNumber
# If two HistoryItems have the same document sequence number, then they
# refer to the same instance of a document. Traversing history from one
# such HistoryItem to another preserves the document.
stream.writeInt64(i + 1)
## size (node->m_documentState)
stream.writeUInt64(0)
## node->m_formContentType
# info used to repost form data
stream.writeQString(None)
## hasFormData
stream.writeBool(False)
## node->m_itemSequenceNumber
# If two HistoryItems have the same item sequence number, then they are
# clones of one another. Traversing history from one such HistoryItem to
# another is a no-op. HistoryItem clones are created for parent and
# sibling frames when only a subframe navigates.
stream.writeInt64(i + 1)
## node->m_referrer
stream.writeQString(None)
## node->m_scrollPoint (x)
try:
stream.writeInt32(item.user_data['scroll-pos'].x())
except (KeyError, TypeError):
stream.writeInt32(0)
## node->m_scrollPoint (y)
try:
stream.writeInt32(item.user_data['scroll-pos'].y())
except (KeyError, TypeError):
stream.writeInt32(0)
## node->m_pageScaleFactor
stream.writeFloat(1)
## hasStateObject
# Support for HTML5 History
stream.writeBool(False)
## node->m_target
stream.writeQString(None)
### Source/WebCore/history/qt/HistoryItemQt.cpp restoreState
## validUserData
# We could restore the user data here, but we prefer to use the
# QWebHistoryItem API for that.
stream.writeBool(False)
def serialize(items): def serialize(items):
"""Serialize a list of QWebHistoryItems to a data stream. """Serialize a list of QWebHistoryItems to a data stream.
@ -180,10 +98,7 @@ def serialize(items):
else: else:
current_idx = 0 current_idx = 0
if qtutils.is_qtwebkit_ng(): _serialize_items(items, current_idx, stream)
_serialize_ng(items, current_idx, stream)
else:
_serialize_old(items, current_idx, stream)
user_data += [item.user_data for item in items] user_data += [item.user_data for item in items]

View File

@ -292,9 +292,6 @@ class WebKitElement(webelem.AbstractWebElement):
elem = elem._parent() # pylint: disable=protected-access elem = elem._parent() # pylint: disable=protected-access
def _move_text_cursor(self): def _move_text_cursor(self):
if self is None:
# old PyQt versions call the slot after the element is deleted.
return
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()

View File

@ -33,7 +33,7 @@ from PyQt5.QtGui import QFont
from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWebKit import QWebSettings
from qutebrowser.config import config, websettings from qutebrowser.config import config, websettings
from qutebrowser.utils import standarddir, urlutils, qtutils from qutebrowser.utils import standarddir, urlutils
from qutebrowser.browser import shared from qutebrowser.browser import shared
@ -131,13 +131,6 @@ def init(_args):
QWebSettings.setOfflineStoragePath( QWebSettings.setOfflineStoragePath(
os.path.join(data_path, 'offline-storage')) os.path.join(data_path, 'offline-storage'))
if (config.val.content.private_browsing and
not qtutils.version_check('5.4.2')):
# WORKAROUND for https://codereview.qt-project.org/#/c/108936/
# Won't work when private browsing is not enabled globally, but that's
# the best we can do...
QWebSettings.setIconDatabasePath('')
websettings.init_mappings(MAPPINGS) websettings.init_mappings(MAPPINGS)
_set_user_stylesheet() _set_user_stylesheet()
config.instance.changed.connect(_update_settings) config.instance.changed.connect(_update_settings)

View File

@ -55,20 +55,14 @@ class WebKitPrinting(browsertab.AbstractPrinting):
"""QtWebKit implementations related to printing.""" """QtWebKit implementations related to printing."""
def _do_check(self):
if not qtutils.check_print_compat():
# WORKAROUND (remove this when we bump the requirements to 5.3.0)
raise browsertab.WebTabError(
"Printing on Qt < 5.3.0 on Windows is broken, please upgrade!")
def check_pdf_support(self): def check_pdf_support(self):
self._do_check() pass
def check_printer_support(self): def check_printer_support(self):
self._do_check() pass
def check_preview_support(self): def check_preview_support(self):
self._do_check() pass
def to_pdf(self, filename): def to_pdf(self, filename):
printer = QPrinter() printer = QPrinter()

View File

@ -22,7 +22,7 @@
import html import html
import functools import functools
from PyQt5.QtCore import pyqtSlot, pyqtSignal, PYQT_VERSION, Qt, QUrl, QPoint from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QUrl, QPoint
from PyQt5.QtGui import QDesktopServices from PyQt5.QtGui import QDesktopServices
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
from PyQt5.QtWidgets import QFileDialog from PyQt5.QtWidgets import QFileDialog
@ -33,8 +33,8 @@ from qutebrowser.config import config
from qutebrowser.browser import pdfjs, shared from qutebrowser.browser import pdfjs, shared
from qutebrowser.browser.webkit import http from qutebrowser.browser.webkit import http
from qutebrowser.browser.webkit.network import networkmanager from qutebrowser.browser.webkit.network import networkmanager
from qutebrowser.utils import (message, usertypes, log, jinja, qtutils, utils, from qutebrowser.utils import (message, usertypes, log, jinja, objreg, debug,
objreg, debug, urlutils) urlutils)
class BrowserPage(QWebPage): class BrowserPage(QWebPage):
@ -87,22 +87,16 @@ class BrowserPage(QWebPage):
self.restoreFrameStateRequested.connect( self.restoreFrameStateRequested.connect(
self.on_restore_frame_state_requested) self.on_restore_frame_state_requested)
if PYQT_VERSION > 0x050300: def javaScriptPrompt(self, frame, js_msg, default):
# WORKAROUND (remove this when we bump the requirements to 5.3.1) """Override javaScriptPrompt to use qutebrowser prompts."""
# We can't override javaScriptPrompt with older PyQt-versions because if self._is_shutting_down:
# of a bug in PyQt. return (False, "")
# See http://www.riverbankcomputing.com/pipermail/pyqt/2014-June/034385.html try:
return shared.javascript_prompt(frame.url(), js_msg, default,
def javaScriptPrompt(self, frame, js_msg, default): abort_on=[self.loadStarted,
"""Override javaScriptPrompt to use qutebrowser prompts.""" self.shutting_down])
if self._is_shutting_down: except shared.CallSuper:
return (False, "") return super().javaScriptPrompt(frame, js_msg, default)
try:
return shared.javascript_prompt(frame.url(), js_msg, default,
abort_on=[self.loadStarted,
self.shutting_down])
except shared.CallSuper:
return super().javaScriptPrompt(frame, js_msg, default)
def _handle_errorpage(self, info, errpage): def _handle_errorpage(self, info, errpage):
"""Display an error page if needed. """Display an error page if needed.
@ -225,10 +219,6 @@ class BrowserPage(QWebPage):
def on_print_requested(self, frame): def on_print_requested(self, frame):
"""Handle printing when requested via javascript.""" """Handle printing when requested via javascript."""
if not qtutils.check_print_compat():
message.error("Printing on Qt < 5.3.0 on Windows is broken, "
"please upgrade!")
return
printdiag = QPrintDialog() printdiag = QPrintDialog()
printdiag.setAttribute(Qt.WA_DeleteOnClose) printdiag.setAttribute(Qt.WA_DeleteOnClose)
printdiag.open(lambda: frame.print(printdiag.printer())) printdiag.open(lambda: frame.print(printdiag.printer()))
@ -350,15 +340,7 @@ class BrowserPage(QWebPage):
frame: The QWebFrame which gets saved. frame: The QWebFrame which gets saved.
item: The QWebHistoryItem to be saved. item: The QWebHistoryItem to be saved.
""" """
try: if frame != self.mainFrame():
if frame != self.mainFrame():
return
except RuntimeError:
# With Qt 5.2.1 (Ubuntu Trusty) we get this when closing a tab:
# RuntimeError: wrapped C/C++ object of type BrowserPage has
# been deleted
# Since the information here isn't that important for closing web
# views anyways, we ignore this error.
return return
data = { data = {
'zoom': frame.zoomFactor(), 'zoom': frame.zoomFactor(),
@ -401,9 +383,6 @@ class BrowserPage(QWebPage):
""" """
return ext in self._extension_handlers return ext in self._extension_handlers
# WORKAROUND for:
# http://www.riverbankcomputing.com/pipermail/pyqt/2014-August/034722.html
@utils.prevent_exceptions(False, PYQT_VERSION < 0x50302)
def extension(self, ext, opt, out): def extension(self, ext, opt, out):
"""Override QWebPage::extension to provide error pages. """Override QWebPage::extension to provide error pages.

View File

@ -29,7 +29,7 @@ from PyQt5.QtWebKitWidgets import QWebView, QWebPage
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.keyinput import modeman from qutebrowser.keyinput import modeman
from qutebrowser.utils import log, usertypes, utils, qtutils, objreg, debug from qutebrowser.utils import log, usertypes, utils, objreg, debug
from qutebrowser.browser.webkit import webpage from qutebrowser.browser.webkit import webpage
@ -57,7 +57,7 @@ class WebView(QWebView):
def __init__(self, *, win_id, tab_id, tab, private, parent=None): def __init__(self, *, win_id, tab_id, tab, private, parent=None):
super().__init__(parent) super().__init__(parent)
if sys.platform == 'darwin' and qtutils.version_check('5.4'): if sys.platform == 'darwin':
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-42948 # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-42948
# See https://github.com/qutebrowser/qutebrowser/issues/462 # See https://github.com/qutebrowser/qutebrowser/issues/462
self.setStyle(QStyleFactory.create('Fusion')) self.setStyle(QStyleFactory.create('Fusion'))
@ -74,13 +74,9 @@ class WebView(QWebView):
page = webpage.BrowserPage(win_id=self.win_id, tab_id=self._tab_id, page = webpage.BrowserPage(win_id=self.win_id, tab_id=self._tab_id,
tabdata=tab.data, private=private, tabdata=tab.data, private=private,
parent=self) parent=self)
page.setVisibilityState(
try: QWebPage.VisibilityStateVisible if self.isVisible()
page.setVisibilityState( else QWebPage.VisibilityStateHidden)
QWebPage.VisibilityStateVisible if self.isVisible()
else QWebPage.VisibilityStateHidden)
except AttributeError:
pass
self.setPage(page) self.setPage(page)
@ -240,12 +236,8 @@ class WebView(QWebView):
Return: Return:
The superclass event return value. The superclass event return value.
""" """
try:
self.page().setVisibilityState(QWebPage.VisibilityStateVisible)
except AttributeError:
pass
super().showEvent(e) super().showEvent(e)
self.page().setVisibilityState(QWebPage.VisibilityStateVisible)
def hideEvent(self, e): def hideEvent(self, e):
"""Extend hideEvent to set the page visibility state to hidden. """Extend hideEvent to set the page visibility state to hidden.
@ -256,12 +248,8 @@ class WebView(QWebView):
Return: Return:
The superclass event return value. The superclass event return value.
""" """
try:
self.page().setVisibilityState(QWebPage.VisibilityStateHidden)
except AttributeError:
pass
super().hideEvent(e) super().hideEvent(e)
self.page().setVisibilityState(QWebPage.VisibilityStateHidden)
def mousePressEvent(self, e): def mousePressEvent(self, e):
"""Set the tabdata ClickTarget on a mousepress. """Set the tabdata ClickTarget on a mousepress.

View File

@ -22,10 +22,11 @@
import inspect import inspect
import collections import collections
import traceback import traceback
import typing
from qutebrowser.commands import cmdexc, argparser from qutebrowser.commands import cmdexc, argparser
from qutebrowser.utils import (log, utils, message, docutils, objreg, from qutebrowser.utils import (log, utils, message, docutils, objreg,
usertypes, typing) usertypes)
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
@ -415,10 +416,7 @@ class Command:
# We also can't use isinstance here because typing.Union doesn't # We also can't use isinstance here because typing.Union doesn't
# support that. # support that.
# pylint: disable=no-member,useless-suppression # pylint: disable=no-member,useless-suppression
try: types = list(typ.__args__)
types = list(typ.__union_params__)
except AttributeError:
types = list(typ.__args__)
# pylint: enable=no-member,useless-suppression # pylint: enable=no-member,useless-suppression
if param.default is not inspect.Parameter.empty: if param.default is not inspect.Parameter.empty:
types.append(type(param.default)) types.append(type(param.default))

View File

@ -19,6 +19,7 @@
"""Configuration storage and config-related utilities.""" """Configuration storage and config-related utilities."""
import sys
import copy import copy
import contextlib import contextlib
import functools import functools
@ -27,9 +28,10 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
from qutebrowser.config import configdata, configexc, configtypes, configfiles from qutebrowser.config import configdata, configexc, configtypes, configfiles
from qutebrowser.utils import utils, objreg, message, log, usertypes from qutebrowser.utils import (utils, objreg, message, log, usertypes, jinja,
from qutebrowser.misc import objects, msgbox qtutils)
from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.misc import objects, msgbox, earlyinit
from qutebrowser.commands import cmdexc, cmdutils, runners
from qutebrowser.completion.models import configmodel from qutebrowser.completion.models import configmodel
# 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
@ -39,6 +41,8 @@ key_instance = 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 = []
# Errors which happened during init, so we can show a message box.
_init_errors = []
class change_filter: # pylint: disable=invalid-name class change_filter: # pylint: disable=invalid-name
@ -173,8 +177,6 @@ class KeyConfig:
def bind(self, key, command, *, mode, force=False, save_yaml=False): def bind(self, key, command, *, mode, force=False, save_yaml=False):
"""Add a new binding from key to command.""" """Add a new binding from key to command."""
# Doing this here to work around a Python 3.4 circular import
from qutebrowser.commands import runners
key = self._prepare(key, mode) key = self._prepare(key, mode)
parser = runners.CommandParser() parser = runners.CommandParser()
@ -382,9 +384,17 @@ class Config(QObject):
self._mutables = {} self._mutables = {}
self._yaml = yaml_config self._yaml = yaml_config
def init_save_manager(self, save_manager):
"""Make sure the config gets saved properly.
We do this outside of __init__ because the config gets created before
the save_manager exists.
"""
self._yaml.init_save_manager(save_manager)
def _set_value(self, opt, value): def _set_value(self, opt, value):
"""Set the given option to the given value.""" """Set the given option to the given value."""
if objects.backend is not None: if not isinstance(objects.backend, objects.NoBackend):
if objects.backend not in opt.backends: if objects.backend not in opt.backends:
raise configexc.BackendError(objects.backend) raise configexc.BackendError(objects.backend)
@ -588,8 +598,6 @@ def set_register_stylesheet(obj, *, stylesheet=None, update=True):
@functools.lru_cache() @functools.lru_cache()
def _render_stylesheet(stylesheet): def _render_stylesheet(stylesheet):
"""Render the given stylesheet jinja template.""" """Render the given stylesheet jinja template."""
# Imported here to avoid a Python 3.4 circular import
from qutebrowser.utils import jinja
with jinja.environment.no_autoescape(): with jinja.environment.no_autoescape():
template = jinja.environment.from_string(stylesheet) template = jinja.environment.from_string(stylesheet)
return template.render(conf=val) return template.render(conf=val)
@ -639,18 +647,14 @@ class StyleSheetObserver(QObject):
instance.changed.connect(self._update_stylesheet) instance.changed.connect(self._update_stylesheet)
def init(parent=None): def early_init(args):
"""Initialize the config. """Initialize the part of the config which works without a QApplication."""
Args:
parent: The parent to pass to QObjects which get initialized.
"""
configdata.init() configdata.init()
yaml_config = configfiles.YamlConfig() yaml_config = configfiles.YamlConfig()
global val, instance, key_instance global val, instance, key_instance
instance = Config(yaml_config=yaml_config, parent=parent) instance = Config(yaml_config=yaml_config)
val = ConfigContainer(instance) val = ConfigContainer(instance)
key_instance = KeyConfig(instance) key_instance = KeyConfig(instance)
@ -671,12 +675,7 @@ def init(parent=None):
raise configexc.ConfigFileErrors('config.py', config_api.errors) raise configexc.ConfigFileErrors('config.py', config_api.errors)
except configexc.ConfigFileErrors as e: except configexc.ConfigFileErrors as e:
log.config.exception("Error while loading config.py") log.config.exception("Error while loading config.py")
errbox = msgbox.msgbox(parent=None, _init_errors.append(e)
title="Error while reading config",
text=e.to_html(),
icon=QMessageBox.Warning,
plain_text=False)
errbox.exec_()
try: try:
if getattr(config_api, 'load_autoconfig', True): if getattr(config_api, 'load_autoconfig', True):
@ -688,12 +687,72 @@ def init(parent=None):
desc = configexc.ConfigErrorDesc("Error", e) desc = configexc.ConfigErrorDesc("Error", e)
raise configexc.ConfigFileErrors('autoconfig.yml', [desc]) raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
except configexc.ConfigFileErrors as e: except configexc.ConfigFileErrors as e:
log.config.exception("Error while loading autoconfig.yml") log.config.exception("Error while loading config.py")
_init_errors.append(e)
configfiles.init()
objects.backend = get_backend(args)
earlyinit.init_with_backend(objects.backend)
def get_backend(args):
"""Find out what backend to use based on available libraries."""
try:
import PyQt5.QtWebKit # pylint: disable=unused-variable
except ImportError:
webkit_available = False
else:
webkit_available = qtutils.is_new_qtwebkit()
str_to_backend = {
'webkit': usertypes.Backend.QtWebKit,
'webengine': usertypes.Backend.QtWebEngine,
}
if args.backend is not None:
return str_to_backend[args.backend]
elif val.backend != 'auto':
return str_to_backend[val.backend]
elif webkit_available:
return usertypes.Backend.QtWebKit
else:
return usertypes.Backend.QtWebEngine
def late_init(save_manager):
"""Initialize the rest of the config after the QApplication is created."""
global _init_errors
for err in _init_errors:
errbox = msgbox.msgbox(parent=None, errbox = msgbox.msgbox(parent=None,
title="Error while reading config", title="Error while reading config",
text=e.to_html(), text=err.to_html(),
icon=QMessageBox.Warning, icon=QMessageBox.Warning,
plain_text=False) plain_text=False)
errbox.exec_() errbox.exec_()
_init_errors = []
configfiles.init() instance.init_save_manager(save_manager)
configfiles.state.init_save_manager(save_manager)
def qt_args(namespace):
"""Get the Qt QApplication arguments based on an argparse namespace.
Args:
namespace: The argparse namespace.
Return:
The argv list to be passed to Qt.
"""
argv = [sys.argv[0]]
if namespace.qt_flag is not None:
argv += ['--' + flag[0] for flag in namespace.qt_flag]
if namespace.qt_arg is not None:
for name, value in namespace.qt_arg:
argv += ['--' + name, value]
argv += ['--' + arg for arg in val.qt_args]
return argv

View File

@ -15,6 +15,9 @@ aliases:
desc: >- desc: >-
Aliases for commands. Aliases for commands.
The keys of the given dictionary are the aliases, while the values are the
commands they map to.
confirm_quit: confirm_quit:
type: ConfirmQuit type: ConfirmQuit
default: [never] default: [never]
@ -85,6 +88,41 @@ session_default_name:
If this is set to null, the session which was last loaded is saved. If this is set to null, the session which was last loaded is saved.
qt_args:
type:
name: List
valtype: String
none_ok: true
default: []
desc: >-
Additional arguments to pass to Qt, without leading `--`.
With QtWebEngine, some Chromium arguments (see
https://peter.sh/experiments/chromium-command-line-switches/ for a list)
will work.
backend:
type:
name: String
valid_values:
- auto: Automatically select either QtWebEngine or QtWebKit
- webkit: Force QtWebKit
- webengine: Force QtWebEngine
default: auto
desc: >-
The backend to use to display websites.
qutebrowser supports two different web rendering engines / backends,
QtWebKit and QtWebEngine.
QtWebKit is based on WebKit (similar to Safari). It was discontinued by the
Qt project with Qt 5.6, but picked up as a well maintained fork:
https://github.com/annulen/webkit/wiki - qutebrowser only supports the fork.
QtWebEngine is Qt's official successor to QtWebKit and based on the Chromium
project. It's slightly more resource hungry that QtWebKit and has a couple
of missing features in qutebrowser, but is generally the preferred choice.
## auto_save ## auto_save
auto_save.interval: auto_save.interval:
@ -402,6 +440,10 @@ content.javascript.log:
desc: >- desc: >-
Log levels to use for JavaScript console logging messages. Log levels to use for JavaScript console logging messages.
When a JavaScript message with the level given in the dictionary key is
logged, the corresponding dictionary value selects the qutebrowser logger to
use.
On QtWebKit, the "unknown" setting is always used. On QtWebKit, the "unknown" setting is always used.
content.javascript.modal_dialog: content.javascript.modal_dialog:
@ -1183,11 +1225,14 @@ url.searchengines:
desc: >- desc: >-
Definitions of search engines which can be used via the address bar. Definitions of search engines which can be used via the address bar.
Maps a searchengine name (such as `DEFAULT`, or `ddg`) to a URL with a `{}`
placeholder. The placeholder will be replaced by the search term, use `{{`
and `}}` for literal `{`/`}` signs.
The searchengine named `DEFAULT` is used when `url.auto_search` is turned on The searchengine named `DEFAULT` is used when `url.auto_search` is turned on
and something else than a URL was entered to be opened. Other search engines and something else than a URL was entered to be opened. Other search engines
can be used by prepending the search engine name to the search term, e.g. can be used by prepending the search engine name to the search term, e.g.
`:open google qutebrowser`. The string `{}` will be replaced by the search `:open google qutebrowser`.
term, use `{{` and `}}` for literal `{`/`}` signs.
url.start_pages: url.start_pages:
type: type:
@ -1713,7 +1758,7 @@ fonts.downloads:
desc: Font used for the downloadbar. desc: Font used for the downloadbar.
fonts.hints: fonts.hints:
default: bold 13px monospace default: bold 10pt monospace
type: Font type: Font
desc: Font used for the hints. desc: Font used for the hints.
@ -2122,7 +2167,7 @@ bindings.default:
want to load no default keybindings at all. want to load no default keybindings at all.
If you want to preserve default bindings (and get new bindings when there is If you want to preserve default bindings (and get new bindings when there is
an update), add new bindings to `bindings.commands` (or use `:bind`) and an update), use `config.bind()` in `config.py` or the `:bind` command, and
leave this setting alone. leave this setting alone.
bindings.commands: bindings.commands:
@ -2148,64 +2193,4 @@ bindings.commands:
`{mode: {key: command}}` `{mode: {key: command}}`
If you want to map a key to another key, check the `bindings.key_mappings` While it's possible to bind keys by modifying this setting, it's recommended
setting instead.
For special keys (can't be part of a keychain), enclose them in `<`...`>`.
For modifiers, you can use either `-` or `+` as delimiters, and these names:
* Control: `Control`, `Ctrl`
* Meta: `Meta`, `Windows`, `Mod4`
* Alt: `Alt`, `Mod1`
* Shift: `Shift`
For simple keys (no `<>`-signs), a capital letter means the key is pressed
with Shift. For special keys (with `<>`-signs), you need to explicitly add
`Shift-` to match a key pressed with shift.
If you want a binding to do nothing, bind it to the `nop` command.
If you want a default binding to be passed through to the website, bind it
to null.
Note that some commands which are only useful for bindings (but not used
interactively) are hidden from the command completion. See `:help` for a
full list of available commands.
The following modes are available:
* normal: The default mode, where most commands are invoked.
* insert: Entered when an input field is focused on a website, or by
pressing `i` in normal mode. Passes through almost all keypresses to the
website, but has some bindings like `<Ctrl-e>` to open an external editor.
Note that single keys can't be bound in this mode.
* hint: Entered when `f` is pressed to select links with the keyboard. Note
that single keys can't be bound in this mode.
* passthrough: Similar to insert mode, but passes through all keypresses
except `<Escape>` to leave the mode. It might be useful to bind `<Escape>`
to some other key in this mode if you want to be able to send an Escape
key to the website as well. Note that single keys can't be bound in this
mode.
* command: Entered when pressing the `:` key in order to enter a command.
Note that single keys can't be bound in this mode.
* prompt: Entered when there's a prompt to display, like for download
locations or when invoked from JavaScript.
+
You can bind normal keys in this mode, but they will be only active when a
yes/no-prompt is asked. For other prompt modes, you can only bind special
keys.
* caret: Entered when pressing the `v` mode, used to select text using the
keyboard.
* register: Entered when qutebrowser is waiting for a register name/key for
commands like `:set-mark`.

View File

@ -19,7 +19,7 @@
"""Exceptions related to config parsing.""" """Exceptions related to config parsing."""
from qutebrowser.utils import utils from qutebrowser.utils import utils, jinja
class Error(Exception): class Error(Exception):
@ -108,7 +108,6 @@ class ConfigFileErrors(Error):
def to_html(self): def to_html(self):
"""Get the error texts as a HTML snippet.""" """Get the error texts as a HTML snippet."""
from qutebrowser.utils import jinja
template = jinja.environment.from_string(""" template = jinja.environment.from_string("""
Errors occurred while reading {{ basename }}: Errors occurred while reading {{ basename }}:

View File

@ -29,8 +29,13 @@ import contextlib
import yaml import yaml
from PyQt5.QtCore import QSettings from PyQt5.QtCore import QSettings
from qutebrowser.config import configexc import qutebrowser
from qutebrowser.utils import objreg, standarddir, utils, qtutils from qutebrowser.config import configexc, config
from qutebrowser.utils import standarddir, utils, qtutils
# The StateConfig instance
state = None
class StateConfig(configparser.ConfigParser): class StateConfig(configparser.ConfigParser):
@ -39,7 +44,6 @@ class StateConfig(configparser.ConfigParser):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
save_manager = objreg.get('save-manager')
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')
for sect in ['general', 'geometry']: for sect in ['general', 'geometry']:
@ -47,8 +51,17 @@ class StateConfig(configparser.ConfigParser):
self.add_section(sect) self.add_section(sect)
except configparser.DuplicateSectionError: except configparser.DuplicateSectionError:
pass pass
# See commit a98060e020a4ba83b663813a4b9404edb47f28ad.
self['general'].pop('fooled', None) deleted_keys = ['fooled', 'backend-warning-shown']
for key in deleted_keys:
self['general'].pop(key, None)
def init_save_manager(self, save_manager):
"""Make sure the config gets saved properly.
We do this outside of __init__ because the config gets created before
the save_manager exists.
"""
save_manager.add_saveable('state-config', self._save) save_manager.add_saveable('state-config', self._save)
def _save(self): def _save(self):
@ -68,12 +81,18 @@ class YamlConfig:
VERSION = 1 VERSION = 1
def __init__(self): def __init__(self):
save_manager = objreg.get('save-manager')
self._filename = os.path.join(standarddir.config(auto=True), self._filename = os.path.join(standarddir.config(auto=True),
'autoconfig.yml') 'autoconfig.yml')
save_manager.add_saveable('yaml-config', self._save)
self.values = {} self.values = {}
def init_save_manager(self, save_manager):
"""Make sure the config gets saved properly.
We do this outside of __init__ because the config gets created before
the save_manager exists.
"""
save_manager.add_saveable('yaml-config', self._save)
def _save(self): def _save(self):
"""Save the changed settings to the YAML file.""" """Save the changed settings to the YAML file."""
data = {'config_version': self.VERSION, 'global': self.values} data = {'config_version': self.VERSION, 'global': self.values}
@ -135,8 +154,8 @@ class ConfigAPI:
errors: Errors which occurred while setting options. errors: Errors which occurred while setting options.
""" """
def __init__(self, config, keyconfig): def __init__(self, conf, keyconfig):
self._config = config self._config = conf
self._keyconfig = keyconfig self._keyconfig = keyconfig
self.load_autoconfig = True self.load_autoconfig = True
self.errors = [] self.errors = []
@ -161,18 +180,17 @@ class ConfigAPI:
with self._handle_error('setting', name): with self._handle_error('setting', name):
self._config.set_obj(name, value) self._config.set_obj(name, value)
def bind(self, key, command, *, mode, force=False): def bind(self, key, command, mode='normal', *, force=False):
with self._handle_error('binding', key): with self._handle_error('binding', key):
self._keyconfig.bind(key, command, mode=mode, force=force) self._keyconfig.bind(key, command, mode=mode, force=force)
def unbind(self, key, *, mode): def unbind(self, key, mode='normal'):
with self._handle_error('unbinding', key): with self._handle_error('unbinding', key):
self._keyconfig.unbind(key, mode=mode) self._keyconfig.unbind(key, mode=mode)
def read_config_py(filename=None): def read_config_py(filename=None):
"""Read a config.py file.""" """Read a config.py file."""
from qutebrowser.config import config
api = ConfigAPI(config.instance, config.key_instance) api = ConfigAPI(config.instance, config.key_instance)
if filename is None: if filename is None:
@ -220,8 +238,9 @@ def read_config_py(filename=None):
def init(): def init():
"""Initialize config storage not related to the main config.""" """Initialize config storage not related to the main config."""
global state
state = StateConfig() state = StateConfig()
objreg.register('state-config', state) state['general']['version'] = qutebrowser.__version__
# Set the QSettings path to something like # Set the QSettings path to something like
# ~/.config/qutebrowser/qsettings/qutebrowser/qutebrowser.conf so it # ~/.config/qutebrowser/qsettings/qutebrowser/qutebrowser.conf so it

View File

@ -59,9 +59,9 @@ from PyQt5.QtCore import QUrl, Qt
from PyQt5.QtGui import QColor, QFont from PyQt5.QtGui import QColor, QFont
from PyQt5.QtWidgets import QTabWidget, QTabBar from PyQt5.QtWidgets import QTabWidget, QTabBar
from qutebrowser.commands import cmdutils from qutebrowser.commands import cmdutils, runners, cmdexc
from qutebrowser.config import configexc from qutebrowser.config import configexc
from qutebrowser.utils import standarddir, utils, qtutils from qutebrowser.utils import standarddir, utils, qtutils, urlutils
SYSTEM_PROXY = object() # Return value for Proxy type SYSTEM_PROXY = object() # Return value for Proxy type
@ -791,7 +791,6 @@ class Command(BaseType):
if not Command.unvalidated: if not Command.unvalidated:
Command.unvalidated = True Command.unvalidated = True
try: try:
from qutebrowser.commands import runners, cmdexc
parser = runners.CommandParser() parser = runners.CommandParser()
try: try:
parser.parse_all(value) parser.parse_all(value)
@ -1287,7 +1286,6 @@ class Proxy(BaseType):
('none', "Don't use any proxy")) ('none', "Don't use any proxy"))
def to_py(self, value): def to_py(self, value):
from qutebrowser.utils import urlutils
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if not value:
return None return None
@ -1352,7 +1350,6 @@ class FuzzyUrl(BaseType):
"""A URL which gets interpreted as search if needed.""" """A URL which gets interpreted as search if needed."""
def to_py(self, value): def to_py(self, value):
from qutebrowser.utils import urlutils
self._basic_py_validation(value, str) self._basic_py_validation(value, str)
if not value: if not value:
return None return None

View File

@ -1,100 +0,0 @@
{% extends "styled.html" %}
{% block style %}
{{super()}}
.note {
font-size: smaller;
color: grey;
}
.mono {
font-family: monospace;
}
{% endblock %}
{% block content %}
<h1>Legacy QtWebKit backend</h1>
<span class="note">Note this warning will only appear once. Use <span class="mono">:open
qute://backend-warning</span> to show it again at a later time.</span>
<p>
You're using qutebrowser with the legacy QtWebKit backend. It's still the
default until a few remaining issues are sorted out. If you can, it's
strongly suggested to switch earlier, as legacy QtWebKit has known security
issues and also breaks things on various websites.
</p>
<h2>Using QtWebEngine instead</h2>
<span class="note">This is usually the better choice if you aren't using Nouveau graphics, and
don't need any features which are currently unavailable with QtWebEngine (like
the <span class="mono">qute://settings</span> page or caret browsing).</span>
{% macro install_webengine(package) -%}
You should be able to install <span class="mono">{{ package }}</span> and start qutebrowser with <span class="mono">--backend webengine</span> to use the new backend.
{%- endmacro %}
{% macro please_open_issue() -%}
If you know more, please <a href="https://github.com/qutebrowser/qutebrowser/issues/new">open an issue</a>!
{%- endmacro %}
{% macro unknown_system() -%}
There's no information available for your system. {{ please_open_issue() }}
{%- endmacro %}
<p>
{% if distribution.parsed == Distribution.ubuntu %}
{% if distribution.version == none %}
{{ unknown_system() }}
{% elif distribution.version >= version('17.04') %}
{{ install_webengine('python3-pyqt5.qtwebengine') }}
{% elif distribution.version >= version('16.04') %}
QtWebEngine is only available in Ubuntu's repositories since 17.04, but you can <a href="https://github.com/qutebrowser/qutebrowser/blob/master/INSTALL.asciidoc#installing-qutebrowser-with-tox">install qutebrowser via tox</a> with <span class="mono">tox -e mkvenv-pypi</span> to use the new backend.
{% else %}
Unfortunately, no easy way is known to install QtWebEngine on Ubuntu &lt; 16.04. {{ please_open_issue() }}
{% endif %}
{% elif distribution.parsed == Distribution.debian %}
{% if distribution.version == none %}
{{ unknown_system() }}
{% elif distribution.version >= version('9') %}
{{ install_webengine('python3-pyqt5.qtwebengine') }}
{% else %}
Unfortunately, no easy way is known to install QtWebEngine on Debian &lt; 9. {{ please_open_issue() }}
{% endif %}
{% elif distribution.parsed in [Distribution.arch, Distribution.manjaro] %}
{{ install_webengine('qt5-webengine') }}
{% elif distribution.parsed == Distribution.void %}
{{ install_webengine('python-PyQt5-webengine') }}
{% elif distribution.parsed == Distribution.fedora %}
{{ install_webengine('qt5-qtwebengine') }}
{% elif distribution.parsed == Distribution.opensuse %}
{{ install_webengine('libqt5-qtwebengine') }}
{% elif distribution.parsed == Distribution.gentoo %}
{{ install_webengine('dev-qt/qtwebengine') }}
{% else %}
{{ unknown_system() }}
{% endif %}
</p>
<h2>Using QtWebKit-NG instead</h2>
<span class="note">This is a drop-in replacement for legacy QtWebKit.</span>
<p>
{% if distribution.parsed == Distribution.debian and distribution.version != none and distribution.version >= version('9') %}
There are unofficial QtWebKit-NG packages <a href="http://repo.paretje.be/unstable/">available</a>.
{% elif distribution.parsed in [Distribution.ubuntu, Distribution.debian] %}
No easy way is known to install QtWebKit-NG on your system.
There are unofficial QtWebKit-NG packages <a href="http://repo.paretje.be/unstable/">available</a>, but they are intended for Debian Unstable.
{{ please_open_issue() }}
{% elif distribution.parsed in [Distribution.arch, Distribution.manjaro] %}
With an updated <span class="mono">qt5-webkit</span> package, you should already get QtWebKit-NG.
{% elif distribution.parsed == Distribution.gentoo %}
There's an unofficial <a href="https://gist.github.com/annulen/309569fb61e5d64a703c055c1e726f71">ebuild</a> available.
{% else %}
{{ unknown_system() }}
{% endif %}
</p>
{% endblock %}

View File

@ -1,61 +0,0 @@
{% extends "styled.html" %}
{% block style %}
{{super()}}
body {
max-width: 1440px;
}
td.title {
word-break: break-all;
}
td.time {
color: #555;
text-align: right;
white-space: nowrap;
}
table {
margin-bottom: 30px;
}
.date {
color: #555;
font-size: 12pt;
padding-bottom: 15px;
font-weight: bold;
text-align: left;
}
.pagination-link {
color: #555;
font-weight: bold;
margn-bottom: 15px;
text-decoration: none;
}
{% endblock %}
{% block content %}
<h1>Browsing history</h1>
<table>
<caption class="date">{{curr_date.strftime("%a, %d %B %Y")}}</caption>
<tbody>
{% for url, title, time, host in history %}
<tr>
<td class="title">
<a href="{{url}}">{{title}}</a>
<span class="hostname">{{host}}</span>
</td>
<td class="time">{{time.strftime("%X")}}</td>
</tr>
{% endfor %}
</tbody>
</table>
<span class="pagination-link"><a href="qute://history/?date={{prev_date.strftime("%Y-%m-%d")}}" rel="prev">Previous</a></span>
{% if today >= next_date %}
<span class="pagination-link"><a href="qute://history/?date={{next_date.strftime("%Y-%m-%d")}}" rel="next">Next</a></span>
{% endif %}
{% endblock %}

View File

@ -28,11 +28,10 @@ from PyQt5.QtCore import pyqtSlot, QRect, QPoint, QTimer, Qt
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QSizePolicy from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QSizePolicy
from qutebrowser.commands import runners, cmdutils from qutebrowser.commands import runners, cmdutils
from qutebrowser.config import config from qutebrowser.config import config, configfiles
from qutebrowser.utils import (message, log, usertypes, qtutils, objreg, utils, from qutebrowser.utils import (message, log, usertypes, qtutils, objreg, utils,
jinja, debug) jinja, debug)
from qutebrowser.mainwindow import tabbedbrowser, messageview, prompt from qutebrowser.mainwindow import messageview, prompt
from qutebrowser.mainwindow.statusbar import bar
from qutebrowser.completion import completionwidget, completer from qutebrowser.completion import completionwidget, completer
from qutebrowser.keyinput import modeman from qutebrowser.keyinput import modeman
from qutebrowser.browser import (commands, downloadview, hints, from qutebrowser.browser import (commands, downloadview, hints,
@ -140,6 +139,11 @@ class MainWindow(QWidget):
parent: The parent the window should get. parent: The parent the window should get.
""" """
super().__init__(parent) super().__init__(parent)
# Late import to avoid a circular dependency
# - browsertab -> hints -> webelem -> mainwindow -> bar -> browsertab
from qutebrowser.mainwindow import tabbedbrowser
from qutebrowser.mainwindow.statusbar import bar
self.setAttribute(Qt.WA_DeleteOnClose) self.setAttribute(Qt.WA_DeleteOnClose)
self._commandrunner = None self._commandrunner = None
self._overlays = [] self._overlays = []
@ -365,9 +369,8 @@ class MainWindow(QWidget):
def _load_state_geometry(self): def _load_state_geometry(self):
"""Load the geometry from the state file.""" """Load the geometry from the state file."""
state_config = objreg.get('state-config')
try: try:
data = state_config['geometry']['mainwindow'] data = configfiles.state['geometry']['mainwindow']
geom = base64.b64decode(data, validate=True) geom = base64.b64decode(data, validate=True)
except KeyError: except KeyError:
# First start # First start
@ -380,10 +383,9 @@ class MainWindow(QWidget):
def _save_geometry(self): def _save_geometry(self):
"""Save the window geometry to the state config.""" """Save the window geometry to the state config."""
state_config = objreg.get('state-config')
data = bytes(self.saveGeometry()) data = bytes(self.saveGeometry())
geom = base64.b64encode(data).decode('ASCII') geom = base64.b64encode(data).decode('ASCII')
state_config['geometry']['mainwindow'] = geom configfiles.state['geometry']['mainwindow'] = geom
def _load_geometry(self, geom): def _load_geometry(self, geom):
"""Load geometry from a bytes object. """Load geometry from a bytes object.

View File

@ -61,10 +61,6 @@ class Progress(QProgressBar):
def on_tab_changed(self, tab): def on_tab_changed(self, tab):
"""Set the correct value when the current tab changed.""" """Set the correct value when the current tab changed."""
if self is None: # pragma: no branch
# This should never happen, but for some weird reason it does
# sometimes.
return # pragma: no cover
self.setValue(tab.progress()) self.setValue(tab.progress())
if tab.load_status() == usertypes.LoadStatus.loading: if tab.load_status() == usertypes.LoadStatus.loading:
self.show() self.show()

View File

@ -28,7 +28,7 @@ from PyQt5.QtGui import QIcon
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.keyinput import modeman from qutebrowser.keyinput import modeman
from qutebrowser.mainwindow import tabwidget from qutebrowser.mainwindow import tabwidget, mainwindow
from qutebrowser.browser import signalfilter, browsertab from qutebrowser.browser import signalfilter, browsertab
from qutebrowser.utils import (log, usertypes, utils, qtutils, objreg, from qutebrowser.utils import (log, usertypes, utils, qtutils, objreg,
urlutils, message, jinja) urlutils, message, jinja)
@ -432,7 +432,6 @@ class TabbedBrowser(tabwidget.TabWidget):
if (config.val.tabs.tabs_are_windows and self.count() > 0 and if (config.val.tabs.tabs_are_windows and self.count() > 0 and
not ignore_tabs_are_windows): not ignore_tabs_are_windows):
from qutebrowser.mainwindow import mainwindow
window = mainwindow.MainWindow(private=self.private) window = mainwindow.MainWindow(private=self.private)
window.show() window.show()
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',

View File

@ -70,9 +70,6 @@ class TabWidget(QTabWidget):
@config.change_filter('tabs') @config.change_filter('tabs')
def _init_config(self): def _init_config(self):
"""Initialize attributes based on the config.""" """Initialize attributes based on the config."""
if self is None: # pragma: no cover
# WORKAROUND for PyQt 5.2
return
tabbar = self.tabBar() tabbar = self.tabBar()
self.setMovable(True) self.setMovable(True)
self.setTabsClosable(False) self.setTabsClosable(False)
@ -558,7 +555,7 @@ class TabBar(QTabBar):
# pylint: enable=bad-config-option # pylint: enable=bad-config-option
if idx == selected: if idx == selected:
setting = setting.selected setting = setting.selected
setting = setting.odd if idx % 2 else setting.even setting = setting.odd if (idx + 1) % 2 else setting.even
tab.palette.setColor(QPalette.Window, setting.bg) tab.palette.setColor(QPalette.Window, setting.bg)
tab.palette.setColor(QPalette.WindowText, setting.fg) tab.palette.setColor(QPalette.WindowText, setting.fg)
@ -744,24 +741,17 @@ class TabBarStyle(QCommonStyle):
log.misc.warning("Could not get layouts for tab!") log.misc.warning("Could not get layouts for tab!")
return QRect() return QRect()
return layouts.text return layouts.text
elif sr == QStyle.SE_TabWidgetTabBar: elif sr in [QStyle.SE_TabWidgetTabBar,
QStyle.SE_TabBarScrollLeftButton]:
# Handling SE_TabBarScrollLeftButton so the left scroll button is
# aligned properly. Otherwise, empty space will be shown after the
# last tab even though the button width is set to 0
#
# Need to use super() because we also use super() to render # Need to use super() because we also use super() to render
# element in drawControl(); otherwise, we may get bit by # element in drawControl(); otherwise, we may get bit by
# style differences... # style differences...
rct = super().subElementRect(sr, opt, widget) return super().subElementRect(sr, opt, widget)
return rct
else: else:
try:
# We need this so the left scroll button is aligned properly.
# Otherwise, empty space will be shown after the last tab even
# though the button width is set to 0
#
# QStyle.SE_TabBarScrollLeftButton was added in Qt 5.7
if sr == QStyle.SE_TabBarScrollLeftButton:
return super().subElementRect(sr, opt, widget)
except AttributeError:
pass
return self._style.subElementRect(sr, opt, widget) return self._style.subElementRect(sr, opt, widget)
def _tab_layout(self, opt): def _tab_layout(self, opt):

View File

@ -43,12 +43,12 @@ except ImportError: # pragma: no cover
# to stderr. # to stderr.
def check_python_version(): def check_python_version():
"""Check if correct python version is run.""" """Check if correct python version is run."""
if sys.hexversion < 0x03040000: if sys.hexversion < 0x03050000:
# We don't use .format() and print_function here just in case someone # We don't use .format() and print_function here just in case someone
# still has < 2.6 installed. # still has < 2.6 installed.
# pylint: disable=bad-builtin # pylint: disable=bad-builtin
version_str = '.'.join(map(str, sys.version_info[:3])) version_str = '.'.join(map(str, sys.version_info[:3]))
text = ("At least Python 3.4 is required to run qutebrowser, but " + text = ("At least Python 3.5 is required to run qutebrowser, but " +
version_str + " is installed!\n") version_str + " is installed!\n")
if Tk and '--no-err-windows' not in sys.argv: # pragma: no cover if Tk and '--no-err-windows' not in sys.argv: # pragma: no cover
root = Tk() root = Tk()

View File

@ -38,7 +38,7 @@ import qutebrowser
from qutebrowser.utils import version, log, utils, objreg, usertypes from qutebrowser.utils import version, log, utils, objreg, usertypes
from qutebrowser.misc import (miscwidgets, autoupdate, msgbox, httpclient, from qutebrowser.misc import (miscwidgets, autoupdate, msgbox, httpclient,
pastebin, objects) pastebin, objects)
from qutebrowser.config import config from qutebrowser.config import config, configfiles
def parse_fatal_stacktrace(text): def parse_fatal_stacktrace(text):
@ -152,22 +152,7 @@ class _CrashDialog(QDialog):
self._pypi_client = autoupdate.PyPIVersionClient(self) self._pypi_client = autoupdate.PyPIVersionClient(self)
self._init_text() self._init_text()
contact = QLabel("I'd like to be able to follow up with you, to keep " self._init_contact_input()
"you posted on the status of this crash and get more "
"information if I need it - how can I contact you?",
wordWrap=True)
self._vbox.addWidget(contact)
self._contact = QTextEdit(tabChangesFocus=True, acceptRichText=False)
try:
state = objreg.get('state-config')
try:
self._contact.setPlainText(state['general']['contact-info'])
except KeyError:
self._contact.setPlaceholderText("Mail or IRC nickname")
except Exception:
log.misc.exception("Failed to get contact information!")
self._contact.setPlaceholderText("Mail or IRC nickname")
self._vbox.addWidget(self._contact, 2)
info = QLabel("What were you doing when this crash/bug happened?") info = QLabel("What were you doing when this crash/bug happened?")
self._vbox.addWidget(info) self._vbox.addWidget(info)
@ -201,6 +186,26 @@ class _CrashDialog(QDialog):
def __repr__(self): def __repr__(self):
return utils.get_repr(self) return utils.get_repr(self)
def _init_contact_input(self):
"""Initialize the widget asking for contact info."""
contact = QLabel("I'd like to be able to follow up with you, to keep "
"you posted on the status of this crash and get more "
"information if I need it - how can I contact you?",
wordWrap=True)
self._vbox.addWidget(contact)
self._contact = QTextEdit(tabChangesFocus=True, acceptRichText=False)
try:
try:
info = configfiles.state['general']['contact-info']
except KeyError:
self._contact.setPlaceholderText("Mail or IRC nickname")
else:
self._contact.setPlainText(info)
except Exception:
log.misc.exception("Failed to get contact information!")
self._contact.setPlaceholderText("Mail or IRC nickname")
self._vbox.addWidget(self._contact, 2)
def _init_text(self): def _init_text(self):
"""Initialize the main text to be displayed on an exception. """Initialize the main text to be displayed on an exception.
@ -296,8 +301,8 @@ class _CrashDialog(QDialog):
def _save_contact_info(self): def _save_contact_info(self):
"""Save the contact info to disk.""" """Save the contact info to disk."""
try: try:
state = objreg.get('state-config') info = self._contact.toPlainText()
state['general']['contact-info'] = self._contact.toPlainText() configfiles.state['general']['contact-info'] = info
except Exception: except Exception:
log.misc.exception("Failed to save contact information!") log.misc.exception("Failed to save contact information!")

View File

@ -19,7 +19,7 @@
"""Things which need to be done really early (e.g. before importing Qt). """Things which need to be done really early (e.g. before importing Qt).
At this point we can be sure we have all python 3.4 features available. At this point we can be sure we have all python 3.5 features available.
""" """
try: try:
@ -29,7 +29,6 @@ try:
except ImportError: except ImportError:
hunter = None hunter = None
import os
import sys import sys
import faulthandler import faulthandler
import traceback import traceback
@ -41,8 +40,6 @@ try:
except ImportError: except ImportError:
tkinter = None tkinter = None
import pkg_resources
# NOTE: No qutebrowser or PyQt import should be done here, as some early # NOTE: No qutebrowser or PyQt import should be done here, as some early
# initialization needs to take place before that! # initialization needs to take place before that!
@ -68,7 +65,7 @@ def _missing_str(name, *, windows=None, pip=None, webengine=False):
if not webengine: if not webengine:
lines = ['<b>If you installed a qutebrowser package for your ' lines = ['<b>If you installed a qutebrowser package for your '
'distribution, please report this as a bug.</b>'] 'distribution, please report this as a bug.</b>']
blocks.append('<br />'.join(lines)) blocks.append('<br />'.join(lines))
if windows is not None: if windows is not None:
lines = ["<b>On Windows:</b>"] lines = ["<b>On Windows:</b>"]
lines += windows.splitlines() lines += windows.splitlines()
@ -140,73 +137,6 @@ def init_faulthandler(fileobj=sys.__stderr__):
faulthandler.register(signal.SIGUSR1) faulthandler.register(signal.SIGUSR1)
def _qt_version():
"""Get the running Qt version.
Needs to be in a function so we can do a local import easily (to not import
from QtCore too early) but can patch this out easily for tests.
"""
from PyQt5.QtCore import qVersion
return pkg_resources.parse_version(qVersion())
def fix_harfbuzz(args):
"""Fix harfbuzz issues.
This switches to the most stable harfbuzz font rendering engine available
on the platform instead of using the system wide one.
This fixes crashes on various sites.
- On Qt 5.2 (and probably earlier) the new engine probably has more
crashes and is also experimental.
e.g. https://bugreports.qt.io/browse/QTBUG-36099
- On Qt 5.3.0 there's a bug that affects a lot of websites:
https://bugreports.qt.io/browse/QTBUG-39278
So the new engine will be more stable.
- On Qt 5.3.1 this bug is fixed and the old engine will be the more stable
one again.
- On Qt 5.4 the new engine is the default and most bugs are taken care of.
IMPORTANT: This needs to be done before QWidgets is imported in any way!
WORKAROUND (remove this when we bump the requirements to 5.3.1)
Args:
args: The argparse namespace.
"""
from qutebrowser.utils import log
if 'PyQt5.QtWidgets' in sys.modules:
msg = "Harfbuzz fix attempted but QtWidgets is already imported!"
if getattr(sys, 'frozen', False):
log.init.debug(msg)
else:
log.init.warning(msg)
if sys.platform.startswith('linux') and args.harfbuzz == 'auto':
if _qt_version() == pkg_resources.parse_version('5.3.0'):
log.init.debug("Using new harfbuzz engine (auto)")
os.environ['QT_HARFBUZZ'] = 'new'
elif _qt_version() < pkg_resources.parse_version('5.4.0'):
log.init.debug("Using old harfbuzz engine (auto)")
os.environ['QT_HARFBUZZ'] = 'old'
else:
log.init.debug("Using system harfbuzz engine (auto)")
elif args.harfbuzz in ['old', 'new']:
# forced harfbuzz variant
# FIXME looking at the Qt code, 'new' isn't a valid value, but leaving
# it empty and using new yields different behavior...
# (probably irrelevant when workaround gets removed)
log.init.debug("Using {} harfbuzz engine (forced)".format(
args.harfbuzz))
os.environ['QT_HARFBUZZ'] = args.harfbuzz
else:
log.init.debug("Using system harfbuzz engine")
def check_pyqt_core(): def check_pyqt_core():
"""Check if PyQt core is installed.""" """Check if PyQt core is installed."""
try: try:
@ -233,26 +163,6 @@ def check_pyqt_core():
sys.exit(1) sys.exit(1)
def get_backend(args):
"""Find out what backend to use based on available libraries.
Note this function returns the backend as a string so we don't have to
import qutebrowser.utils.usertypes yet.
"""
try:
import PyQt5.QtWebKit # pylint: disable=unused-variable
webkit_available = True
except ImportError:
webkit_available = False
if args.backend is not None:
return args.backend
elif webkit_available:
return 'webkit'
else:
return 'webengine'
def qt_version(qversion=None, qt_version_str=None): def qt_version(qversion=None, qt_version_str=None):
"""Get a Qt version string based on the runtime/compiled versions.""" """Get a Qt version string based on the runtime/compiled versions."""
if qversion is None: if qversion is None:
@ -268,50 +178,65 @@ def qt_version(qversion=None, qt_version_str=None):
return qversion return qversion
def check_qt_version(backend): def check_qt_version():
"""Check if the Qt version is recent enough.""" """Check if the Qt version is recent enough."""
from PyQt5.QtCore import PYQT_VERSION, PYQT_VERSION_STR from PyQt5.QtCore import PYQT_VERSION, PYQT_VERSION_STR
from qutebrowser.utils import qtutils from qutebrowser.utils import qtutils
if (not qtutils.version_check('5.2.0', strict=True) or if (not qtutils.version_check('5.7.1', strict=True) or
PYQT_VERSION < 0x050200): PYQT_VERSION < 0x050200):
text = ("Fatal error: Qt and PyQt >= 5.2.0 are required, but Qt {} / " text = ("Fatal error: Qt >= 5.7.1 and PyQt >= 5.7 are required, "
"PyQt {} is installed.".format(qt_version(), "but Qt {} / PyQt {} is installed.".format(qt_version(),
PYQT_VERSION_STR)) PYQT_VERSION_STR))
_die(text)
elif (backend == 'webengine' and (
not qtutils.version_check('5.7.1', strict=True) or
PYQT_VERSION < 0x050700)):
text = ("Fatal error: Qt >= 5.7.1 and PyQt >= 5.7 are required for "
"QtWebEngine support, but Qt {} / PyQt {} is installed."
.format(qt_version(), PYQT_VERSION_STR))
_die(text) _die(text)
def check_ssl_support(backend): def check_ssl_support():
"""Check if SSL support is available.""" """Check if SSL support is available."""
from qutebrowser.utils import log # pylint: disable=unused-variable
try: try:
from PyQt5.QtNetwork import QSslSocket from PyQt5.QtNetwork import QSslSocket
except ImportError: except ImportError:
_die("Fatal error: Your Qt is built without SSL support.") _die("Fatal error: Your Qt is built without SSL support.")
def check_backend_ssl_support(backend):
"""Check for full SSL availability when we know the backend."""
from PyQt5.QtNetwork import QSslSocket
from qutebrowser.utils import log, usertypes
text = ("Could not initialize QtNetwork SSL support. If you use " text = ("Could not initialize QtNetwork SSL support. If you use "
"OpenSSL 1.1 with a PyQt package from PyPI (e.g. on Archlinux " "OpenSSL 1.1 with a PyQt package from PyPI (e.g. on Archlinux "
"or Debian Stretch), you need to set LD_LIBRARY_PATH to the path " "or Debian Stretch), you need to set LD_LIBRARY_PATH to the path "
"of OpenSSL 1.0.") "of OpenSSL 1.0. This only affects downloads.")
if backend == 'webengine':
text += " This only affects downloads."
if not QSslSocket.supportsSsl(): if not QSslSocket.supportsSsl():
if backend == 'webkit': if backend == usertypes.Backend.QtWebKit:
_die("Could not initialize SSL support.") _die("Could not initialize SSL support.")
else: else:
assert backend == 'webengine' assert backend == usertypes.Backend.QtWebEngine
log.init.warning(text) log.init.warning(text)
def check_libraries(backend): def _check_modules(modules):
"""Make sure the given modules are available."""
from qutebrowser.utils import log
for name, text in modules.items():
try:
# https://github.com/pallets/jinja/pull/628
# https://bitbucket.org/birkenfeld/pygments-main/issues/1314/
# https://github.com/pallets/jinja/issues/646
# https://bitbucket.org/fdik/pypeg/commits/dd15ca462b532019c0a3be1d39b8ee2f3fa32f4e
messages = ['invalid escape sequence',
'Flags not at the start of the expression']
with log.ignore_py_warnings(
category=DeprecationWarning,
message=r'({})'.format('|'.join(messages))):
importlib.import_module(name)
except ImportError as e:
_die(text, e)
def check_libraries():
"""Check if all needed Python libraries are installed.""" """Check if all needed Python libraries are installed."""
modules = { modules = {
'pkg_resources': 'pkg_resources':
@ -338,33 +263,38 @@ def check_libraries(backend):
pip="PyYAML"), pip="PyYAML"),
'PyQt5.QtQml': _missing_str("PyQt5.QtQml"), 'PyQt5.QtQml': _missing_str("PyQt5.QtQml"),
'PyQt5.QtSql': _missing_str("PyQt5.QtSql"), 'PyQt5.QtSql': _missing_str("PyQt5.QtSql"),
'PyQt5.QtOpenGL': _missing_str("PyQt5.QtOpenGL"),
} }
if backend == 'webengine': _check_modules(modules)
modules['PyQt5.QtWebEngineWidgets'] = _missing_str("QtWebEngine",
webengine=True)
modules['PyQt5.QtOpenGL'] = _missing_str("PyQt5.QtOpenGL") def check_backend_libraries(backend):
"""Make sure the libraries needed by the given backend are available.
Args:
backend: The backend as usertypes.Backend member.
"""
from qutebrowser.utils import usertypes
if backend == usertypes.Backend.QtWebEngine:
modules = {
'PyQt5.QtWebEngineWidgets':
_missing_str("QtWebEngine", webengine=True),
}
else: else:
assert backend == 'webkit' assert backend == usertypes.Backend.QtWebKit, backend
modules['PyQt5.QtWebKit'] = _missing_str("PyQt5.QtWebKit") modules = {
modules['PyQt5.QtWebKitWidgets'] = _missing_str( 'PyQt5.QtWebKit': _missing_str("PyQt5.QtWebKit"),
"PyQt5.QtWebKitWidgets") 'PyQt5.QtWebKitWidgets': _missing_str("PyQt5.QtWebKitWidgets"),
}
_check_modules(modules)
from qutebrowser.utils import log
for name, text in modules.items(): def check_new_webkit(backend):
try: """Make sure we use QtWebEngine or a new QtWebKit."""
# https://github.com/pallets/jinja/pull/628 from qutebrowser.utils import usertypes, qtutils
# https://bitbucket.org/birkenfeld/pygments-main/issues/1314/ if backend == usertypes.Backend.QtWebKit and not qtutils.is_new_qtwebkit():
# https://github.com/pallets/jinja/issues/646 _die("qutebrowser does not support legacy QtWebKit versions anymore, "
# https://bitbucket.org/fdik/pypeg/commits/dd15ca462b532019c0a3be1d39b8ee2f3fa32f4e "see the installation docs for details.")
messages = ['invalid escape sequence',
'Flags not at the start of the expression']
with log.ignore_py_warnings(
category=DeprecationWarning,
message=r'({})'.format('|'.join(messages))):
importlib.import_module(name)
except ImportError as e:
_die(text, e)
def remove_inputhook(): def remove_inputhook():
@ -395,18 +325,7 @@ def check_optimize_flag():
"unexpected behavior may occur.") "unexpected behavior may occur.")
def set_backend(backend): def early_init(args):
"""Set the objects.backend global to the given backend (as string)."""
from qutebrowser.misc import objects
from qutebrowser.utils import usertypes
backends = {
'webkit': usertypes.Backend.QtWebKit,
'webengine': usertypes.Backend.QtWebEngine,
}
objects.backend = backends[backend]
def earlyinit(args):
"""Do all needed early initialization. """Do all needed early initialization.
Note that it's vital the other earlyinit functions get called in the right Note that it's vital the other earlyinit functions get called in the right
@ -423,15 +342,23 @@ def earlyinit(args):
check_pyqt_core() check_pyqt_core()
# Init logging as early as possible # Init logging as early as possible
init_log(args) init_log(args)
# Now the faulthandler is enabled we fix the Qt harfbuzzing library, before
# importing QtWidgets.
fix_harfbuzz(args)
# Now we can be sure QtCore is available, so we can print dialogs on # Now we can be sure QtCore is available, so we can print dialogs on
# errors, so people only using the GUI notice them as well. # errors, so people only using the GUI notice them as well.
backend = get_backend(args) check_libraries()
check_qt_version(backend) check_qt_version()
remove_inputhook() remove_inputhook()
check_libraries(backend) check_ssl_support()
check_ssl_support(backend)
check_optimize_flag() check_optimize_flag()
set_backend(backend)
def init_with_backend(backend):
"""Do later stages of init when we know the backend.
Args:
backend: The backend as usertypes.Backend member.
"""
assert not isinstance(backend, str), backend
assert backend is not None
check_backend_libraries(backend)
check_backend_ssl_support(backend)
check_new_webkit(backend)

View File

@ -22,5 +22,14 @@
# NOTE: We need to be careful with imports here, as this is imported from # NOTE: We need to be careful with imports here, as this is imported from
# earlyinit. # earlyinit.
class NoBackend:
"""Special object when there's no backend set so we notice that."""
def __eq__(self, other):
raise AssertionError("No backend set!")
# A usertypes.Backend member # A usertypes.Backend member
backend = None backend = NoBackend()

View File

@ -113,19 +113,12 @@ class SaveManager(QObject):
self.saveables = collections.OrderedDict() self.saveables = collections.OrderedDict()
self._save_timer = usertypes.Timer(self, name='save-timer') self._save_timer = usertypes.Timer(self, name='save-timer')
self._save_timer.timeout.connect(self.autosave) self._save_timer.timeout.connect(self.autosave)
self._set_autosave_interval()
config.instance.changed.connect(self._set_autosave_interval)
def __repr__(self): def __repr__(self):
return utils.get_repr(self, saveables=self.saveables) return utils.get_repr(self, saveables=self.saveables)
def init_autosave(self):
"""Initialize auto-saving.
We don't do this in __init__ because the config needs to be initialized
first, but the config needs the save manager.
"""
self._set_autosave_interval()
config.instance.changed.connect(self._set_autosave_interval)
@config.change_filter('auto_save.interval') @config.change_filter('auto_save.interval')
def _set_autosave_interval(self): def _set_autosave_interval(self):
"""Set the auto-save interval.""" """Set the auto-save interval."""

View File

@ -30,8 +30,9 @@ import yaml
from qutebrowser.utils import (standarddir, objreg, qtutils, log, message, from qutebrowser.utils import (standarddir, objreg, qtutils, log, message,
utils) utils)
from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.config import config from qutebrowser.config import config, configfiles
from qutebrowser.completion.models import miscmodels from qutebrowser.completion.models import miscmodels
from qutebrowser.mainwindow import mainwindow
default = object() # Sentinel value default = object() # Sentinel value
@ -294,8 +295,7 @@ class SessionManager(QObject):
raise SessionError(e) raise SessionError(e)
if load_next_time: if load_next_time:
state_config = objreg.get('state-config') configfiles.state['general']['session'] = name
state_config['general']['session'] = name
return name return name
def save_autosave(self): def save_autosave(self):
@ -372,7 +372,6 @@ class SessionManager(QObject):
name: The name of the session to load. name: The name of the session to load.
temp: If given, don't set the current session. temp: If given, don't set the current session.
""" """
from qutebrowser.mainwindow import mainwindow
path = self._get_session_path(name, check_exists=True) path = self._get_session_path(name, check_exists=True)
try: try:
with open(path, encoding='utf-8') as f: with open(path, encoding='utf-8') as f:

View File

@ -95,9 +95,6 @@ def get_argparser():
action='store_false', dest='color') action='store_false', dest='color')
debug.add_argument('--force-color', help="Force colored logging", debug.add_argument('--force-color', help="Force colored logging",
action='store_true') action='store_true')
debug.add_argument('--harfbuzz', choices=['old', 'new', 'system', 'auto'],
default='auto', help="HarfBuzz engine version to use. "
"Default: auto.")
debug.add_argument('--relaxed-config', action='store_true', debug.add_argument('--relaxed-config', action='store_true',
help="Silently remove unknown config options.") help="Silently remove unknown config options.")
debug.add_argument('--nowindow', action='store_true', help="Don't show " debug.add_argument('--nowindow', action='store_true', help="Don't show "
@ -170,8 +167,8 @@ def main():
# from json. # from json.
data = json.loads(args.json_args) data = json.loads(args.json_args)
args = argparse.Namespace(**data) args = argparse.Namespace(**data)
earlyinit.earlyinit(args) earlyinit.early_init(args)
# We do this imports late as earlyinit needs to be run first (because of # We do this imports late as earlyinit needs to be run first (because of
# the harfbuzz fix and version checking). # version checking and other early initialization)
from qutebrowser import app from qutebrowser import app
return app.run(args) return app.run(args)

View File

@ -63,7 +63,7 @@ class DocstringParser:
"""Generate documentation based on a docstring of a command handler. """Generate documentation based on a docstring of a command handler.
The docstring needs to follow the format described in CONTRIBUTING. The docstring needs to follow the format described in doc/contributing.
Attributes: Attributes:
_state: The current state of the parser state machine. _state: The current state of the parser state machine.

View File

@ -345,7 +345,7 @@ def qt_message_handler(msg_type, context, msg):
try: try:
qt_to_logging[QtCore.QtInfoMsg] = logging.INFO qt_to_logging[QtCore.QtInfoMsg] = logging.INFO
except AttributeError: except AttributeError:
# Qt < 5.5 # While we don't support Qt < 5.5 anymore, logging still needs to work
pass pass
# Change levels of some well-known messages to debug so they don't get # Change levels of some well-known messages to debug so they don't get

View File

@ -105,13 +105,9 @@ class ObjectRegistry(collections.UserDict):
func = partial_objs[name] func = partial_objs[name]
try: try:
self[name].destroyed.disconnect(func) self[name].destroyed.disconnect(func)
except (RuntimeError, TypeError): except RuntimeError:
# If C++ has deleted the object, the slot is already # If C++ has deleted the object, the slot is already
# disconnected. # disconnected.
#
# With older PyQt-versions (5.2.1) we'll get a "TypeError:
# pyqtSignal must be bound to a QObject" instead:
# https://github.com/qutebrowser/qutebrowser/issues/257
pass pass
del partial_objs[name] del partial_objs[name]
@ -145,7 +141,7 @@ class ObjectRegistry(collections.UserDict):
for name, obj in self.data.items(): for name, obj in self.data.items():
try: try:
obj_repr = repr(obj) obj_repr = repr(obj)
except (RuntimeError, TypeError): except RuntimeError:
# Underlying object deleted probably # Underlying object deleted probably
obj_repr = '<deleted>' obj_repr = '<deleted>'
lines.append("{}: {}".format(name, obj_repr)) lines.append("{}: {}".format(name, obj_repr))

View File

@ -28,30 +28,17 @@ Module attributes:
import io import io
import os
import sys
import operator import operator
import contextlib import contextlib
import pkg_resources
from PyQt5.QtCore import (qVersion, QEventLoop, QDataStream, QByteArray, from PyQt5.QtCore import (qVersion, QEventLoop, QDataStream, QByteArray,
QIODevice, QSaveFile, QT_VERSION_STR) QIODevice, QSaveFile, QT_VERSION_STR)
from PyQt5.QtWidgets import QApplication
try: try:
from PyQt5.QtWebKit import qWebKitVersion from PyQt5.QtWebKit import qWebKitVersion
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
qWebKitVersion = None qWebKitVersion = None
from qutebrowser.utils import log
with log.ignore_py_warnings(category=PendingDeprecationWarning, module='imp'):
with log.ignore_py_warnings(category=ImportWarning):
# This imports 'imp' and gives us a PendingDeprecationWarning on
# Debian Jessie.
#
# On Archlinux, we get ImportWarning from
# importlib/_bootstrap_external.py for modules with missing __init__.
import pkg_resources
MAXVALS = { MAXVALS = {
'int': 2 ** 31 - 1, 'int': 2 ** 31 - 1,
@ -104,8 +91,8 @@ def version_check(version, exact=False, strict=False):
return result return result
def is_qtwebkit_ng(): def is_new_qtwebkit():
"""Check if the given version is QtWebKit-NG.""" """Check if the given version is a new QtWebKit."""
assert qWebKitVersion is not None assert qWebKitVersion is not None
return (pkg_resources.parse_version(qWebKitVersion()) > return (pkg_resources.parse_version(qWebKitVersion()) >
pkg_resources.parse_version('538.1')) pkg_resources.parse_version('538.1'))
@ -139,36 +126,6 @@ def check_overflow(arg, ctype, fatal=True):
return arg return arg
def get_args(namespace):
"""Get the Qt QApplication arguments based on an argparse namespace.
Args:
namespace: The argparse namespace.
Return:
The argv list to be passed to Qt.
"""
argv = [sys.argv[0]]
if namespace.qt_flag is not None:
argv += ['--' + flag[0] for flag in namespace.qt_flag]
if namespace.qt_arg is not None:
for name, value in namespace.qt_arg:
argv += ['--' + name, value]
return argv
def check_print_compat():
"""Check if printing should work in the given Qt version."""
# WORKAROUND (remove this when we bump the requirements to 5.3.0)
if os.name == 'nt':
return version_check('5.3')
else:
return True
def ensure_valid(obj): def ensure_valid(obj):
"""Ensure a Qt object with an .isValid() method is valid.""" """Ensure a Qt object with an .isValid() method is valid."""
if not obj.isValid(): if not obj.isValid():
@ -242,23 +199,6 @@ def savefile_open(filename, binary=False, encoding='utf-8'):
raise QtOSError(f, msg="Commit failed!") raise QtOSError(f, msg="Commit failed!")
@contextlib.contextmanager
def unset_organization():
"""Temporarily unset QApplication.organizationName().
This is primarily needed in config.py.
"""
qapp = QApplication.instance()
if qapp is not None:
orgname = qapp.organizationName()
qapp.setOrganizationName(None)
try:
yield
finally:
if qapp is not None:
qapp.setOrganizationName(orgname)
class PyQIODevice(io.BufferedIOBase): class PyQIODevice(io.BufferedIOBase):
"""Wrapper for a QIODevice which provides a python interface. """Wrapper for a QIODevice which provides a python interface.

View File

@ -23,10 +23,12 @@ import os
import sys import sys
import shutil import shutil
import os.path import os.path
import contextlib
from PyQt5.QtCore import QStandardPaths from PyQt5.QtCore import QStandardPaths
from PyQt5.QtWidgets import QApplication
from qutebrowser.utils import log, qtutils, debug, usertypes, message from qutebrowser.utils import log, debug, usertypes, message
# The cached locations # The cached locations
_locations = {} _locations = {}
@ -45,6 +47,23 @@ class EmptyValueError(Exception):
"""Error raised when QStandardPaths returns an empty value.""" """Error raised when QStandardPaths returns an empty value."""
@contextlib.contextmanager
def _unset_organization():
"""Temporarily unset QApplication.organizationName().
This is primarily needed in config.py.
"""
qapp = QApplication.instance()
if qapp is not None:
orgname = qapp.organizationName()
qapp.setOrganizationName(None)
try:
yield
finally:
if qapp is not None:
qapp.setOrganizationName(orgname)
def _init_config(args): def _init_config(args):
"""Initialize the location for configs.""" """Initialize the location for configs."""
typ = QStandardPaths.ConfigLocation typ = QStandardPaths.ConfigLocation
@ -204,7 +223,7 @@ def _writable_location(typ):
# FIXME old Qt # FIXME old Qt
getattr(QStandardPaths, 'AppDataLocation', object())], typ_str getattr(QStandardPaths, 'AppDataLocation', object())], typ_str
with qtutils.unset_organization(): with _unset_organization():
path = QStandardPaths.writableLocation(typ) path = QStandardPaths.writableLocation(typ)
log.misc.debug("writable location for {}: {}".format(typ_str, path)) log.misc.debug("writable location for {}: {}".format(typ_str, path))

View File

@ -1,68 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016-2017 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=unused-import,bad-mcs-method-argument
"""Wrapper for Python 3.5's typing module.
This wrapper is needed as both Python 3.5 and typing for PyPI isn't commonly
packaged yet. As we don't actually need anything from the typing module at
runtime, we instead mock the typing classes (using objects to make things
easier) so the typing module isn't a hard dependency.
"""
# Those are defined here to make them testable easily
class FakeTypingMeta(type):
"""Fake typing metaclass like typing.TypingMeta."""
def __init__(self, *args, # pylint: disable=super-init-not-called
**_kwds):
pass
class FakeUnionMeta(FakeTypingMeta):
"""Fake union metaclass metaclass like typing.UnionMeta."""
def __new__(cls, name, bases, namespace, parameters=None):
if parameters is None:
return super().__new__(cls, name, bases, namespace)
self = super().__new__(cls, name, bases, {})
self.__union_params__ = tuple(parameters)
return self
def __getitem__(self, parameters):
return self.__class__(self.__name__, self.__bases__,
dict(self.__dict__), parameters=parameters)
class FakeUnion(metaclass=FakeUnionMeta):
"""Fake Union type like typing.Union."""
__union_params__ = None
try:
from typing import Union
except ImportError: # pragma: no cover
Union = FakeUnion

View File

@ -355,14 +355,8 @@ class Question(QObject):
log.misc.debug("Question was already aborted") log.misc.debug("Question was already aborted")
return return
self.is_aborted = True self.is_aborted = True
try: self.aborted.emit()
self.aborted.emit() self.completed.emit()
self.completed.emit()
except TypeError:
# WORKAROUND
# We seem to get "pyqtSignal must be bound to a QObject, not
# 'Question' here, which makes no sense at all..."
log.misc.exception("Error while aborting question")
class Timer(QTimer): class Timer(QTimer):

View File

@ -42,7 +42,7 @@ except ImportError: # pragma: no cover
from yaml import SafeLoader as YamlLoader, SafeDumper as YamlDumper from yaml import SafeLoader as YamlLoader, SafeDumper as YamlDumper
import qutebrowser import qutebrowser
from qutebrowser.utils import qtutils, log from qutebrowser.utils import qtutils, log, debug
fake_clipboard = None fake_clipboard = None
@ -425,14 +425,13 @@ class KeyInfo:
self.text = text self.text = text
def __repr__(self): def __repr__(self):
# Meh, dependency cycle...
from qutebrowser.utils.debug import qenum_key
if self.modifiers is None: if self.modifiers is None:
modifiers = None modifiers = None
else: else:
#modifiers = qflags_key(Qt, self.modifiers) #modifiers = qflags_key(Qt, self.modifiers)
modifiers = hex(int(self.modifiers)) modifiers = hex(int(self.modifiers))
return get_repr(self, constructor=True, key=qenum_key(Qt, self.key), return get_repr(self, constructor=True,
key=debug.qenum_key(Qt, self.key),
modifiers=modifiers, text=self.text) modifiers=modifiers, text=self.text)
def __eq__(self, other): def __eq__(self, other):
@ -814,10 +813,12 @@ def open_file(filename, cmdline=None):
the filename is appended to the cmdline. the filename is appended to the cmdline.
""" """
# Import late to avoid circular imports: # Import late to avoid circular imports:
# utils -> config -> configdata -> configtypes -> cmdutils -> command -> # - usertypes -> utils -> guiprocess -> message -> usertypes
# utils # - usertypes -> utils -> config -> configdata -> configtypes ->
from qutebrowser.misc import guiprocess # cmdutils -> command -> message -> usertypes
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.misc import guiprocess
# the default program to open downloads with - will be empty string # the default program to open downloads with - will be empty string
# if we want to use the default # if we want to use the default
override = config.val.downloads.open_dispatcher override = config.val.downloads.open_dispatcher

View File

@ -44,7 +44,7 @@ except ImportError: # pragma: no cover
QWebEngineProfile = None QWebEngineProfile = None
import qutebrowser import qutebrowser
from qutebrowser.utils import log, utils, standarddir, usertypes, qtutils from qutebrowser.utils import log, utils, standarddir, usertypes
from qutebrowser.misc import objects, earlyinit, sql from qutebrowser.misc import objects, earlyinit, sql
from qutebrowser.browser import pdfjs from qutebrowser.browser import pdfjs
@ -190,7 +190,6 @@ def _module_versions():
('pygments', ['__version__']), ('pygments', ['__version__']),
('yaml', ['__version__']), ('yaml', ['__version__']),
('cssutils', ['__version__']), ('cssutils', ['__version__']),
('typing', []),
('PyQt5.QtWebEngineWidgets', []), ('PyQt5.QtWebEngineWidgets', []),
('PyQt5.QtWebKitWidgets', []), ('PyQt5.QtWebKitWidgets', []),
]) ])
@ -304,9 +303,7 @@ def _chromium_version():
def _backend(): def _backend():
"""Get the backend line with relevant information.""" """Get the backend line with relevant information."""
if objects.backend == usertypes.Backend.QtWebKit: if objects.backend == usertypes.Backend.QtWebKit:
return '{} (WebKit {})'.format( return 'new QtWebKit (WebKit {})'.format(qWebKitVersion())
'QtWebKit-NG' if qtutils.is_qtwebkit_ng() else 'legacy QtWebKit',
qWebKitVersion())
else: else:
webengine = usertypes.Backend.QtWebEngine webengine = usertypes.Backend.QtWebEngine
assert objects.backend == webengine, objects.backend assert objects.backend == webengine, objects.backend

View File

@ -40,13 +40,7 @@ class AsciiDoc:
"""Abstraction of an asciidoc subprocess.""" """Abstraction of an asciidoc subprocess."""
FILES = [ FILES = ['faq', 'changelog', 'contributing', 'quickstart', 'userscripts']
('FAQ.asciidoc', 'qutebrowser/html/doc/FAQ.html'),
('CHANGELOG.asciidoc', 'qutebrowser/html/doc/CHANGELOG.html'),
('CONTRIBUTING.asciidoc', 'qutebrowser/html/doc/CONTRIBUTING.html'),
('doc/quickstart.asciidoc', 'qutebrowser/html/doc/quickstart.html'),
('doc/userscripts.asciidoc', 'qutebrowser/html/doc/userscripts.html'),
]
def __init__(self, args): def __init__(self, args):
self._cmd = None self._cmd = None
@ -80,7 +74,9 @@ class AsciiDoc:
def _build_docs(self): def _build_docs(self):
"""Render .asciidoc files to .html sites.""" """Render .asciidoc files to .html sites."""
files = self.FILES[:] files = [('doc/{}.asciidoc'.format(f),
'qutebrowser/html/doc/{}.html'.format(f))
for f in self.FILES]
for src in glob.glob('doc/help/*.asciidoc'): for src in glob.glob('doc/help/*.asciidoc'):
name, _ext = os.path.splitext(os.path.basename(src)) name, _ext = os.path.splitext(os.path.basename(src))
dst = 'qutebrowser/html/doc/{}.html'.format(name) dst = 'qutebrowser/html/doc/{}.html'.format(name)

View File

@ -21,7 +21,7 @@
NAME ?= qutebrowser NAME ?= qutebrowser
SOURCE_DIR ?= . SOURCE_DIR ?= .
SOURCE_FILES ?= dist/qutebrowser.app COPYING SOURCE_FILES ?= dist/qutebrowser.app LICENSE
TEMPLATE_DMG ?= template.dmg TEMPLATE_DMG ?= template.dmg
TEMPLATE_SIZE ?= 300m TEMPLATE_SIZE ?= 300m

View File

@ -149,8 +149,6 @@ PERFECT_FILES = [
'utils/jinja.py'), 'utils/jinja.py'),
('tests/unit/utils/test_error.py', ('tests/unit/utils/test_error.py',
'utils/error.py'), 'utils/error.py'),
('tests/unit/utils/test_typing.py',
'utils/typing.py'),
('tests/unit/utils/test_javascript.py', ('tests/unit/utils/test_javascript.py',
'utils/javascript.py'), 'utils/javascript.py'),
@ -291,7 +289,7 @@ def main_check_all():
tests. tests.
This runs pytest with the used executable, so check_coverage.py should be This runs pytest with the used executable, so check_coverage.py should be
called with something like ./.tox/py34/bin/python. called with something like ./.tox/py36/bin/python.
""" """
for test_file, src_file in PERFECT_FILES: for test_file, src_file in PERFECT_FILES:
if test_file is None: if test_file is None:

View File

@ -5,10 +5,6 @@
# #
case $TESTENV in case $TESTENV in
py34-cov)
exe=/usr/bin/python3.4
full=full
;;
py3*-pyqt*) py3*-pyqt*)
exe=$(readlink -f .tox/$TESTENV/bin/python) exe=$(readlink -f .tox/$TESTENV/bin/python)
full= full=

View File

@ -121,7 +121,8 @@ setupdata = {
'Operating System :: Microsoft :: Windows :: Windows 7', 'Operating System :: Microsoft :: Windows :: Windows 7',
'Operating System :: POSIX :: Linux', 'Operating System :: POSIX :: Linux',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Topic :: Internet', 'Topic :: Internet',
'Topic :: Internet :: WWW/HTTP', 'Topic :: Internet :: WWW/HTTP',
'Topic :: Internet :: WWW/HTTP :: Browsers', 'Topic :: Internet :: WWW/HTTP :: Browsers',

View File

@ -35,14 +35,15 @@ from helpers import logfail
from helpers.logfail import fail_on_logging from helpers.logfail import fail_on_logging
from helpers.messagemock import message_mock from helpers.messagemock import message_mock
from helpers.fixtures import * from helpers.fixtures import *
from qutebrowser.utils import qtutils, standarddir from qutebrowser.utils import qtutils, standarddir, usertypes
from qutebrowser.misc import objects
import qutebrowser.app # To register commands import qutebrowser.app # To register commands
# Set hypothesis settings # Set hypothesis settings
hypothesis.settings.register_profile('default', hypothesis.settings.register_profile(
hypothesis.settings(strict=True)) 'default', hypothesis.settings(strict=True, deadline=400))
hypothesis.settings.load_profile('default') hypothesis.settings.load_profile('default')
@ -62,7 +63,6 @@ def _apply_platform_markers(config, item):
('no_ci', 'CI' in os.environ, "Skipped on CI."), ('no_ci', 'CI' in os.environ, "Skipped on CI."),
('issue2478', os.name == 'nt' and config.webengine, ('issue2478', os.name == 'nt' and config.webengine,
"Broken with QtWebEngine on Windows"), "Broken with QtWebEngine on Windows"),
('qt55', not qtutils.version_check('5.5'), "Requires Qt 5.5 or newer"),
] ]
for searched_marker, condition, default_reason in markers: for searched_marker, condition, default_reason in markers:
@ -128,12 +128,9 @@ def pytest_collection_modifyitems(config, items):
item.add_marker(pytest.mark.xfail(run=False)) item.add_marker(pytest.mark.xfail(run=False))
if item.get_marker('js_prompt'): if item.get_marker('js_prompt'):
if config.webengine: if config.webengine:
js_prompt_pyqt_version = 0x050700 item.add_marker(pytest.mark.skipif(
else: PYQT_VERSION <= 0x050700,
js_prompt_pyqt_version = 0x050300 reason='JS prompts are not supported with PyQt 5.7'))
item.add_marker(pytest.mark.skipif(
PYQT_VERSION <= js_prompt_pyqt_version,
reason='JS prompts are not supported with this PyQt version'))
if deselected: if deselected:
deselected_items.append(item) deselected_items.append(item)
@ -188,6 +185,14 @@ def check_display(request):
raise Exception("No display and no Xvfb available!") raise Exception("No display and no Xvfb available!")
@pytest.fixture(autouse=True)
def set_backend(monkeypatch, request):
"""Make sure the backend global is set."""
backend = (usertypes.Backend.QtWebEngine if request.config.webengine
else usertypes.Backend.QtWebKit)
monkeypatch.setattr(objects, 'backend', backend)
@pytest.hookimpl(tryfirst=True, hookwrapper=True) @pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call): def pytest_runtest_makereport(item, call):
"""Make test information available in fixtures. """Make test information available in fixtures.

View File

@ -34,7 +34,7 @@ from PyQt5.QtCore import PYQT_VERSION
pytest.register_assert_rewrite('end2end.fixtures') pytest.register_assert_rewrite('end2end.fixtures')
from end2end.fixtures.webserver import httpbin, httpbin_after_test, ssl_server from end2end.fixtures.webserver import server, server_after_test, ssl_server
from end2end.fixtures.quteprocess import (quteproc_process, quteproc, from end2end.fixtures.quteprocess import (quteproc_process, quteproc,
quteproc_new) quteproc_new)
from end2end.fixtures.testprocess import pytest_runtest_makereport from end2end.fixtures.testprocess import pytest_runtest_makereport
@ -109,8 +109,6 @@ def _get_backend_tag(tag):
'qtwebengine_todo': pytest.mark.qtwebengine_todo, 'qtwebengine_todo': pytest.mark.qtwebengine_todo,
'qtwebengine_skip': pytest.mark.qtwebengine_skip, 'qtwebengine_skip': pytest.mark.qtwebengine_skip,
'qtwebkit_skip': pytest.mark.qtwebkit_skip, 'qtwebkit_skip': pytest.mark.qtwebkit_skip,
'qtwebkit_ng_xfail': pytest.mark.qtwebkit_ng_xfail,
'qtwebkit_ng_skip': pytest.mark.qtwebkit_ng_skip,
} }
if not any(tag.startswith(t + ':') for t in pytest_marks): if not any(tag.startswith(t + ':') for t in pytest_marks):
return None return None
@ -143,10 +141,6 @@ def pytest_collection_modifyitems(config, items):
config.webengine), config.webengine),
('qtwebkit_skip', 'Skipped with QtWebKit', pytest.mark.skipif, ('qtwebkit_skip', 'Skipped with QtWebKit', pytest.mark.skipif,
not config.webengine), not config.webengine),
('qtwebkit_ng_xfail', 'Failing with QtWebKit-NG', pytest.mark.xfail,
not config.webengine and qtutils.is_qtwebkit_ng()),
('qtwebkit_ng_skip', 'Skipped with QtWebKit-NG', pytest.mark.skipif,
not config.webengine and qtutils.is_qtwebkit_ng()),
('qtwebengine_flaky', 'Flaky with QtWebEngine', pytest.mark.skipif, ('qtwebengine_flaky', 'Flaky with QtWebEngine', pytest.mark.skipif,
config.webengine), config.webengine),
('qtwebengine_mac_xfail', 'Fails on macOS with QtWebEngine', ('qtwebengine_mac_xfail', 'Fails on macOS with QtWebEngine',

View File

@ -5,7 +5,7 @@
<title>Failing download when redirecting/aborting</title> <title>Failing download when redirecting/aborting</title>
</head> </head>
<body> <body>
<a href="/custom/redirect-later?delay=1">redirect after 1s</a> <a href="/redirect-later?delay=1">redirect after 1s</a>
<a href="/does-not-exist">404</a> <a href="/does-not-exist">404</a>
</body> </body>
</html> </html>

View File

@ -80,7 +80,7 @@
<h2>...and how?</h2> <h2>...and how?</h2>
<p>See <a href="https://github.com/qutebrowser/qutebrowser/blob/master/CONTRIBUTING.asciidoc"> <p>See <a href="https://github.com/qutebrowser/qutebrowser/blob/master/doc/contributing.asciidoc">
here</a> for more information.</p> here</a> for more information.</p>
<h2>More useless trivia!</h2> <h2>More useless trivia!</h2>

View File

@ -97,7 +97,7 @@ the
<h2>...and how?</h2> <h2>...and how?</h2>
=20 =20
<p>See <a href=3D"https://github.com/qutebrowser/qutebrowser/blob/m= <p>See <a href=3D"https://github.com/qutebrowser/qutebrowser/blob/m=
aster/CONTRIBUTING.asciidoc"> aster/doc/contributing.asciidoc">
here</a> for more information.</p> here</a> for more information.</p>
=20 =20
<h2>More useless trivia!</h2> <h2>More useless trivia!</h2>

View File

@ -6,11 +6,7 @@
var my_window; var my_window;
function open_modal() { function open_modal() {
if (window.showModalDialog) { window.open('about:blank', 'window', 'modal');
window.showModalDialog();
} else {
window.open('about:blank', 'window', 'modal');
}
} }
function open_normal() { function open_normal() {

View File

@ -116,14 +116,14 @@ def pytest_runtest_makereport(item, call):
@bdd.given(bdd.parsers.parse("I set {opt} to {value}")) @bdd.given(bdd.parsers.parse("I set {opt} to {value}"))
def set_setting_given(quteproc, httpbin, opt, value): def set_setting_given(quteproc, server, opt, value):
"""Set a qutebrowser setting. """Set a qutebrowser setting.
This is available as "Given:" step so it can be used as "Background:". This is available as "Given:" step so it can be used as "Background:".
""" """
if value == '<empty>': if value == '<empty>':
value = '' value = ''
value = value.replace('(port)', str(httpbin.port)) value = value.replace('(port)', str(server.port))
quteproc.set_setting(opt, value) quteproc.set_setting(opt, value)
@ -174,7 +174,7 @@ def pdfjs_available():
@bdd.when(bdd.parsers.parse("I open {path}")) @bdd.when(bdd.parsers.parse("I open {path}"))
def open_path(quteproc, httpbin, path): def open_path(quteproc, server, path):
"""Open a URL. """Open a URL.
- If used like "When I open ... in a new tab", the URL is opened in a new - If used like "When I open ... in a new tab", the URL is opened in a new
@ -183,7 +183,7 @@ def open_path(quteproc, httpbin, path):
- With "... in a private window" it's opened in a new private window. - With "... in a private window" it's opened in a new private window.
- With "... as a URL", it's opened according to new_instance_open_target. - With "... as a URL", it's opened according to new_instance_open_target.
""" """
path = path.replace('(port)', str(httpbin.port)) path = path.replace('(port)', str(server.port))
new_tab = False new_tab = False
new_bg_tab = False new_bg_tab = False
@ -227,16 +227,16 @@ def open_path(quteproc, httpbin, path):
@bdd.when(bdd.parsers.parse("I set {opt} to {value}")) @bdd.when(bdd.parsers.parse("I set {opt} to {value}"))
def set_setting(quteproc, httpbin, opt, value): def set_setting(quteproc, server, opt, value):
"""Set a qutebrowser setting.""" """Set a qutebrowser setting."""
if value == '<empty>': if value == '<empty>':
value = '' value = ''
value = value.replace('(port)', str(httpbin.port)) value = value.replace('(port)', str(server.port))
quteproc.set_setting(opt, value) quteproc.set_setting(opt, value)
@bdd.when(bdd.parsers.parse("I run {command}")) @bdd.when(bdd.parsers.parse("I run {command}"))
def run_command(quteproc, httpbin, tmpdir, command): def run_command(quteproc, server, tmpdir, command):
"""Run a qutebrowser command. """Run a qutebrowser command.
The suffix "with count ..." can be used to pass a count to the command. The suffix "with count ..." can be used to pass a count to the command.
@ -254,7 +254,7 @@ def run_command(quteproc, httpbin, tmpdir, command):
else: else:
invalid = False invalid = False
command = command.replace('(port)', str(httpbin.port)) command = command.replace('(port)', str(server.port))
command = command.replace('(testdata)', utils.abs_datapath()) command = command.replace('(testdata)', utils.abs_datapath())
command = command.replace('(tmpdir)', str(tmpdir)) command = command.replace('(tmpdir)', str(tmpdir))
command = command.replace('(dirsep)', os.sep) command = command.replace('(dirsep)', os.sep)
@ -264,9 +264,9 @@ def run_command(quteproc, httpbin, tmpdir, command):
@bdd.when(bdd.parsers.parse("I reload")) @bdd.when(bdd.parsers.parse("I reload"))
def reload(qtbot, httpbin, quteproc, command): def reload(qtbot, server, quteproc, command):
"""Reload and wait until a new request is received.""" """Reload and wait until a new request is received."""
with qtbot.waitSignal(httpbin.new_request): with qtbot.waitSignal(server.new_request):
quteproc.send_cmd(':reload') quteproc.send_cmd(':reload')
@ -294,10 +294,10 @@ def wait_in_log(quteproc, is_regex, pattern, do_skip):
@bdd.when(bdd.parsers.re(r'I wait for the (?P<category>error|message|warning) ' @bdd.when(bdd.parsers.re(r'I wait for the (?P<category>error|message|warning) '
r'"(?P<message>.*)"')) r'"(?P<message>.*)"'))
def wait_for_message(quteproc, httpbin, category, message): def wait_for_message(quteproc, server, category, message):
"""Wait for a given statusbar message/error/warning.""" """Wait for a given statusbar message/error/warning."""
quteproc.log_summary('Waiting for {} "{}"'.format(category, message)) quteproc.log_summary('Waiting for {} "{}"'.format(category, message))
expect_message(quteproc, httpbin, category, message) expect_message(quteproc, server, category, message)
@bdd.when(bdd.parsers.parse("I wait {delay}s")) @bdd.when(bdd.parsers.parse("I wait {delay}s"))
@ -328,8 +328,8 @@ def selection_not_supported(qapp):
@bdd.when(bdd.parsers.re(r'I put "(?P<content>.*)" into the ' @bdd.when(bdd.parsers.re(r'I put "(?P<content>.*)" into the '
r'(?P<what>primary selection|clipboard)')) r'(?P<what>primary selection|clipboard)'))
def fill_clipboard(quteproc, httpbin, what, content): def fill_clipboard(quteproc, server, what, content):
content = content.replace('(port)', str(httpbin.port)) content = content.replace('(port)', str(server.port))
content = content.replace(r'\n', '\n') content = content.replace(r'\n', '\n')
quteproc.send_cmd(':debug-set-fake-clipboard "{}"'.format(content)) quteproc.send_cmd(':debug-set-fake-clipboard "{}"'.format(content))
@ -337,8 +337,8 @@ def fill_clipboard(quteproc, httpbin, what, content):
@bdd.when(bdd.parsers.re(r'I put the following lines into the ' @bdd.when(bdd.parsers.re(r'I put the following lines into the '
r'(?P<what>primary selection|clipboard):\n' r'(?P<what>primary selection|clipboard):\n'
r'(?P<content>.+)$', flags=re.DOTALL)) r'(?P<content>.+)$', flags=re.DOTALL))
def fill_clipboard_multiline(quteproc, httpbin, what, content): def fill_clipboard_multiline(quteproc, server, what, content):
fill_clipboard(quteproc, httpbin, what, textwrap.dedent(content)) fill_clipboard(quteproc, server, what, textwrap.dedent(content))
@bdd.when(bdd.parsers.parse('I hint with args "{args}"')) @bdd.when(bdd.parsers.parse('I hint with args "{args}"'))
@ -395,28 +395,28 @@ def path_should_be_loaded(quteproc, path):
@bdd.then(bdd.parsers.parse("{path} should be requested")) @bdd.then(bdd.parsers.parse("{path} should be requested"))
def path_should_be_requested(httpbin, path): def path_should_be_requested(server, path):
"""Make sure the given path was loaded from the webserver.""" """Make sure the given path was loaded from the webserver."""
httpbin.wait_for(verb='GET', path='/' + path) server.wait_for(verb='GET', path='/' + path)
@bdd.then(bdd.parsers.parse("The requests should be:\n{pages}")) @bdd.then(bdd.parsers.parse("The requests should be:\n{pages}"))
def list_of_requests(httpbin, pages): def list_of_requests(server, pages):
"""Make sure the given requests were done from the webserver.""" """Make sure the given requests were done from the webserver."""
expected_requests = [httpbin.ExpectedRequest('GET', '/' + path.strip()) expected_requests = [server.ExpectedRequest('GET', '/' + path.strip())
for path in pages.split('\n')] for path in pages.split('\n')]
actual_requests = httpbin.get_requests() actual_requests = server.get_requests()
assert actual_requests == expected_requests assert actual_requests == expected_requests
@bdd.then(bdd.parsers.parse("The unordered requests should be:\n{pages}")) @bdd.then(bdd.parsers.parse("The unordered requests should be:\n{pages}"))
def list_of_requests_unordered(httpbin, pages): def list_of_requests_unordered(server, pages):
"""Make sure the given requests were done (in no particular order).""" """Make sure the given requests were done (in no particular order)."""
expected_requests = [httpbin.ExpectedRequest('GET', '/' + path.strip()) expected_requests = [server.ExpectedRequest('GET', '/' + path.strip())
for path in pages.split('\n')] for path in pages.split('\n')]
actual_requests = httpbin.get_requests() actual_requests = server.get_requests()
# Requests are not hashable, we need to convert to ExpectedRequests # Requests are not hashable, we need to convert to ExpectedRequests
actual_requests = [httpbin.ExpectedRequest.from_request(req) actual_requests = [server.ExpectedRequest.from_request(req)
for req in actual_requests] for req in actual_requests]
assert (collections.Counter(actual_requests) == assert (collections.Counter(actual_requests) ==
collections.Counter(expected_requests)) collections.Counter(expected_requests))
@ -424,14 +424,14 @@ def list_of_requests_unordered(httpbin, pages):
@bdd.then(bdd.parsers.re(r'the (?P<category>error|message|warning) ' @bdd.then(bdd.parsers.re(r'the (?P<category>error|message|warning) '
r'"(?P<message>.*)" should be shown')) r'"(?P<message>.*)" should be shown'))
def expect_message(quteproc, httpbin, category, message): def expect_message(quteproc, server, category, message):
"""Expect the given message in the qutebrowser log.""" """Expect the given message in the qutebrowser log."""
category_to_loglevel = { category_to_loglevel = {
'message': logging.INFO, 'message': logging.INFO,
'error': logging.ERROR, 'error': logging.ERROR,
'warning': logging.WARNING, 'warning': logging.WARNING,
} }
message = message.replace('(port)', str(httpbin.port)) message = message.replace('(port)', str(server.port))
quteproc.mark_expected(category='message', quteproc.mark_expected(category='message',
loglevel=category_to_loglevel[category], loglevel=category_to_loglevel[category],
message=message) message=message)
@ -439,12 +439,12 @@ def expect_message(quteproc, httpbin, category, message):
@bdd.then(bdd.parsers.re(r'(?P<is_regex>regex )?"(?P<pattern>[^"]+)" should ' @bdd.then(bdd.parsers.re(r'(?P<is_regex>regex )?"(?P<pattern>[^"]+)" should '
r'be logged')) r'be logged'))
def should_be_logged(quteproc, httpbin, is_regex, pattern): def should_be_logged(quteproc, server, is_regex, pattern):
"""Expect the given pattern on regex in the log.""" """Expect the given pattern on regex in the log."""
if is_regex: if is_regex:
pattern = re.compile(pattern) pattern = re.compile(pattern)
else: else:
pattern = pattern.replace('(port)', str(httpbin.port)) pattern = pattern.replace('(port)', str(server.port))
line = quteproc.wait_for(message=pattern) line = quteproc.wait_for(message=pattern)
line.expected = True line.expected = True
@ -493,7 +493,7 @@ def no_crash():
def check_header(quteproc, header, value): def check_header(quteproc, header, value):
"""Check if a given header is set correctly. """Check if a given header is set correctly.
This assumes we're on the httpbin header page. This assumes we're on the server header page.
""" """
content = quteproc.get_content() content = quteproc.get_content()
data = json.loads(content) data = json.loads(content)
@ -589,16 +589,16 @@ def check_open_tabs(quteproc, request, tabs):
@bdd.then(bdd.parsers.re(r'the (?P<what>primary selection|clipboard) should ' @bdd.then(bdd.parsers.re(r'the (?P<what>primary selection|clipboard) should '
r'contain "(?P<content>.*)"')) r'contain "(?P<content>.*)"'))
def clipboard_contains(quteproc, httpbin, what, content): def clipboard_contains(quteproc, server, what, content):
expected = content.replace('(port)', str(httpbin.port)) expected = content.replace('(port)', str(server.port))
expected = expected.replace('\\n', '\n') expected = expected.replace('\\n', '\n')
quteproc.wait_for(message='Setting fake {}: {}'.format( quteproc.wait_for(message='Setting fake {}: {}'.format(
what, json.dumps(expected))) what, json.dumps(expected)))
@bdd.then(bdd.parsers.parse('the clipboard should contain:\n{content}')) @bdd.then(bdd.parsers.parse('the clipboard should contain:\n{content}'))
def clipboard_contains_multiline(quteproc, httpbin, content): def clipboard_contains_multiline(quteproc, server, content):
expected = textwrap.dedent(content).replace('(port)', str(httpbin.port)) expected = textwrap.dedent(content).replace('(port)', str(server.port))
quteproc.wait_for(message='Setting fake clipboard: {}'.format( quteproc.wait_for(message='Setting fake clipboard: {}'.format(
json.dumps(expected))) json.dumps(expected)))

View File

@ -292,7 +292,7 @@ Feature: Downloading things from a website.
Scenario: Opening a mhtml download directly Scenario: Opening a mhtml download directly
When I set downloads.location.prompt to true When I set downloads.location.prompt to true
And I open html And I open /
And I run :download --mhtml And I run :download --mhtml
And I wait for the download prompt for "*" And I wait for the download prompt for "*"
And I directly open the download And I directly open the download
@ -572,27 +572,27 @@ Feature: Downloading things from a website.
Scenario: Downloading with redirect to itself Scenario: Downloading with redirect to itself
When I set downloads.location.prompt to false When I set downloads.location.prompt to false
And I run :download http://localhost:(port)/custom/redirect-self And I run :download http://localhost:(port)/redirect-self
And I wait until the download is finished And I wait until the download is finished
Then the downloaded file redirect-self should exist Then the downloaded file redirect-self should exist
Scenario: Downloading with absolute redirect Scenario: Downloading with absolute redirect
When I set downloads.location.prompt to false When I set downloads.location.prompt to false
And I run :download http://localhost:(port)/absolute-redirect/1 And I run :download http://localhost:(port)/absolute-redirect
And I wait until the download is finished And I wait until the download is finished
Then the downloaded file 1 should exist Then the downloaded file absolute-redirect should exist
Scenario: Downloading with relative redirect Scenario: Downloading with relative redirect
When I set downloads.location.prompt to false When I set downloads.location.prompt to false
And I run :download http://localhost:(port)/relative-redirect/1 And I run :download http://localhost:(port)/relative-redirect
And I wait until the download is finished And I wait until the download is finished
Then the downloaded file 1 should exist Then the downloaded file relative-redirect should exist
## Other ## Other
Scenario: Download without a content-size Scenario: Download without a content-size
When I set downloads.location.prompt to false When I set downloads.location.prompt to false
When I run :download http://localhost:(port)/custom/content-size When I run :download http://localhost:(port)/content-size
And I wait until the download is finished And I wait until the download is finished
Then the downloaded file content-size should exist Then the downloaded file content-size should exist
@ -604,13 +604,13 @@ Feature: Downloading things from a website.
Scenario: Downloading 20MB file Scenario: Downloading 20MB file
When I set downloads.location.prompt to false When I set downloads.location.prompt to false
And I run :download http://localhost:(port)/custom/twenty-mb And I run :download http://localhost:(port)/twenty-mb
And I wait until the download is finished And I wait until the download is finished
Then the downloaded file twenty-mb should be 20971520 bytes big Then the downloaded file twenty-mb should be 20971520 bytes big
Scenario: Downloading 20MB file with late prompt confirmation Scenario: Downloading 20MB file with late prompt confirmation
When I set downloads.location.prompt to true When I set downloads.location.prompt to true
And I run :download http://localhost:(port)/custom/twenty-mb And I run :download http://localhost:(port)/twenty-mb
And I wait 1s And I wait 1s
And I run :prompt-accept And I run :prompt-accept
And I wait until the download is finished And I wait until the download is finished
@ -645,12 +645,6 @@ Feature: Downloading things from a website.
Then the downloaded file download.bin should exist Then the downloaded file download.bin should exist
And the downloaded file download2.bin should not exist And the downloaded file download2.bin should not exist
Scenario: Downloading a file with unknown size
When I set downloads.location.prompt to false
And I open stream-bytes/1024 without waiting
And I wait until the download is finished
Then the downloaded file 1024 should exist
@qtwebengine_skip: We can't get the UA from the page there @qtwebengine_skip: We can't get the UA from the page there
Scenario: user-agent when using :download Scenario: user-agent when using :download
When I open user-agent When I open user-agent
@ -660,16 +654,14 @@ Feature: Downloading things from a website.
@qtwebengine_skip: We can't get the UA from the page there @qtwebengine_skip: We can't get the UA from the page there
Scenario: user-agent when using hints Scenario: user-agent when using hints
When I set hints.mode to number When I open /
And I open /
And I run :hint links download And I run :hint links download
And I press the keys "us" # user-agent And I run :follow-hint a
And I run :follow-hint 0
And I wait until the download is finished And I wait until the download is finished
Then the downloaded file user-agent should contain Safari/ Then the downloaded file user-agent should contain Safari/
@qtwebengine_skip: Handled by QtWebEngine, not by us @qtwebengine_skip: Handled by QtWebEngine, not by us
Scenario: Downloading a "Internal server error" with disposition: inline (#2304) Scenario: Downloading a "Internal server error" with disposition: inline (#2304)
When I set downloads.location.prompt to false When I set downloads.location.prompt to false
And I open custom/500-inline And I open 500-inline
Then the error "Download error: *INTERNAL SERVER ERROR" should be shown Then the error "Download error: *INTERNAL SERVER ERROR" should be shown

View File

@ -46,10 +46,10 @@ Feature: Page history
@qtwebengine_todo: Error page message is not implemented @qtwebengine_todo: Error page message is not implemented
Scenario: History with a 404 Scenario: History with a 404
When I open status/404 without waiting When I open 404 without waiting
And I wait for "Error while loading http://localhost:*/status/404: NOT FOUND" in the log And I wait for "Error while loading http://localhost:*/404: NOT FOUND" in the log
Then the history should contain: Then the history should contain:
http://localhost:(port)/status/404 Error loading page: http://localhost:(port)/status/404 http://localhost:(port)/404 Error loading page: http://localhost:(port)/404
Scenario: History with invalid URL Scenario: History with invalid URL
When I run :tab-only When I run :tab-only
@ -103,13 +103,4 @@ Feature: Page history
And I open qute:history without waiting And I open qute:history without waiting
And I wait until qute://history is loaded And I wait until qute://history is loaded
Then the page should contain the plaintext "3.txt" Then the page should contain the plaintext "3.txt"
Then the page should contain the plaintext "4.txt" Then the page should contain the plaintext "4.txt"
## Bugs
@qtwebengine_skip @qtwebkit_ng_skip
Scenario: Opening a valid URL which turns out invalid
When I set url.auto_search to naive
And I run :open http://foo%40bar@baz
Then "QFSFileEngine::open: No file name specified" should be logged
And "Error while loading : Host not found" should be logged

View File

@ -17,7 +17,7 @@ Feature: Javascript stuff
And I run :click-element id close-normal And I run :click-element id close-normal
Then "Focus object changed: *" should be logged Then "Focus object changed: *" should be logged
@qtwebkit_ng_skip @qtwebkit_skip
Scenario: Opening/closing a modal window via JS Scenario: Opening/closing a modal window via JS
When I open data/javascript/window_open.html When I open data/javascript/window_open.html
And I run :tab-only And I run :tab-only
@ -26,7 +26,6 @@ Feature: Javascript stuff
And I run :tab-focus 1 And I run :tab-focus 1
And I run :click-element id close-normal And I run :click-element id close-normal
Then "Focus object changed: *" should be logged Then "Focus object changed: *" should be logged
# WebModalDialog with QtWebKit, WebDialog with QtWebEngine
And "Web*Dialog requested, but we don't support that!" should be logged And "Web*Dialog requested, but we don't support that!" should be logged
# https://github.com/qutebrowser/qutebrowser/issues/906 # https://github.com/qutebrowser/qutebrowser/issues/906

View File

@ -186,15 +186,15 @@ Feature: Various utility commands.
Given I have a fresh instance Given I have a fresh instance
# We can't use "When I open" because we don't want to wait for load # We can't use "When I open" because we don't want to wait for load
# finished # finished
When I run :open http://localhost:(port)/custom/redirect-later?delay=-1 When I run :open http://localhost:(port)/redirect-later?delay=-1
And I wait for "emitting: cur_load_status_changed('loading') (tab *)" in the log And I wait for "emitting: cur_load_status_changed('loading') (tab *)" in the log
And I wait 1s And I wait 1s
And I run :stop And I run :stop
And I open custom/redirect-later-continue in a new tab And I open redirect-later-continue in a new tab
And I wait 1s And I wait 1s
Then the unordered requests should be: Then the unordered requests should be:
custom/redirect-later-continue redirect-later-continue
custom/redirect-later?delay=-1 redirect-later?delay=-1
# no request on / because we stopped the redirect # no request on / because we stopped the redirect
Scenario: :stop with wrong count Scenario: :stop with wrong count

View File

@ -42,7 +42,6 @@ Feature: Using private browsing
## https://github.com/qutebrowser/qutebrowser/issues/1219 ## https://github.com/qutebrowser/qutebrowser/issues/1219
@qtwebkit_ng_skip: private browsing is not implemented yet
Scenario: Sharing cookies with private browsing Scenario: Sharing cookies with private browsing
When I open cookies/set?qute-test=42 without waiting in a private window When I open cookies/set?qute-test=42 without waiting in a private window
And I wait until cookies is loaded And I wait until cookies is loaded

View File

@ -25,7 +25,7 @@ bdd.scenarios('adblock.feature')
@bdd.when(bdd.parsers.parse('I set up "{lists}" as block lists')) @bdd.when(bdd.parsers.parse('I set up "{lists}" as block lists'))
def set_up_blocking(quteproc, lists, httpbin): def set_up_blocking(quteproc, lists, server):
url = 'http://localhost:{}/data/adblock/'.format(httpbin.port) url = 'http://localhost:{}/data/adblock/'.format(server.port)
urls = [url + item.strip() for item in lists.split(',')] urls = [url + item.strip() for item in lists.split(',')]
quteproc.set_setting('content.host_blocking.lists', json.dumps(urls)) quteproc.set_setting('content.host_blocking.lists', json.dumps(urls))

View File

@ -27,9 +27,9 @@ bdd.scenarios('editor.feature')
@bdd.when(bdd.parsers.parse('I set up a fake editor replacing "{text}" by ' @bdd.when(bdd.parsers.parse('I set up a fake editor replacing "{text}" by '
'"{replacement}"')) '"{replacement}"'))
def set_up_editor_replacement(quteproc, httpbin, tmpdir, text, replacement): def set_up_editor_replacement(quteproc, server, tmpdir, text, replacement):
"""Set up editor.command to a small python script doing a replacement.""" """Set up editor.command to a small python script doing a replacement."""
text = text.replace('(port)', str(httpbin.port)) text = text.replace('(port)', str(server.port))
script = tmpdir / 'script.py' script = tmpdir / 'script.py'
script.write(textwrap.dedent(""" script.write(textwrap.dedent("""
import sys import sys
@ -47,7 +47,7 @@ def set_up_editor_replacement(quteproc, httpbin, tmpdir, text, replacement):
@bdd.when(bdd.parsers.parse('I set up a fake editor returning "{text}"')) @bdd.when(bdd.parsers.parse('I set up a fake editor returning "{text}"'))
def set_up_editor(quteproc, httpbin, tmpdir, text): def set_up_editor(quteproc, server, tmpdir, text):
"""Set up editor.command to a small python script inserting a text.""" """Set up editor.command to a small python script inserting a text."""
script = tmpdir / 'script.py' script = tmpdir / 'script.py'
script.write(textwrap.dedent(""" script.write(textwrap.dedent("""

View File

@ -35,7 +35,7 @@ def turn_on_sql_history(quteproc):
@bdd.then(bdd.parsers.parse("the history should contain:\n{expected}")) @bdd.then(bdd.parsers.parse("the history should contain:\n{expected}"))
def check_history(quteproc, httpbin, tmpdir, expected): def check_history(quteproc, server, tmpdir, expected):
path = tmpdir / 'history' path = tmpdir / 'history'
quteproc.send_cmd(':debug-dump-history "{}"'.format(path)) quteproc.send_cmd(':debug-dump-history "{}"'.format(path))
quteproc.wait_for(category='message', loglevel=logging.INFO, quteproc.wait_for(category='message', loglevel=logging.INFO,
@ -45,10 +45,10 @@ def check_history(quteproc, httpbin, tmpdir, expected):
# ignore access times, they will differ in each run # ignore access times, they will differ in each run
actual = '\n'.join(re.sub('^\\d+-?', '', line).strip() for line in f) actual = '\n'.join(re.sub('^\\d+-?', '', line).strip() for line in f)
expected = expected.replace('(port)', str(httpbin.port)) expected = expected.replace('(port)', str(server.port))
assert actual == expected assert actual == expected
@bdd.then("the history should be empty") @bdd.then("the history should be empty")
def check_history_empty(quteproc, httpbin, tmpdir): def check_history_empty(quteproc, server, tmpdir):
check_history(quteproc, httpbin, tmpdir, '') check_history(quteproc, server, tmpdir, '')

View File

@ -27,7 +27,7 @@ bdd.scenarios('private.feature')
def check_cookie(quteproc, name, value): def check_cookie(quteproc, name, value):
"""Check if a given cookie is set correctly. """Check if a given cookie is set correctly.
This assumes we're on the httpbin cookies page. This assumes we're on the server cookies page.
""" """
content = quteproc.get_content() content = quteproc.get_content()
data = json.loads(content) data = json.loads(content)

View File

@ -34,13 +34,13 @@ def create_session_file(quteproc, name, contents):
@bdd.when(bdd.parsers.parse('I replace "{pattern}" by "{replacement}" in the ' @bdd.when(bdd.parsers.parse('I replace "{pattern}" by "{replacement}" in the '
'"{name}" session file')) '"{name}" session file'))
def session_replace(quteproc, httpbin, pattern, replacement, name): def session_replace(quteproc, server, pattern, replacement, name):
# First wait until the session was actually saved # First wait until the session was actually saved
quteproc.wait_for(category='message', loglevel=logging.INFO, quteproc.wait_for(category='message', loglevel=logging.INFO,
message='Saved session {}.'.format(name)) message='Saved session {}.'.format(name))
filename = os.path.join(quteproc.basedir, 'data', 'sessions', filename = os.path.join(quteproc.basedir, 'data', 'sessions',
name + '.yml') name + '.yml')
replacement = replacement.replace('(port)', str(httpbin.port)) # yo dawg replacement = replacement.replace('(port)', str(server.port)) # yo dawg
with open(filename, 'r', encoding='utf-8') as f: with open(filename, 'r', encoding='utf-8') as f:
data = f.read() data = f.read()
with open(filename, 'w', encoding='utf-8') as f: with open(filename, 'w', encoding='utf-8') as f:

View File

@ -21,8 +21,6 @@ import pytest
import pytest_bdd as bdd import pytest_bdd as bdd
from PyQt5.QtCore import PYQT_VERSION
bdd.scenarios('yankpaste.feature') bdd.scenarios('yankpaste.feature')
@ -34,10 +32,6 @@ def init_fake_clipboard(quteproc):
@bdd.when(bdd.parsers.parse('I insert "{value}" into the text field')) @bdd.when(bdd.parsers.parse('I insert "{value}" into the text field'))
def set_text_field(request, quteproc, value): def set_text_field(quteproc, value):
if request.config.webengine and PYQT_VERSION >= 0x50700: quteproc.send_cmd(":jseval --world=0 set_text('{}')".format(value))
cmd = ":jseval --world=0 set_text('{}')".format(value)
else:
cmd = ":jseval set_text('{}')".format(value)
quteproc.send_cmd(cmd)
quteproc.wait_for_js('textarea set to: ' + value) quteproc.wait_for_js('textarea set to: ' + value)

View File

@ -415,10 +415,10 @@ class QuteProc(testprocess.Process):
if any(path.startswith(scheme) for scheme in special_schemes): if any(path.startswith(scheme) for scheme in special_schemes):
return path return path
else: else:
httpbin = self.request.getfixturevalue('httpbin') server = self.request.getfixturevalue('server')
return '{}://localhost:{}/{}'.format( return '{}://localhost:{}/{}'.format(
'https' if https else 'http', 'https' if https else 'http',
httpbin.port if port is None else port, server.port if port is None else port,
path if path != '/' else '') path if path != '/' else '')
def wait_for_js(self, message): def wait_for_js(self, message):
@ -778,7 +778,7 @@ def _xpath_escape(text):
@pytest.fixture(scope='module') @pytest.fixture(scope='module')
def quteproc_process(qapp, httpbin, request): def quteproc_process(qapp, server, request):
"""Fixture for qutebrowser process which is started once per file.""" """Fixture for qutebrowser process which is started once per file."""
# Passing request so it has an initial config # Passing request so it has an initial config
proc = QuteProc(request) proc = QuteProc(request)
@ -788,7 +788,7 @@ def quteproc_process(qapp, httpbin, request):
@pytest.fixture @pytest.fixture
def quteproc(quteproc_process, httpbin, request): def quteproc(quteproc_process, server, request):
"""Per-test qutebrowser fixture which uses the per-file process.""" """Per-test qutebrowser fixture which uses the per-file process."""
request.node._quteproc_log = quteproc_process.captured_log request.node._quteproc_log = quteproc_process.captured_log
quteproc_process.before_test() quteproc_process.before_test()
@ -798,7 +798,7 @@ def quteproc(quteproc_process, httpbin, request):
@pytest.fixture @pytest.fixture
def quteproc_new(qapp, httpbin, request): def quteproc_new(qapp, server, request):
"""Per-test qutebrowser process to test invocations.""" """Per-test qutebrowser process to test invocations."""
proc = QuteProc(request) proc = QuteProc(request)
request.node._quteproc_log = proc.captured_log request.node._quteproc_log = proc.captured_log

View File

@ -66,23 +66,23 @@ class FakeRequest:
"""Fake for request.""" """Fake for request."""
def __init__(self, node, config, httpbin): def __init__(self, node, config, server):
self.node = node self.node = node
self.config = config self.config = config
self._httpbin = httpbin self._server = server
def getfixturevalue(self, name): def getfixturevalue(self, name):
assert name == 'httpbin' assert name == 'server'
return self._httpbin return self._server
@pytest.fixture @pytest.fixture
def request_mock(quteproc, monkeypatch, httpbin): def request_mock(quteproc, monkeypatch, server):
"""Patch out a pytest request.""" """Patch out a pytest request."""
fake_call = FakeRepCall() fake_call = FakeRepCall()
fake_config = FakeConfig() fake_config = FakeConfig()
fake_node = FakeNode(fake_call) fake_node = FakeNode(fake_call)
fake_request = FakeRequest(fake_node, fake_config, httpbin) fake_request = FakeRequest(fake_node, fake_config, server)
assert not hasattr(fake_request.node.rep_call, 'wasxfail') assert not hasattr(fake_request.node.rep_call, 'wasxfail')
monkeypatch.setattr(quteproc, 'request', fake_request) monkeypatch.setattr(quteproc, 'request', fake_request)
return fake_request return fake_request

View File

@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Test the httpbin webserver used for tests.""" """Test the server webserver used for tests."""
import json import json
import urllib.request import urllib.request
@ -27,14 +27,14 @@ import pytest
@pytest.mark.parametrize('path, content, expected', [ @pytest.mark.parametrize('path, content, expected', [
('/', '<title>httpbin(1): HTTP Client Testing Service</title>', True), ('/', 'qutebrowser test webserver', True),
# https://github.com/Runscope/httpbin/issues/245 # https://github.com/Runscope/server/issues/245
('/', 'www.google-analytics.com', False), ('/', 'www.google-analytics.com', False),
('/data/hello.txt', 'Hello World!', True), ('/data/hello.txt', 'Hello World!', True),
]) ])
def test_httpbin(httpbin, qtbot, path, content, expected): def test_server(server, qtbot, path, content, expected):
with qtbot.waitSignal(httpbin.new_request, timeout=100): with qtbot.waitSignal(server.new_request, timeout=100):
url = 'http://localhost:{}{}'.format(httpbin.port, path) url = 'http://localhost:{}{}'.format(server.port, path)
try: try:
response = urllib.request.urlopen(url) response = urllib.request.urlopen(url)
except urllib.error.HTTPError as e: except urllib.error.HTTPError as e:
@ -47,7 +47,7 @@ def test_httpbin(httpbin, qtbot, path, content, expected):
data = response.read().decode('utf-8') data = response.read().decode('utf-8')
assert httpbin.get_requests() == [httpbin.ExpectedRequest('GET', path)] assert server.get_requests() == [server.ExpectedRequest('GET', path)]
assert (content in data) == expected assert (content in data) == expected
@ -58,7 +58,7 @@ def test_httpbin(httpbin, qtbot, path, content, expected):
({'verb': 'GET', 'path': '/', 'status': 200}, 'GET', '/foo', False), ({'verb': 'GET', 'path': '/', 'status': 200}, 'GET', '/foo', False),
({'verb': 'POST', 'path': '/', 'status': 200}, 'GET', '/', False), ({'verb': 'POST', 'path': '/', 'status': 200}, 'GET', '/', False),
]) ])
def test_expected_request(httpbin, line, verb, path, equal): def test_expected_request(server, line, verb, path, equal):
expected = httpbin.ExpectedRequest(verb, path) expected = server.ExpectedRequest(verb, path)
request = httpbin.Request(json.dumps(line)) request = server.Request(json.dumps(line))
assert (expected == request) == equal assert (expected == request) == equal

View File

@ -90,7 +90,7 @@ def _render_log(data, threshold=100):
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call): def pytest_runtest_makereport(item, call):
"""Add qutebrowser/httpbin sections to captured output if a test failed.""" """Add qutebrowser/server sections to captured output if a test failed."""
outcome = yield outcome = yield
if call.when not in ['call', 'teardown']: if call.when not in ['call', 'teardown']:
return return
@ -100,7 +100,7 @@ def pytest_runtest_makereport(item, call):
return return
quteproc_log = getattr(item, '_quteproc_log', None) quteproc_log = getattr(item, '_quteproc_log', None)
httpbin_log = getattr(item, '_httpbin_log', None) server_log = getattr(item, '_server_log', None)
if not hasattr(report.longrepr, 'addsection'): if not hasattr(report.longrepr, 'addsection'):
# In some conditions (on macOS and Windows it seems), report.longrepr # In some conditions (on macOS and Windows it seems), report.longrepr
@ -114,8 +114,8 @@ def pytest_runtest_makereport(item, call):
if quteproc_log is not None: if quteproc_log is not None:
report.longrepr.addsection("qutebrowser output", report.longrepr.addsection("qutebrowser output",
_render_log(quteproc_log)) _render_log(quteproc_log))
if httpbin_log is not None: if server_log is not None:
report.longrepr.addsection("httpbin output", _render_log(httpbin_log)) report.longrepr.addsection("server output", _render_log(server_log))
class Process(QObject): class Process(QObject):

View File

@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Fixtures for the httpbin webserver.""" """Fixtures for the server webserver."""
import re import re
import sys import sys
@ -35,7 +35,7 @@ from qutebrowser.utils import utils
class Request(testprocess.Line): class Request(testprocess.Line):
"""A parsed line from the httpbin/flask log output. """A parsed line from the flask log output.
Attributes: Attributes:
verb/path/status: Parsed from the log output. verb/path/status: Parsed from the log output.
@ -67,22 +67,20 @@ class Request(testprocess.Line):
'/favicon.ico': [http.client.NOT_FOUND], '/favicon.ico': [http.client.NOT_FOUND],
'/does-not-exist': [http.client.NOT_FOUND], '/does-not-exist': [http.client.NOT_FOUND],
'/does-not-exist-2': [http.client.NOT_FOUND], '/does-not-exist-2': [http.client.NOT_FOUND],
'/status/404': [http.client.NOT_FOUND], '/404': [http.client.NOT_FOUND],
'/custom/redirect-later': [http.client.FOUND], '/redirect-later': [http.client.FOUND],
'/custom/redirect-self': [http.client.FOUND], '/redirect-self': [http.client.FOUND],
'/redirect-to': [http.client.FOUND], '/redirect-to': [http.client.FOUND],
'/relative-redirect': [http.client.FOUND],
'/absolute-redirect': [http.client.FOUND],
'/cookies/set': [http.client.FOUND], '/cookies/set': [http.client.FOUND],
'/custom/500-inline': [http.client.INTERNAL_SERVER_ERROR], '/500-inline': [http.client.INTERNAL_SERVER_ERROR],
} }
for i in range(15): for i in range(15):
path_to_statuses['/redirect/{}'.format(i)] = [http.client.FOUND] path_to_statuses['/redirect/{}'.format(i)] = [http.client.FOUND]
path_to_statuses['/relative-redirect/{}'.format(i)] = [
http.client.FOUND]
path_to_statuses['/absolute-redirect/{}'.format(i)] = [
http.client.FOUND]
for suffix in ['', '1', '2', '3', '4', '5', '6']: for suffix in ['', '1', '2', '3', '4', '5', '6']:
key = '/basic-auth/user{}/password{}'.format(suffix, suffix) key = '/basic-auth/user{}/password{}'.format(suffix, suffix)
path_to_statuses[key] = [http.client.UNAUTHORIZED, http.client.OK] path_to_statuses[key] = [http.client.UNAUTHORIZED, http.client.OK]
@ -130,7 +128,7 @@ class ExpectedRequest:
class WebserverProcess(testprocess.Process): class WebserverProcess(testprocess.Process):
"""Abstraction over a running HTTPbin server process. """Abstraction over a running Flask server process.
Reads the log from its stdout and parses it. Reads the log from its stdout and parses it.
@ -186,31 +184,31 @@ class WebserverProcess(testprocess.Process):
@pytest.fixture(scope='session', autouse=True) @pytest.fixture(scope='session', autouse=True)
def httpbin(qapp): def server(qapp):
"""Fixture for an httpbin object which ensures clean setup/teardown.""" """Fixture for an server object which ensures clean setup/teardown."""
httpbin = WebserverProcess('webserver_sub') server = WebserverProcess('webserver_sub')
httpbin.start() server.start()
yield httpbin yield server
httpbin.cleanup() server.cleanup()
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def httpbin_after_test(httpbin, request): def server_after_test(server, request):
"""Fixture to clean httpbin request list after each test.""" """Fixture to clean server request list after each test."""
request.node._httpbin_log = httpbin.captured_log request.node._server_log = server.captured_log
yield yield
httpbin.after_test() server.after_test()
@pytest.fixture @pytest.fixture
def ssl_server(request, qapp): def ssl_server(request, qapp):
"""Fixture for a webserver with a self-signed SSL certificate. """Fixture for a webserver with a self-signed SSL certificate.
This needs to be explicitly used in a test, and overwrites the httpbin log This needs to be explicitly used in a test, and overwrites the server log
used in that test. used in that test.
""" """
server = WebserverProcess('webserver_sub_ssl') server = WebserverProcess('webserver_sub_ssl')
request.node._httpbin_log = server.captured_log request.node._server_log = server.captured_log
server.start() server.start()
yield server yield server
server.after_test() server.after_test()

View File

@ -17,9 +17,13 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>. # along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""httpbin web server for end2end tests. """Web server for end2end tests.
This script gets called as a QProcess from end2end/conftest.py. This script gets called as a QProcess from end2end/conftest.py.
Some of the handlers here are inspired by the server project, but simplified
for qutebrowser's needs. Note that it probably doesn't handle e.g. multiple
parameters or headers with the same name properly.
""" """
import sys import sys
@ -29,15 +33,20 @@ import signal
import os import os
import threading import threading
from httpbin.core import app
from httpbin.structures import CaseInsensitiveDict
import cheroot.wsgi import cheroot.wsgi
import flask import flask
app = flask.Flask(__name__)
_redirect_later_event = None _redirect_later_event = None
@app.route('/')
def root():
"""Show simple text."""
return flask.Response(b'qutebrowser test webserver, '
b'<a href="/user-agent">user agent</a>')
@app.route('/data/<path:path>') @app.route('/data/<path:path>')
def send_data(path): def send_data(path):
"""Send a given data file to qutebrowser. """Send a given data file to qutebrowser.
@ -57,15 +66,14 @@ def send_data(path):
return flask.send_from_directory(data_dir, path) return flask.send_from_directory(data_dir, path)
@app.route('/custom/redirect-later') @app.route('/redirect-later')
def redirect_later(): def redirect_later():
"""302 redirect to / after the given delay. """302 redirect to / after the given delay.
If delay is -1, wait until a request on redirect-later-continue is done. If delay is -1, wait until a request on redirect-later-continue is done.
""" """
global _redirect_later_event global _redirect_later_event
args = CaseInsensitiveDict(flask.request.args.items()) delay = int(flask.request.args.get('delay', '1'))
delay = int(args.get('delay', '1'))
if delay == -1: if delay == -1:
_redirect_later_event = threading.Event() _redirect_later_event = threading.Event()
ok = _redirect_later_event.wait(timeout=30 * 1000) ok = _redirect_later_event.wait(timeout=30 * 1000)
@ -77,7 +85,7 @@ def redirect_later():
return x return x
@app.route('/custom/redirect-later-continue') @app.route('/redirect-later-continue')
def redirect_later_continue(): def redirect_later_continue():
"""Continue a redirect-later request.""" """Continue a redirect-later request."""
if _redirect_later_event is None: if _redirect_later_event is None:
@ -87,7 +95,50 @@ def redirect_later_continue():
return flask.Response(b'Continued redirect.') return flask.Response(b'Continued redirect.')
@app.route('/custom/content-size') @app.route('/redirect-self')
def redirect_self():
"""302 Redirects to itself."""
return app.make_response(flask.redirect(flask.url_for('redirect_self')))
@app.route('/redirect/<int:n>')
def redirect_n_times(n):
"""302 Redirects n times."""
assert n > 0
return flask.redirect(flask.url_for('redirect_n_times', n=n-1))
@app.route('/relative-redirect')
def relative_redirect():
"""302 Redirect once."""
response = app.make_response('')
response.status_code = 302
response.headers['Location'] = flask.url_for('root')
return response
@app.route('/absolute-redirect')
def absolute_redirect():
"""302 Redirect once."""
response = app.make_response('')
response.status_code = 302
response.headers['Location'] = flask.url_for('root', _external=True)
return response
@app.route('/redirect-to')
def redirect_to():
"""302/3XX Redirects to the given URL."""
# We need to build the response manually and convert to UTF-8 to prevent
# werkzeug from "fixing" the URL. This endpoint should set the Location
# header to the exact string supplied.
response = app.make_response('')
response.status_code = 302
response.headers['Location'] = flask.request.args['url'].encode('utf-8')
return response
@app.route('/content-size')
def content_size(): def content_size():
"""Send two bytes of data without a content-size.""" """Send two bytes of data without a content-size."""
def generate_bytes(): def generate_bytes():
@ -102,7 +153,7 @@ def content_size():
return response return response
@app.route('/custom/twenty-mb') @app.route('/twenty-mb')
def twenty_mb(): def twenty_mb():
"""Send 20MB of data.""" """Send 20MB of data."""
def generate_bytes(): def generate_bytes():
@ -116,13 +167,7 @@ def twenty_mb():
return response return response
@app.route('/custom/redirect-self') @app.route('/500-inline')
def redirect_self():
"""302 Redirects to itself."""
return app.make_response(flask.redirect(flask.url_for('redirect_self')))
@app.route('/custom/500-inline')
def internal_error_attachment(): def internal_error_attachment():
"""A 500 error with Content-Disposition: inline.""" """A 500 error with Content-Disposition: inline."""
response = flask.Response(b"", headers={ response = flask.Response(b"", headers={
@ -133,6 +178,85 @@ def internal_error_attachment():
return response return response
@app.route('/cookies')
def view_cookies():
"""Show cookies."""
return flask.jsonify(cookies=flask.request.cookies)
@app.route('/cookies/set')
def set_cookies():
"""Set cookie(s) as provided by the query string."""
r = app.make_response(flask.redirect(flask.url_for('view_cookies')))
for key, value in flask.request.args.items():
r.set_cookie(key=key, value=value)
return r
@app.route('/basic-auth/<user>/<passwd>')
def basic_auth(user='user', passwd='passwd'):
"""Prompt the user for authorization using HTTP Basic Auth."""
auth = flask.request.authorization
if not auth or auth.username != user or auth.password != passwd:
r = flask.make_response()
r.status_code = 401
r.headers = {'WWW-Authenticate': 'Basic realm="Fake Realm"'}
return r
return flask.jsonify(authenticated=True, user=user)
@app.route('/drip')
def drip():
"""Drip data over a duration."""
duration = float(flask.request.args.get('duration'))
numbytes = int(flask.request.args.get('numbytes'))
pause = duration / numbytes
def generate_bytes():
for _ in range(numbytes):
yield u"*".encode('utf-8')
time.sleep(pause)
response = flask.Response(generate_bytes(), headers={
"Content-Type": "application/octet-stream",
"Content-Length": str(numbytes),
})
response.status_code = 200
return response
@app.route('/404')
def status_404():
r = flask.make_response()
r.status_code = 404
return r
@app.route('/headers')
def view_headers():
"""Return HTTP headers."""
return flask.jsonify(headers=dict(flask.request.headers))
@app.route('/response-headers')
def response_headers():
"""Return a set of response headers from the query string."""
headers = flask.request.args
response = flask.jsonify(headers)
response.headers.extend(headers)
response = flask.jsonify(dict(response.headers))
response.headers.extend(headers)
return response
@app.route('/user-agent')
def view_user_agent():
"""Return User-Agent."""
return flask.jsonify({'user-agent': flask.request.headers['user-agent']})
@app.after_request @app.after_request
def log_request(response): def log_request(response):
"""Log a webserver request.""" """Log a webserver request."""

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