Merge branch 'master' into referer-header

This commit is contained in:
Martin Tournoij 2015-06-27 20:43:54 +02:00
commit f806eefba6
80 changed files with 4138 additions and 1018 deletions

18
.appveyor.yml Normal file
View File

@ -0,0 +1,18 @@
shallow_clone: true
version: '{branch}-{build}'
cache: C:\Users\appveyor\pip\wheels
build: off
environment:
PYTHON: 'C:\Python34'
PYTHONUNBUFFERED: 1
install:
- C:\Python27\python -u scripts\ci_install.py
test_script:
- C:\Python34\Scripts\tox -e smoke
- C:\Python34\Scripts\tox -e smoke-frozen
- C:\Python34\Scripts\tox -e unittests
- C:\Python34\Scripts\tox -e unittests-frozen
- C:\Python34\Scripts\tox -e pyflakes
- C:\Python34\Scripts\tox -e pylint

View File

@ -4,7 +4,6 @@
ignore=resources.py ignore=resources.py
extension-pkg-whitelist=PyQt5,sip extension-pkg-whitelist=PyQt5,sip
load-plugins=pylint_checkers.config, load-plugins=pylint_checkers.config,
pylint_checkers.crlf,
pylint_checkers.modeline, pylint_checkers.modeline,
pylint_checkers.openencoding, pylint_checkers.openencoding,
pylint_checkers.settrace pylint_checkers.settrace
@ -28,7 +27,8 @@ disable=no-self-use,
broad-except, broad-except,
bare-except, bare-except,
eval-used, eval-used,
exec-used exec-used,
file-ignored
[BASIC] [BASIC]
module-rgx=(__)?[a-z][a-z0-9_]*(__)?$ module-rgx=(__)?[a-z][a-z0-9_]*(__)?$

28
.travis.yml Normal file
View File

@ -0,0 +1,28 @@
dist: trusty
os:
- linux
- osx
# Not really, but this is here so we can do stuff by hand.
language: c
install:
- python scripts/ci_install.py
script:
- xvfb-run -s "-screen 0 640x480x16" tox -e unittests,smoke
- tox -e misc
- tox -e pep257
- tox -e pyflakes
- tox -e pep8
- tox -e mccabe
- tox -e pylint
- tox -e pyroma
- tox -e check-manifest
# Travis bug - OS X builds get routed to Ubuntu Trusty if "dist: trusty" is
# given.
matrix:
allow_failures:
- os: osx

View File

@ -21,34 +21,44 @@ Added
~~~~~ ~~~~~
- New commands `:message-info`, `:message-error` and `:message-warning` to show messages in the statusbar, e.g. from an userscript. - New commands `:message-info`, `:message-error` and `:message-warning` to show messages in the statusbar, e.g. from an userscript.
- There are now some example userscripts in `misc/userscripts`.
- New command `:scroll-px` which replaces `:scroll` for pixel-exact scrolling. - New command `:scroll-px` which replaces `:scroll` for pixel-exact scrolling.
- New command `:jseval` to run a javascript snippet on the current page.
- New (hidden) command `:follow-selected` (bound to `Enter`/`Ctrl-Enter` by default) to follow the link which is currently selected (e.g. after searching via `/`).
- New (hidden) command `:clear-keychain` to clear a partially entered keychain (bound to `<Escape>` by default, in addition to clearing search).
- New setting `ui -> smooth-scrolling`. - New setting `ui -> smooth-scrolling`.
- New setting `content -> webgl` to enable/disable https://www.khronos.org/webgl/[WebGL]. - New setting `content -> webgl` to enable/disable https://www.khronos.org/webgl/[WebGL].
- New setting `content -> css-regions` to enable/disable support for http://dev.w3.org/csswg/css-regions/[CSS Regions]. - New setting `content -> css-regions` to enable/disable support for http://dev.w3.org/csswg/css-regions/[CSS Regions].
- New setting `content -> hyperlink-auditing` to enable/disable support for https://html.spec.whatwg.org/multipage/semantics.html#hyperlink-auditing[hyperlink auditing]. - New setting `content -> hyperlink-auditing` to enable/disable support for https://html.spec.whatwg.org/multipage/semantics.html#hyperlink-auditing[hyperlink auditing].
- Support for Qt 5.5 and tox 2.0 - New setting `tabs -> mousewheel-tab-switching` to control mousewheel behavior on the tab bar.
- New arguments `--datadir` and `--cachedir` to set the data/cache location. - New arguments `--datadir` and `--cachedir` to set the data/cache location.
- New arguments `--basedir` and `--temp-basedir` (intended for debugging) to set a different base directory for all data, which allows multiple invocations. - New arguments `--basedir` and `--temp-basedir` (intended for debugging) to set a different base directory for all data, which allows multiple invocations.
- New argument `--no-err-windows` to suppress all error windows. - New argument `--no-err-windows` to suppress all error windows.
- New visual/caret mode (bound to `v`) to select text by keyboard.
- New setting `tabs -> mousewheel-tab-switching` to control mousewheel behavior on the tab bar.
- New arguments `--top-navigate` and `--bottom-navigate` (`-t`/`-b`) for `:scroll-page` to specify a navigation action (e.g. automatically go to the next page when arriving at the bottom). - New arguments `--top-navigate` and `--bottom-navigate` (`-t`/`-b`) for `:scroll-page` to specify a navigation action (e.g. automatically go to the next page when arriving at the bottom).
- New flag `-d`/`--detach` for `:spawn` to detach the spawned process so it's not closed when qutebrowser is. - New flag `-d`/`--detach` for `:spawn` to detach the spawned process so it's not closed when qutebrowser is.
- New (hidden) command `:follow-selected` (bound to `Enter`/`Ctrl-Enter` by default) to follow the link which is currently selected (e.g. after searching via `/`). - New flag `-v`/`--verbose` for `:spawn` to print informations when the process started/exited successfully.
- Many new color settings (foreground setting for every background setting).
- New setting `ui -> modal-js-dialog` to use the standard modal dialogs for javascript questions instead of using the statusbar. - New setting `ui -> modal-js-dialog` to use the standard modal dialogs for javascript questions instead of using the statusbar.
- New setting `colors -> webpage.bg` to set the background color to use for websites which don't set one. - New setting `colors -> webpage.bg` to set the background color to use for websites which don't set one.
- New setting `completion -> auto-open` to only open the completion when tab is pressed (if set to false).
- New visual/caret mode (bound to `v`) to select text by keyboard.
- There are now some example userscripts in `misc/userscripts`.
- Support for Qt 5.5 and tox 2.0
Changed Changed
~~~~~~~ ~~~~~~~
- `QUTE_HTML` and `QUTE_TEXT` for userscripts now don't store the contents directly, and instead contain a filename. - *Breaking change for userscripts:* `QUTE_HTML` and `QUTE_TEXT` for userscripts now don't store the contents directly, and instead contain a filename.
- `:spawn` now shows the command being executed in the statusbar, use `-q`/`--quiet` for the old behavior.
- The `content -> geolocation` and `notifications` settings now support a `true` value to always allow those. However, this is *not recommended*. - The `content -> geolocation` and `notifications` settings now support a `true` value to always allow those. However, this is *not recommended*.
- New bindings `<Ctrl-R>` (rapid), `<Ctrl-F>` (foreground) and `<Ctrl-B>` (background) to switch hint modes while hinting. - New bindings `<Ctrl-R>` (rapid), `<Ctrl-F>` (foreground) and `<Ctrl-B>` (background) to switch hint modes while hinting.
- `<Ctrl-M>` and numpad-enter are now bound by default for bindings where `<Return>` was bound. - `<Ctrl-M>` and numpad-enter are now bound by default for bindings where `<Return>` was bound.
- `:hint tab` and `F` now respect the `background-tabs` setting. To enforce a foreground tab (what `F` did before), use `:hint tab-fg` or `;f`. - `:hint tab` and `F` now respect the `background-tabs` setting. To enforce a foreground tab (what `F` did before), use `:hint tab-fg` or `;f`.
- `:scroll` now takes a direction argument (`up`/`down`/`left`/`right`/`top`/`bottom`/`page-up`/`page-down`) instead of two pixel arguments (`dx`/`dy`). The old form still works but is deprecated. - `:scroll` now takes a direction argument (`up`/`down`/`left`/`right`/`top`/`bottom`/`page-up`/`page-down`) instead of two pixel arguments (`dx`/`dy`). The old form still works but is deprecated.
- The `ui -> user-stylesheet` setting now also takes file paths relative to the config directory.
- The `content -> cookies-accept` setting now has new `no-3rdparty` (default) and `no-unknown-3rdparty` values to block third-party cookies. The `default` value got renamed to `all`.
- Improved startup time by reading the webpage history while qutebrowser is open.
- The way `:spawn` splits its commandline has been changed slightly to allow commands with flags.
- The default for the `new-instance-open-target` setting has been changed to `tab`.
- Sessions now store zoom/scroll-position separately for each entry.
Deprecated Deprecated
~~~~~~~~~~ ~~~~~~~~~~
@ -59,28 +69,28 @@ Removed
~~~~~~~ ~~~~~~~
- The `--no-crash-dialog` argument which was intended for debugging only was removed as it's replaced by `--no-err-windows` which suppresses all error windows. - The `--no-crash-dialog` argument which was intended for debugging only was removed as it's replaced by `--no-err-windows` which suppresses all error windows.
- Support for Qt installations without SSL support was dropped.
Fixed Fixed
~~~~~ ~~~~~
- Scrolling should now work more reliably on some pages where arrow keys worked but `hjkl` didn't. - Scrolling should now work more reliably on some pages where arrow keys worked but `hjkl` didn't.
- Small improvements when checking if an input is an URL or not. - Small improvements when checking if an input is an URL or not.
- Fixed wrong cursor position when completing the first item in the completion.
v0.2.2 (unreleased) - Fixed exception when using search engines with {foo} in their name.
------------------- - Fixed a bug where the same title was shown for all tabs on some systems.
- Don't install the scripts package when installing qutebrowser.
Fixed
~~~~~
- Fixed searching for terms starting with a hyphen (e.g. `/-foo`) - Fixed searching for terms starting with a hyphen (e.g. `/-foo`)
- Proxy authentication credentials are now remembered between different tabs. - Proxy authentication credentials are now remembered between different tabs.
- Fixed updating of the tab title on pages without title. - Fixed updating of the tab title on pages without title.
- Fixed AssertionError when closing many windows quickly. - Fixed AssertionError when closing many windows quickly.
- Various fixes for deprecated key bindings and auto-migrations. - Various fixes for deprecated key bindings and auto-migrations.
- Workaround for qutebrowser not starting when there are NUL-bytes in the history (because of a currently unknown bug) - Workaround for qutebrowser not starting when there are NUL-bytes in the history (because of a currently unknown bug).
- Fixed handling of keybindings containing Ctrl/Meta on OS X. - Fixed handling of keybindings containing Ctrl/Meta on OS X.
- Fixed crash when downloading an URL without filename (e.g. magnet links) via "Save as...". - Fixed crash when downloading an URL without filename (e.g. magnet links) via "Save as...".
- Fixed exception when starting qutebrowser with `:set` as argument. - Fixed exception when starting qutebrowser with `:set` as argument.
- Fixed horrible completion performance when the `shrink` option was set.
- Sessions now store zoom/scroll-position correctly.
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.1[v0.2.1] https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.1[v0.2.1]
----------------------------------------------------------------------- -----------------------------------------------------------------------

View File

@ -153,7 +153,7 @@ Useful websites
Some resources which might be handy: Some resources which might be handy:
* http://qt-project.org/doc/qt-5/classes.html[The Qt5 reference] * http://doc.qt.io/qt-5/classes.html[The Qt5 reference]
* https://docs.python.org/3/library/index.html[The Python reference] * https://docs.python.org/3/library/index.html[The Python reference]
* http://httpbin.org/[httpbin, a test service for HTTP requests/responses] * http://httpbin.org/[httpbin, a test service for HTTP requests/responses]
* http://requestb.in/[RequestBin, a service to inspect HTTP requests] * http://requestb.in/[RequestBin, a service to inspect HTTP requests]
@ -211,8 +211,7 @@ Other
Languages] (http://www.rfc-editor.org/errata_search.php?rfc=5646[Errata]) Languages] (http://www.rfc-editor.org/errata_search.php?rfc=5646[Errata])
* http://www.w3.org/TR/CSS2/[Cascading Style Sheets Level 2 Revision 1 (CSS * http://www.w3.org/TR/CSS2/[Cascading Style Sheets Level 2 Revision 1 (CSS
2.1) Specification] 2.1) Specification]
* http://qt-project.org/doc/qt-4.8/stylesheet-reference.html[Qt Style Sheets * http://doc.qt.io/qt-5/stylesheet-reference.html[Qt Style Sheets Reference]
Reference]
* http://mimesniff.spec.whatwg.org/[MIME Sniffing Standard] * http://mimesniff.spec.whatwg.org/[MIME Sniffing Standard]
* http://spec.whatwg.org/[WHATWG specifications] * http://spec.whatwg.org/[WHATWG specifications]
* http://www.w3.org/html/wg/drafts/html/master/Overview.html[HTML 5.1 Nightly] * http://www.w3.org/html/wg/drafts/html/master/Overview.html[HTML 5.1 Nightly]
@ -238,9 +237,7 @@ There are some exceptions to that:
* `QThread` is used instead of Python threads because it provides signals and * `QThread` is used instead of Python threads because it provides signals and
slots. slots.
* `QProcess` is used instead of Python's `subprocess` if certain actions (e.g. * `QProcess` is used instead of Python's `subprocess`
cleanup) when the process finished are desired, as it provides signals for
that.
* `QUrl` is used instead of storing URLs as string, see the * `QUrl` is used instead of storing URLs as string, see the
<<handling-urls,handling URLs>> section for details. <<handling-urls,handling URLs>> section for details.
@ -295,8 +292,8 @@ All objects can be printed by starting with the `--debug` flag and using the
The registry is mainly used for <<commands,command handlers>> but also can be The registry is mainly used for <<commands,command handlers>> but also can be
useful in places where using Qt's useful in places where using Qt's
http://qt-project.org/doc/qt-5/signalsandslots.html[signals and slots] http://doc.qt.io/qt-5/signalsandslots.html[signals and slots] mechanism would
mechanism would be difficult. be difficult.
Logging Logging
~~~~~~~ ~~~~~~~
@ -541,7 +538,7 @@ New Qt release
* Run all tests and check nothing is broken. * Run all tests and check nothing is broken.
* Check the * Check the
https://bugreports.qt-project.org/issues/?jql=reporter%20%3D%20%22The%20Compiler%22%20ORDER%20BY%20fixVersion%20ASC[Qt bugtracker] https://bugreports.qt.io/issues/?jql=reporter%20%3D%20%22The%20Compiler%22%20ORDER%20BY%20fixVersion%20ASC[Qt bugtracker]
and make sure all bugs marked as resolved are actually fixed. and make sure all bugs marked as resolved are actually fixed.
* Update own PKGBUILDs based on upstream Archlinux updates and rebuild. * Update own PKGBUILDs based on upstream Archlinux updates and rebuild.
* Update recommended Qt version in `README` * Update recommended Qt version in `README`

View File

@ -4,8 +4,8 @@ The Compiler <mail@qutebrowser.org>
[qanda] [qanda]
What is qutebrowser based on?:: What is qutebrowser based on?::
qutebrowser uses http://www.python.org/[Python], http://qt-project.org/[Qt] qutebrowser uses http://www.python.org/[Python], http://qt.io/[Qt] and
and http://www.riverbankcomputing.com/software/pyqt/intro[PyQt]. http://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
+ +
The concept of it is largely inspired by http://portix.bitbucket.org/dwb/[dwb] The concept of it is largely inspired by http://portix.bitbucket.org/dwb/[dwb]
and http://www.vimperator.org/vimperator[Vimperator]. Many actions and and http://www.vimperator.org/vimperator[Vimperator]. Many actions and
@ -15,7 +15,7 @@ Why another browser?::
It might be hard to believe, but I didn't find any browser which I was It might be hard to believe, but I didn't find any browser which I was
happy with, so I started to write my own. Also, I needed a project to get happy with, so I started to write my own. Also, I needed a project to get
into writing GUI applications with Python and into writing GUI applications with Python and
link:http://qt-project.org/[Qt]/link:http://www.riverbankcomputing.com/software/pyqt/intro[PyQt]. link:http://qt.io/[Qt]/link:http://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
+ +
Read the next few questions to find out why I was unhappy with existing Read the next few questions to find out why I was unhappy with existing
software. software.
@ -32,12 +32,11 @@ API] seems to lack basic features like proxy support, and almost no projects
seem to have started porting to WebKit2 (I only know of seem to have started porting to WebKit2 (I only know of
http://www.uzbl.org/[uzbl]). http://www.uzbl.org/[uzbl]).
+ +
qutebrowser uses http://qt-project.org/[Qt] and qutebrowser uses http://qt.io/[Qt] and http://wiki.qt.io/QtWebKit[QtWebKit]
http://qt-project.org/wiki/QtWebKit[QtWebKit] instead, which suffers from far instead, which suffers from far less such crashes. It might switch to
less such crashes. It might switch to http://wiki.qt.io/QtWebEngine[QtWebEngine] in the future, which is based on
http://qt-project.org/wiki/QtWebEngine[QtWebEngine] in the future, which is Google's https://en.wikipedia.org/wiki/Blink_(layout_engine)[Blink] rendering
based on Google's https://en.wikipedia.org/wiki/Blink_(layout_engine)[Blink] engine.
rendering engine.
What's wrong with https://www.mozilla.org/en-US/firefox/new/[Firefox] and link:http://5digits.org/pentadactyl/[Pentadactyl]/link:http://www.vimperator.org/vimperator[Vimperator]?:: What's wrong with https://www.mozilla.org/en-US/firefox/new/[Firefox] and link:http://5digits.org/pentadactyl/[Pentadactyl]/link:http://www.vimperator.org/vimperator[Vimperator]?::
Firefox likes to break compatibility with addons on each upgrade, gets Firefox likes to break compatibility with addons on each upgrade, gets
@ -54,10 +53,10 @@ What's wrong with http://www.chromium.org/Home[Chromium] and https://vimium.gith
Why Python?:: Why Python?::
I enjoy writing Python since 2011, which made it one of the possible I enjoy writing Python since 2011, which made it one of the possible
choices. I wanted to use http://qt-project.org/[Qt] because of choices. I wanted to use http://qt.io/[Qt] because of
http://qt-project.org/wiki/QtWebKit[QtWebKit] so I didn't have http://wiki.qt.io/QtWebKit[QtWebKit] so I didn't have
http://qt-project.org/wiki/Category:LanguageBindings[many other choices]. I http://wiki.qt.io/Category:LanguageBindings[many other choices]. I don't
don't like C++ and can't write it very well, so that wasn't an alternative. like C++ and can't write it very well, so that wasn't an alternative.
But isn't Python too slow for a browser?:: But isn't Python too slow for a browser?::
http://www.infoworld.com/d/application-development/van-rossum-python-not-too-slow-188715[No.] http://www.infoworld.com/d/application-development/van-rossum-python-not-too-slow-188715[No.]

View File

@ -1,12 +1,13 @@
global-exclude __pycache__ *.pyc *.pyo global-exclude __pycache__ *.pyc *.pyo
recursive-include qutebrowser *.py
recursive-include qutebrowser/html *.html recursive-include qutebrowser/html *.html
recursive-include qutebrowser/test *.py recursive-include qutebrowser/test *.py
recursive-include qutebrowser/javascript *.js recursive-include qutebrowser/javascript *.js
graft icons graft icons
graft scripts/pylint_checkers
graft doc/img graft doc/img
graft misc graft misc
graft scripts
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 COPYING doc/* README.asciidoc CONTRIBUTING.asciidoc FAQ.asciidoc INSTALL.asciidoc CHANGELOG.asciidoc
@ -15,13 +16,15 @@ include requirements.txt
include tox.ini include tox.ini
include qutebrowser.py include qutebrowser.py
exclude scripts/cleanup.py include scripts/__init__.py
exclude scripts/minimal_webkit_testbrowser.py include scripts/hostblock_blame.py
exclude scripts/run_profile.py include scripts/importer.py
exclude scripts/src2asciidoc.sh include scripts/keytester.py
exclude scripts/gen_resources.sh include scripts/link_pyqt.py
exclude scripts/quit_segfault_test.sh include scripts/minimal_webkit_testbrowser.py
exclude scripts/segfault_test.sh include scripts/setupcommon.py
include scripts/utils.py
exclude doc/notes exclude doc/notes
recursive-exclude doc *.asciidoc recursive-exclude doc *.asciidoc
include doc/qutebrowser.1.asciidoc include doc/qutebrowser.1.asciidoc
@ -31,3 +34,6 @@ exclude .coveragerc
exclude .pylintrc exclude .pylintrc
exclude .eslintrc exclude .eslintrc
exclude doc/help exclude doc/help
exclude .appveyor.yml
exclude .travis.yml
exclude misc/appveyor_install.py

View File

@ -68,7 +68,7 @@ Contributions / Bugs
-------------------- --------------------
You want to contribute to qutebrowser? Awesome! Please read You want to contribute to qutebrowser? Awesome! Please read
link:doc/CONTRIBUTING.asciidoc[the contribution guidelines] for details and link: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
@ -89,10 +89,10 @@ 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 * http://www.python.org/[Python] 3.4
* http://qt-project.org/[Qt] 5.2.0 or newer (5.4.1 recommended) * http://qt.io/[Qt] 5.2.0 or newer (5.4.2 recommended)
* QtWebKit * QtWebKit
* http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.2.0 or newer * http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.2.0 or newer
(5.4.1 recommended) for Python 3 (5.4.2 recommended) for Python 3
* 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]
@ -137,10 +137,12 @@ Contributors, sorted by the number of commits in descending order:
* Bruno Oliveira * Bruno Oliveira
* Raphael Pierzina * Raphael Pierzina
* Joel Torstensson * Joel Torstensson
* Martin Tournoij
* Claude * Claude
* Lamar Pavel
* Austin Anderson
* Artur Shaik * Artur Shaik
* Antoni Boucher * Antoni Boucher
* Martin Tournoij
* ZDarian * ZDarian
* Peter Vilim * Peter Vilim
* John ShaggyTwoDope Jenkins * John ShaggyTwoDope Jenkins
@ -159,6 +161,7 @@ Contributors, sorted by the number of commits in descending order:
* Mathias Fussenegger * Mathias Fussenegger
* Larry Hynes * Larry Hynes
* Fritz V155 Reichwald * Fritz V155 Reichwald
* Franz Fellner
* error800 * error800
* Thorsten Wißmann * Thorsten Wißmann
* Thiago Barroso Perrotta * Thiago Barroso Perrotta
@ -166,7 +169,6 @@ Contributors, sorted by the number of commits in descending order:
* Helen Sherwood-Taylor * Helen Sherwood-Taylor
* HalosGhost * HalosGhost
* Gregor Pohl * Gregor Pohl
* Franz Fellner
* Eivind Uggedal * Eivind Uggedal
* Andreas Fischer * Andreas Fischer
// QUTE_AUTHORS_END // QUTE_AUTHORS_END
@ -220,7 +222,7 @@ Also, thanks to:
* Everyone who had the patience to test qutebrowser before v0.1. * Everyone who had the patience to test qutebrowser before v0.1.
* Everyone triaging/fixing my bugs in the * Everyone triaging/fixing my bugs in the
https://bugreports.qt-project.org/secure/Dashboard.jspa[Qt bugtracker] https://bugreports.qt.io/secure/Dashboard.jspa[Qt bugtracker]
* Everyone answering my questions on http://stackoverflow.com/[Stack Overflow] * Everyone answering my questions on http://stackoverflow.com/[Stack Overflow]
and in IRC. and in IRC.
* All the projects which were a great help while developing qutebrowser. * All the projects which were a great help while developing qutebrowser.

View File

@ -20,6 +20,7 @@
|<<hint,hint>>|Start hinting. |<<hint,hint>>|Start hinting.
|<<home,home>>|Open main startpage in current tab. |<<home,home>>|Open main startpage in current tab.
|<<inspector,inspector>>|Toggle the web inspector. |<<inspector,inspector>>|Toggle the web inspector.
|<<jseval,jseval>>|Evaluate a JavaScript string.
|<<later,later>>|Execute a command after some time. |<<later,later>>|Execute a command after some time.
|<<navigate,navigate>>|Open typical prev/next links or navigate using the URL path. |<<navigate,navigate>>|Open typical prev/next links or navigate using the URL path.
|<<open,open>>|Open a URL in the current/[count]th tab. |<<open,open>>|Open a URL in the current/[count]th tab.
@ -241,6 +242,22 @@ Open main startpage in current tab.
=== inspector === inspector
Toggle the web inspector. Toggle the web inspector.
[[jseval]]
=== jseval
Syntax: +:jseval [*--quiet*] 'js-code'+
Evaluate a JavaScript string.
==== positional arguments
* +'js-code'+: The string to evaluate.
==== optional arguments
* +*-q*+, +*--quiet*+: Don't show resulting JS object.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
[[later]] [[later]]
=== later === later
Syntax: +:later 'ms' 'command'+ Syntax: +:later 'ms' 'command'+
@ -512,18 +529,23 @@ Preset the statusbar to some text.
[[spawn]] [[spawn]]
=== spawn === spawn
Syntax: +:spawn [*--userscript*] [*--quiet*] 'args' ['args' ...]+ Syntax: +:spawn [*--userscript*] [*--verbose*] [*--detach*] 'cmdline'+
Spawn a command in a shell. Spawn a command in a shell.
Note the {url} variable which gets replaced by the current URL might be useful here. Note the {url} variable which gets replaced by the current URL might be useful here.
==== positional arguments ==== positional arguments
* +'args'+: The commandline to execute. * +'cmdline'+: The commandline to execute.
==== optional arguments ==== optional arguments
* +*-u*+, +*--userscript*+: Run the command as an userscript. * +*-u*+, +*--userscript*+: Run the command as an userscript.
* +*-q*+, +*--quiet*+: Don't print the commandline being executed. * +*-v*+, +*--verbose*+: Show notifications when the command started/exited.
* +*-d*+, +*--detach*+: Whether the command should be detached from qutebrowser.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
* With this command, +;;+ is interpreted literally instead of splitting off a second command.
[[stop]] [[stop]]
=== stop === stop
@ -642,13 +664,14 @@ Save open pages and quit.
[[yank]] [[yank]]
=== yank === yank
Syntax: +:yank [*--title*] [*--sel*]+ Syntax: +:yank [*--title*] [*--sel*] [*--domain*]+
Yank the current URL/title to the clipboard or primary selection. Yank the current URL/title to the clipboard or primary selection.
==== optional arguments ==== optional arguments
* +*-t*+, +*--title*+: Yank the title instead of the URL. * +*-t*+, +*--title*+: Yank the title instead of the URL.
* +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard. * +*-s*+, +*--sel*+: Use the primary selection instead of the clipboard.
* +*-d*+, +*--domain*+: Yank only the scheme, domain, and port number.
[[zoom]] [[zoom]]
=== zoom === zoom
@ -684,6 +707,7 @@ How many steps to zoom out.
[options="header",width="75%",cols="25%,75%"] [options="header",width="75%",cols="25%,75%"]
|============== |==============
|Command|Description |Command|Description
|<<clear-keychain,clear-keychain>>|Clear the currently entered key chain.
|<<command-accept,command-accept>>|Execute the command currently in the commandline. |<<command-accept,command-accept>>|Execute the command currently in the commandline.
|<<command-history-next,command-history-next>>|Go forward in the commandline history. |<<command-history-next,command-history-next>>|Go forward in the commandline history.
|<<command-history-prev,command-history-prev>>|Go back in the commandline history. |<<command-history-prev,command-history-prev>>|Go back in the commandline history.
@ -738,6 +762,10 @@ How many steps to zoom out.
|<<toggle-selection,toggle-selection>>|Toggle caret selection mode. |<<toggle-selection,toggle-selection>>|Toggle caret selection mode.
|<<yank-selected,yank-selected>>|Yank the selected text to the clipboard or primary selection. |<<yank-selected,yank-selected>>|Yank the selected text to the clipboard or primary selection.
|============== |==============
[[clear-keychain]]
=== clear-keychain
Clear the currently entered key chain.
[[command-accept]] [[command-accept]]
=== command-accept === command-accept
Execute the command currently in the commandline. Execute the command currently in the commandline.

View File

@ -38,7 +38,7 @@
|<<ui-display-statusbar-messages,display-statusbar-messages>>|Whether to display javascript statusbar messages. |<<ui-display-statusbar-messages,display-statusbar-messages>>|Whether to display javascript statusbar messages.
|<<ui-zoom-text-only,zoom-text-only>>|Whether the zoom factor on a frame applies only to the text or to all content. |<<ui-zoom-text-only,zoom-text-only>>|Whether the zoom factor on a frame applies only to the text or to all content.
|<<ui-frame-flattening,frame-flattening>>|Whether to expand each subframe to its contents. |<<ui-frame-flattening,frame-flattening>>|Whether to expand each subframe to its contents.
|<<ui-user-stylesheet,user-stylesheet>>|User stylesheet to use (absolute filename or CSS string). Will expand environment variables. |<<ui-user-stylesheet,user-stylesheet>>|User stylesheet to use (absolute filename, filename relative to the config directory or CSS string). Will expand environment variables.
|<<ui-css-media-type,css-media-type>>|Set the CSS media type. |<<ui-css-media-type,css-media-type>>|Set the CSS media type.
|<<ui-smooth-scrolling,smooth-scrolling>>|Whether to enable smooth scrolling for webpages. |<<ui-smooth-scrolling,smooth-scrolling>>|Whether to enable smooth scrolling for webpages.
|<<ui-remove-finished-downloads,remove-finished-downloads>>|Whether to remove finished downloads automatically. |<<ui-remove-finished-downloads,remove-finished-downloads>>|Whether to remove finished downloads automatically.
@ -65,6 +65,7 @@
[options="header",width="75%",cols="25%,75%"] [options="header",width="75%",cols="25%,75%"]
|============== |==============
|Setting|Description |Setting|Description
|<<completion-auto-open,auto-open>>|Automatically open completion when typing.
|<<completion-download-path-suggestion,download-path-suggestion>>|What to display in the download filename input. |<<completion-download-path-suggestion,download-path-suggestion>>|What to display in the download filename input.
|<<completion-timestamp-format,timestamp-format>>|How to format timestamps (e.g. for history) |<<completion-timestamp-format,timestamp-format>>|How to format timestamps (e.g. for history)
|<<completion-show,show>>|Whether to show the autocompletion window. |<<completion-show,show>>|Whether to show the autocompletion window.
@ -99,7 +100,7 @@
|<<tabs-select-on-remove,select-on-remove>>|Which tab to select when the focused tab is removed. |<<tabs-select-on-remove,select-on-remove>>|Which tab to select when the focused tab is removed.
|<<tabs-new-tab-position,new-tab-position>>|How new tabs are positioned. |<<tabs-new-tab-position,new-tab-position>>|How new tabs are positioned.
|<<tabs-new-tab-position-explicit,new-tab-position-explicit>>|How new tabs opened explicitly are positioned. |<<tabs-new-tab-position-explicit,new-tab-position-explicit>>|How new tabs opened explicitly are positioned.
|<<tabs-last-close,last-close>>|Behaviour when the last tab is closed. |<<tabs-last-close,last-close>>|Behavior when the last tab is closed.
|<<tabs-hide-auto,hide-auto>>|Hide the tab bar if only one tab is open. |<<tabs-hide-auto,hide-auto>>|Hide the tab bar if only one tab is open.
|<<tabs-hide-always,hide-always>>|Always hide the tab bar. |<<tabs-hide-always,hide-always>>|Always hide the tab bar.
|<<tabs-wrap,wrap>>|Whether to wrap when changing tabs. |<<tabs-wrap,wrap>>|Whether to wrap when changing tabs.
@ -149,7 +150,7 @@
|<<content-ignore-javascript-alert,ignore-javascript-alert>>|Whether all javascript alerts should be ignored. |<<content-ignore-javascript-alert,ignore-javascript-alert>>|Whether all javascript alerts should be ignored.
|<<content-local-content-can-access-remote-urls,local-content-can-access-remote-urls>>|Whether locally loaded documents are allowed to access remote urls. |<<content-local-content-can-access-remote-urls,local-content-can-access-remote-urls>>|Whether locally loaded documents are allowed to access remote urls.
|<<content-local-content-can-access-file-urls,local-content-can-access-file-urls>>|Whether locally loaded documents are allowed to access other local urls. |<<content-local-content-can-access-file-urls,local-content-can-access-file-urls>>|Whether locally loaded documents are allowed to access other local urls.
|<<content-cookies-accept,cookies-accept>>|Whether to accept cookies. |<<content-cookies-accept,cookies-accept>>|Control which cookies to accept.
|<<content-cookies-store,cookies-store>>|Whether to store cookies. |<<content-cookies-store,cookies-store>>|Whether to store cookies.
|<<content-host-block-lists,host-block-lists>>|List of URLs of lists which contain hosts to block. |<<content-host-block-lists,host-block-lists>>|List of URLs of lists which contain hosts to block.
|<<content-host-blocking-enabled,host-blocking-enabled>>|Whether host blocking is enabled. |<<content-host-blocking-enabled,host-blocking-enabled>>|Whether host blocking is enabled.
@ -187,13 +188,21 @@
|<<colors-completion.item.selected.border.top,completion.item.selected.border.top>>|Top border color of the completion widget category headers. |<<colors-completion.item.selected.border.top,completion.item.selected.border.top>>|Top border color of the completion widget category headers.
|<<colors-completion.item.selected.border.bottom,completion.item.selected.border.bottom>>|Bottom border color of the selected completion item. |<<colors-completion.item.selected.border.bottom,completion.item.selected.border.bottom>>|Bottom border color of the selected completion item.
|<<colors-completion.match.fg,completion.match.fg>>|Foreground color of the matched text in the completion. |<<colors-completion.match.fg,completion.match.fg>>|Foreground color of the matched text in the completion.
|<<colors-statusbar.bg,statusbar.bg>>|Foreground color of the statusbar.
|<<colors-statusbar.fg,statusbar.fg>>|Foreground color of the statusbar. |<<colors-statusbar.fg,statusbar.fg>>|Foreground color of the statusbar.
|<<colors-statusbar.bg,statusbar.bg>>|Foreground color of the statusbar.
|<<colors-statusbar.fg.error,statusbar.fg.error>>|Foreground color of the statusbar if there was an error.
|<<colors-statusbar.bg.error,statusbar.bg.error>>|Background color of the statusbar if there was an error. |<<colors-statusbar.bg.error,statusbar.bg.error>>|Background color of the statusbar if there was an error.
|<<colors-statusbar.fg.warning,statusbar.fg.warning>>|Foreground color of the statusbar if there is a warning.
|<<colors-statusbar.bg.warning,statusbar.bg.warning>>|Background color of the statusbar if there is a warning. |<<colors-statusbar.bg.warning,statusbar.bg.warning>>|Background color of the statusbar if there is a warning.
|<<colors-statusbar.fg.prompt,statusbar.fg.prompt>>|Foreground color of the statusbar if there is a prompt.
|<<colors-statusbar.bg.prompt,statusbar.bg.prompt>>|Background color of the statusbar if there is a prompt. |<<colors-statusbar.bg.prompt,statusbar.bg.prompt>>|Background color of the statusbar if there is a prompt.
|<<colors-statusbar.fg.insert,statusbar.fg.insert>>|Foreground color of the statusbar in insert mode.
|<<colors-statusbar.bg.insert,statusbar.bg.insert>>|Background color of the statusbar in insert mode. |<<colors-statusbar.bg.insert,statusbar.bg.insert>>|Background color of the statusbar in insert mode.
|<<colors-statusbar.fg.command,statusbar.fg.command>>|Foreground color of the statusbar in command mode.
|<<colors-statusbar.bg.command,statusbar.bg.command>>|Background color of the statusbar in command mode.
|<<colors-statusbar.fg.caret,statusbar.fg.caret>>|Foreground color of the statusbar in caret mode.
|<<colors-statusbar.bg.caret,statusbar.bg.caret>>|Background color of the statusbar in caret mode. |<<colors-statusbar.bg.caret,statusbar.bg.caret>>|Background color of the statusbar in caret mode.
|<<colors-statusbar.fg.caret-selection,statusbar.fg.caret-selection>>|Foreground color of the statusbar in caret mode with a selection
|<<colors-statusbar.bg.caret-selection,statusbar.bg.caret-selection>>|Background color of the statusbar in caret mode with a selection |<<colors-statusbar.bg.caret-selection,statusbar.bg.caret-selection>>|Background color of the statusbar in caret mode with a selection
|<<colors-statusbar.progress.bg,statusbar.progress.bg>>|Background color of the progress bar. |<<colors-statusbar.progress.bg,statusbar.progress.bg>>|Background color of the progress bar.
|<<colors-statusbar.url.fg,statusbar.url.fg>>|Default foreground color of the URL in the statusbar. |<<colors-statusbar.url.fg,statusbar.url.fg>>|Default foreground color of the URL in the statusbar.
@ -202,10 +211,10 @@
|<<colors-statusbar.url.fg.warn,statusbar.url.fg.warn>>|Foreground color of the URL in the statusbar when there's a warning. |<<colors-statusbar.url.fg.warn,statusbar.url.fg.warn>>|Foreground color of the URL in the statusbar when there's a warning.
|<<colors-statusbar.url.fg.hover,statusbar.url.fg.hover>>|Foreground color of the URL in the statusbar for hovered links. |<<colors-statusbar.url.fg.hover,statusbar.url.fg.hover>>|Foreground color of the URL in the statusbar for hovered links.
|<<colors-tabs.fg.odd,tabs.fg.odd>>|Foreground color of unselected odd tabs. |<<colors-tabs.fg.odd,tabs.fg.odd>>|Foreground color of unselected odd tabs.
|<<colors-tabs.fg.even,tabs.fg.even>>|Foreground color of unselected even tabs.
|<<colors-tabs.fg.selected,tabs.fg.selected>>|Foreground color of selected tabs.
|<<colors-tabs.bg.odd,tabs.bg.odd>>|Background color of unselected odd tabs. |<<colors-tabs.bg.odd,tabs.bg.odd>>|Background color of unselected odd tabs.
|<<colors-tabs.fg.even,tabs.fg.even>>|Foreground color of unselected even tabs.
|<<colors-tabs.bg.even,tabs.bg.even>>|Background color of unselected even tabs. |<<colors-tabs.bg.even,tabs.bg.even>>|Background color of unselected even tabs.
|<<colors-tabs.fg.selected,tabs.fg.selected>>|Foreground color of selected tabs.
|<<colors-tabs.bg.selected,tabs.bg.selected>>|Background color of selected tabs. |<<colors-tabs.bg.selected,tabs.bg.selected>>|Background color of selected tabs.
|<<colors-tabs.bg.bar,tabs.bg.bar>>|Background color of the tab bar. |<<colors-tabs.bg.bar,tabs.bg.bar>>|Background color of the tab bar.
|<<colors-tabs.indicator.start,tabs.indicator.start>>|Color gradient start for the tab indicator. |<<colors-tabs.indicator.start,tabs.indicator.start>>|Color gradient start for the tab indicator.
@ -213,13 +222,16 @@
|<<colors-tabs.indicator.error,tabs.indicator.error>>|Color for the tab indicator on errors.. |<<colors-tabs.indicator.error,tabs.indicator.error>>|Color for the tab indicator on errors..
|<<colors-tabs.indicator.system,tabs.indicator.system>>|Color gradient interpolation system for the tab indicator. |<<colors-tabs.indicator.system,tabs.indicator.system>>|Color gradient interpolation system for the tab indicator.
|<<colors-hints.fg,hints.fg>>|Font color for hints. |<<colors-hints.fg,hints.fg>>|Font color for hints.
|<<colors-hints.fg.match,hints.fg.match>>|Font color for the matched part of hints.
|<<colors-hints.bg,hints.bg>>|Background color for hints. |<<colors-hints.bg,hints.bg>>|Background color for hints.
|<<colors-downloads.fg,downloads.fg>>|Foreground color for downloads. |<<colors-hints.fg.match,hints.fg.match>>|Font color for the matched part of hints.
|<<colors-downloads.bg.bar,downloads.bg.bar>>|Background color for the download bar. |<<colors-downloads.bg.bar,downloads.bg.bar>>|Background color for the download bar.
|<<colors-downloads.bg.start,downloads.bg.start>>|Color gradient start for downloads. |<<colors-downloads.fg.start,downloads.fg.start>>|Color gradient start for download text.
|<<colors-downloads.bg.stop,downloads.bg.stop>>|Color gradient end for downloads. |<<colors-downloads.bg.start,downloads.bg.start>>|Color gradient start for download backgrounds.
|<<colors-downloads.bg.system,downloads.bg.system>>|Color gradient interpolation system for downloads. |<<colors-downloads.fg.stop,downloads.fg.stop>>|Color gradient end for download text.
|<<colors-downloads.bg.stop,downloads.bg.stop>>|Color gradient stop for download backgrounds.
|<<colors-downloads.fg.system,downloads.fg.system>>|Color gradient interpolation system for download text.
|<<colors-downloads.bg.system,downloads.bg.system>>|Color gradient interpolation system for download backgrounds.
|<<colors-downloads.fg.error,downloads.fg.error>>|Foreground color for downloads with errors.
|<<colors-downloads.bg.error,downloads.bg.error>>|Background color for downloads with errors. |<<colors-downloads.bg.error,downloads.bg.error>>|Background color for downloads with errors.
|<<colors-webpage.bg,webpage.bg>>|Background color for webpages if unset (or empty to use the theme's color) |<<colors-webpage.bg,webpage.bg>>|Background color for webpages if unset (or empty to use the theme's color)
|============== |==============
@ -407,7 +419,7 @@ Valid values:
* +tab-bg-silent+: Open a new background tab in the existing window without activating the window. * +tab-bg-silent+: Open a new background tab in the existing window without activating the window.
* +window+: Open in a new window. * +window+: Open in a new window.
Default: +pass:[window]+ Default: +pass:[tab]+
[[general-log-javascript-console]] [[general-log-javascript-console]]
=== log-javascript-console === log-javascript-console
@ -530,7 +542,7 @@ Default: +pass:[false]+
[[ui-user-stylesheet]] [[ui-user-stylesheet]]
=== user-stylesheet === user-stylesheet
User stylesheet to use (absolute filename or CSS string). Will expand environment variables. User stylesheet to use (absolute filename, filename relative to the config directory or CSS string). Will expand environment variables.
Default: +pass:[::-webkit-scrollbar { width: 0px; height: 0px; }]+ Default: +pass:[::-webkit-scrollbar { width: 0px; height: 0px; }]+
@ -683,6 +695,17 @@ Default: +pass:[true]+
== completion == completion
Options related to completion and command history. Options related to completion and command history.
[[completion-auto-open]]
=== auto-open
Automatically open completion when typing.
Valid values:
* +true+
* +false+
Default: +pass:[true]+
[[completion-download-path-suggestion]] [[completion-download-path-suggestion]]
=== download-path-suggestion === download-path-suggestion
What to display in the download filename input. What to display in the download filename input.
@ -911,7 +934,7 @@ Default: +pass:[last]+
[[tabs-last-close]] [[tabs-last-close]]
=== last-close === last-close
Behaviour when the last tab is closed. Behavior when the last tab is closed.
Valid values: Valid values:
@ -1316,14 +1339,16 @@ Default: +pass:[true]+
[[content-cookies-accept]] [[content-cookies-accept]]
=== cookies-accept === cookies-accept
Whether to accept cookies. Control which cookies to accept.
Valid values: Valid values:
* +default+: Default QtWebKit behavior. * +all+: Accept all cookies.
* +no-3rdparty+: Accept cookies from the same origin only.
* +no-unknown-3rdparty+: Accept cookies from the same origin only, unless a cookie is already set for the domain.
* +never+: Don't accept cookies at all. * +never+: Don't accept cookies at all.
Default: +pass:[default]+ Default: +pass:[no-3rdparty]+
[[content-cookies-store]] [[content-cookies-store]]
=== cookies-store === cookies-store
@ -1434,7 +1459,7 @@ Default: +pass:[true]+
=== next-regexes === next-regexes
A comma-separated list of regexes to use for 'next' links. A comma-separated list of regexes to use for 'next' links.
Default: +pass:[\bnext\b,\bmore\b,\bnewer\b,\b[&gt;→≫]\b,\b(&gt;&gt;|»)\b]+ Default: +pass:[\bnext\b,\bmore\b,\bnewer\b,\b[&gt;→≫]\b,\b(&gt;&gt;|»)\b,\bcontinue\b]+
[[hints-prev-regexes]] [[hints-prev-regexes]]
=== prev-regexes === prev-regexes
@ -1461,7 +1486,9 @@ A value can be in one of the following format:
* transparent (no color) * transparent (no color)
* `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or percentages) * `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or percentages)
* `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359) * `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359)
* A gradient as explained in http://qt-project.org/doc/qt-4.8/stylesheet-reference.html#list-of-property-types[the Qt documentation] under ``Gradient''. * A gradient as explained in http://doc.qt.io/qt-5/stylesheet-reference.html#list-of-property-types[the Qt documentation] under ``Gradient''.
A *.system value determines the color system to use for color interpolation between similarly-named *.start and *.stop entries, regardless of how they are defined in the options. Valid values are 'rgb', 'hsv', and 'hsl'.
The `hints.*` values are a special case as they're real CSS colors, not Qt-CSS colors. There, for a gradient, you need to use `-webkit-gradient`, see https://www.webkit.org/blog/175/introducing-css-gradients/[the WebKit documentation]. The `hints.*` values are a special case as they're real CSS colors, not Qt-CSS colors. There, for a gradient, you need to use `-webkit-gradient`, see https://www.webkit.org/blog/175/introducing-css-gradients/[the WebKit documentation].
@ -1537,17 +1564,23 @@ Foreground color of the matched text in the completion.
Default: +pass:[#ff4444]+ Default: +pass:[#ff4444]+
[[colors-statusbar.fg]]
=== statusbar.fg
Foreground color of the statusbar.
Default: +pass:[white]+
[[colors-statusbar.bg]] [[colors-statusbar.bg]]
=== statusbar.bg === statusbar.bg
Foreground color of the statusbar. Foreground color of the statusbar.
Default: +pass:[black]+ Default: +pass:[black]+
[[colors-statusbar.fg]] [[colors-statusbar.fg.error]]
=== statusbar.fg === statusbar.fg.error
Foreground color of the statusbar. Foreground color of the statusbar if there was an error.
Default: +pass:[white]+ Default: +pass:[${statusbar.fg}]+
[[colors-statusbar.bg.error]] [[colors-statusbar.bg.error]]
=== statusbar.bg.error === statusbar.bg.error
@ -1555,30 +1588,72 @@ Background color of the statusbar if there was an error.
Default: +pass:[red]+ Default: +pass:[red]+
[[colors-statusbar.fg.warning]]
=== statusbar.fg.warning
Foreground color of the statusbar if there is a warning.
Default: +pass:[${statusbar.fg}]+
[[colors-statusbar.bg.warning]] [[colors-statusbar.bg.warning]]
=== statusbar.bg.warning === statusbar.bg.warning
Background color of the statusbar if there is a warning. Background color of the statusbar if there is a warning.
Default: +pass:[darkorange]+ Default: +pass:[darkorange]+
[[colors-statusbar.fg.prompt]]
=== statusbar.fg.prompt
Foreground color of the statusbar if there is a prompt.
Default: +pass:[${statusbar.fg}]+
[[colors-statusbar.bg.prompt]] [[colors-statusbar.bg.prompt]]
=== statusbar.bg.prompt === statusbar.bg.prompt
Background color of the statusbar if there is a prompt. Background color of the statusbar if there is a prompt.
Default: +pass:[darkblue]+ Default: +pass:[darkblue]+
[[colors-statusbar.fg.insert]]
=== statusbar.fg.insert
Foreground color of the statusbar in insert mode.
Default: +pass:[${statusbar.fg}]+
[[colors-statusbar.bg.insert]] [[colors-statusbar.bg.insert]]
=== statusbar.bg.insert === statusbar.bg.insert
Background color of the statusbar in insert mode. Background color of the statusbar in insert mode.
Default: +pass:[darkgreen]+ Default: +pass:[darkgreen]+
[[colors-statusbar.fg.command]]
=== statusbar.fg.command
Foreground color of the statusbar in command mode.
Default: +pass:[${statusbar.fg}]+
[[colors-statusbar.bg.command]]
=== statusbar.bg.command
Background color of the statusbar in command mode.
Default: +pass:[${statusbar.bg}]+
[[colors-statusbar.fg.caret]]
=== statusbar.fg.caret
Foreground color of the statusbar in caret mode.
Default: +pass:[${statusbar.fg}]+
[[colors-statusbar.bg.caret]] [[colors-statusbar.bg.caret]]
=== statusbar.bg.caret === statusbar.bg.caret
Background color of the statusbar in caret mode. Background color of the statusbar in caret mode.
Default: +pass:[purple]+ Default: +pass:[purple]+
[[colors-statusbar.fg.caret-selection]]
=== statusbar.fg.caret-selection
Foreground color of the statusbar in caret mode with a selection
Default: +pass:[${statusbar.fg}]+
[[colors-statusbar.bg.caret-selection]] [[colors-statusbar.bg.caret-selection]]
=== statusbar.bg.caret-selection === statusbar.bg.caret-selection
Background color of the statusbar in caret mode with a selection Background color of the statusbar in caret mode with a selection
@ -1627,30 +1702,30 @@ Foreground color of unselected odd tabs.
Default: +pass:[white]+ Default: +pass:[white]+
[[colors-tabs.fg.even]]
=== tabs.fg.even
Foreground color of unselected even tabs.
Default: +pass:[white]+
[[colors-tabs.fg.selected]]
=== tabs.fg.selected
Foreground color of selected tabs.
Default: +pass:[white]+
[[colors-tabs.bg.odd]] [[colors-tabs.bg.odd]]
=== tabs.bg.odd === tabs.bg.odd
Background color of unselected odd tabs. Background color of unselected odd tabs.
Default: +pass:[grey]+ Default: +pass:[grey]+
[[colors-tabs.fg.even]]
=== tabs.fg.even
Foreground color of unselected even tabs.
Default: +pass:[white]+
[[colors-tabs.bg.even]] [[colors-tabs.bg.even]]
=== tabs.bg.even === tabs.bg.even
Background color of unselected even tabs. Background color of unselected even tabs.
Default: +pass:[darkgrey]+ Default: +pass:[darkgrey]+
[[colors-tabs.fg.selected]]
=== tabs.fg.selected
Foreground color of selected tabs.
Default: +pass:[white]+
[[colors-tabs.bg.selected]] [[colors-tabs.bg.selected]]
=== tabs.bg.selected === tabs.bg.selected
Background color of selected tabs. Background color of selected tabs.
@ -1699,23 +1774,17 @@ Font color for hints.
Default: +pass:[black]+ Default: +pass:[black]+
[[colors-hints.fg.match]]
=== hints.fg.match
Font color for the matched part of hints.
Default: +pass:[green]+
[[colors-hints.bg]] [[colors-hints.bg]]
=== hints.bg === hints.bg
Background color for hints. Background color for hints.
Default: +pass:[-webkit-gradient(linear, left top, left bottom, color-stop(0%,#FFF785), color-stop(100%,#FFC542))]+ Default: +pass:[-webkit-gradient(linear, left top, left bottom, color-stop(0%,#FFF785), color-stop(100%,#FFC542))]+
[[colors-downloads.fg]] [[colors-hints.fg.match]]
=== downloads.fg === hints.fg.match
Foreground color for downloads. Font color for the matched part of hints.
Default: +pass:[#ffffff]+ Default: +pass:[green]+
[[colors-downloads.bg.bar]] [[colors-downloads.bg.bar]]
=== downloads.bg.bar === downloads.bg.bar
@ -1723,21 +1792,33 @@ Background color for the download bar.
Default: +pass:[black]+ Default: +pass:[black]+
[[colors-downloads.fg.start]]
=== downloads.fg.start
Color gradient start for download text.
Default: +pass:[white]+
[[colors-downloads.bg.start]] [[colors-downloads.bg.start]]
=== downloads.bg.start === downloads.bg.start
Color gradient start for downloads. Color gradient start for download backgrounds.
Default: +pass:[#0000aa]+ Default: +pass:[#0000aa]+
[[colors-downloads.fg.stop]]
=== downloads.fg.stop
Color gradient end for download text.
Default: +pass:[${downloads.fg.start}]+
[[colors-downloads.bg.stop]] [[colors-downloads.bg.stop]]
=== downloads.bg.stop === downloads.bg.stop
Color gradient end for downloads. Color gradient stop for download backgrounds.
Default: +pass:[#00aa00]+ Default: +pass:[#00aa00]+
[[colors-downloads.bg.system]] [[colors-downloads.fg.system]]
=== downloads.bg.system === downloads.fg.system
Color gradient interpolation system for downloads. Color gradient interpolation system for download text.
Valid values: Valid values:
@ -1747,6 +1828,24 @@ Valid values:
Default: +pass:[rgb]+ Default: +pass:[rgb]+
[[colors-downloads.bg.system]]
=== downloads.bg.system
Color gradient interpolation system for download backgrounds.
Valid values:
* +rgb+: Interpolate in the RGB color system.
* +hsv+: Interpolate in the HSV color system.
* +hsl+: Interpolate in the HSL color system.
Default: +pass:[rgb]+
[[colors-downloads.fg.error]]
=== downloads.fg.error
Foreground color for downloads with errors.
Default: +pass:[white]+
[[colors-downloads.bg.error]] [[colors-downloads.bg.error]]
=== downloads.bg.error === downloads.bg.error
Background color for downloads with errors. Background color for downloads with errors.

View File

@ -50,7 +50,7 @@ from qutebrowser.mainwindow import mainwindow
from qutebrowser.misc import readline, ipc, savemanager, sessions, crashsignal from qutebrowser.misc import readline, ipc, savemanager, sessions, crashsignal
from qutebrowser.misc import utilcmds # pylint: disable=unused-import from qutebrowser.misc import utilcmds # pylint: disable=unused-import
from qutebrowser.utils import (log, version, message, utils, qtutils, urlutils, from qutebrowser.utils import (log, version, message, utils, qtutils, urlutils,
objreg, usertypes, standarddir, error) objreg, usertypes, standarddir, error, debug)
# We import utilcmds to run the cmdutils.register decorators. # We import utilcmds to run the cmdutils.register decorators.
@ -61,7 +61,7 @@ def run(args):
"""Initialize everthing and run the application.""" """Initialize everthing and run the application."""
# pylint: disable=too-many-statements # pylint: disable=too-many-statements
if args.version: if args.version:
print(version.version()) print(version.version(short=True))
print() print()
print() print()
print(qutebrowser.__copyright__) print(qutebrowser.__copyright__)
@ -148,7 +148,9 @@ def init(args, crash_handler):
error.handle_fatal_exc(e, args, "Error while initializing!", error.handle_fatal_exc(e, args, "Error while initializing!",
pre_text="Error while initializing") pre_text="Error while initializing")
sys.exit(usertypes.Exit.err_init) sys.exit(usertypes.Exit.err_init)
QTimer.singleShot(0, functools.partial(_process_args, args)) QTimer.singleShot(0, functools.partial(_process_args, args))
QTimer.singleShot(10, functools.partial(_init_late_modules, args))
log.init.debug("Initializing eventfilter...") log.init.debug("Initializing eventfilter...")
event_filter = EventFilter(qApp) event_filter = EventFilter(qApp)
@ -428,6 +430,23 @@ def _init_modules(args, crash_handler):
objreg.get('config').changed.connect(_maybe_hide_mouse_cursor) objreg.get('config').changed.connect(_maybe_hide_mouse_cursor)
def _init_late_modules(args):
"""Initialize modules which can be inited after the window is shown."""
try:
log.init.debug("Reading web history...")
reader = objreg.get('web-history').async_read()
with debug.log_time(log.init, 'Reading history'):
while True:
QApplication.processEvents()
next(reader)
except StopIteration:
pass
except (OSError, UnicodeDecodeError) as e:
error.handle_fatal_exc(e, args, "Error while initializing!",
pre_text="Error while initializing")
sys.exit(usertypes.Exit.err_init)
class Quitter: class Quitter:
"""Utility class to quit/restart the QApplication. """Utility class to quit/restart the QApplication.

View File

@ -22,7 +22,6 @@
import re import re
import os import os
import shlex import shlex
import subprocess
import posixpath import posixpath
import functools import functools
import xml.etree.ElementTree import xml.etree.ElementTree
@ -37,14 +36,14 @@ import pygments
import pygments.lexers import pygments.lexers
import pygments.formatters import pygments.formatters
from qutebrowser.commands import userscripts, cmdexc, cmdutils from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
from qutebrowser.config import config, configexc from qutebrowser.config import config, configexc
from qutebrowser.browser import webelem, inspector from qutebrowser.browser import webelem, inspector
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) objreg, utils)
from qutebrowser.utils.usertypes import KeyMode from qutebrowser.utils.usertypes import KeyMode
from qutebrowser.misc import editor from qutebrowser.misc import editor, guiprocess
class CommandDispatcher: class CommandDispatcher:
@ -696,19 +695,28 @@ class CommandDispatcher:
frame.scroll(dx, dy) frame.scroll(dx, dy)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
def yank(self, title=False, sel=False): def yank(self, title=False, sel=False, domain=False):
"""Yank the current URL/title to the clipboard or primary selection. """Yank the current URL/title to the clipboard or primary selection.
Args: Args:
sel: Use the primary selection instead of the clipboard. sel: Use the primary selection instead of the clipboard.
title: Yank the title instead of the URL. title: Yank the title instead of the URL.
domain: Yank only the scheme, domain, and port number.
""" """
clipboard = QApplication.clipboard() clipboard = QApplication.clipboard()
if title: if title:
s = self._tabbed_browser.page_title(self._current_index()) s = self._tabbed_browser.page_title(self._current_index())
what = 'title'
elif domain:
port = self._current_url().port()
s = '{}://{}{}'.format(self._current_url().scheme(),
self._current_url().host(),
':' + str(port) if port > -1 else '')
what = 'domain'
else: else:
s = self._current_url().toString( s = self._current_url().toString(
QUrl.FullyEncoded | QUrl.RemovePassword) QUrl.FullyEncoded | QUrl.RemovePassword)
what = 'URL'
if sel and clipboard.supportsSelection(): if sel and clipboard.supportsSelection():
mode = QClipboard.Selection mode = QClipboard.Selection
target = "primary selection" target = "primary selection"
@ -717,8 +725,8 @@ class CommandDispatcher:
target = "clipboard" target = "clipboard"
log.misc.debug("Yanking to {}: '{}'".format(target, s)) log.misc.debug("Yanking to {}: '{}'".format(target, s))
clipboard.setText(s, mode) clipboard.setText(s, mode)
what = 'Title' if title else 'URL' message.info(self._win_id, "Yanked {} to {}: {}".format(
message.info(self._win_id, "{} yanked to {}".format(what, target)) what, target, s))
@cmdutils.register(instance='command-dispatcher', scope='window', @cmdutils.register(instance='command-dispatcher', scope='window',
count='count') count='count')
@ -729,7 +737,11 @@ class CommandDispatcher:
count: How many steps to zoom in. count: How many steps to zoom in.
""" """
tab = self._current_widget() tab = self._current_widget()
tab.zoom(count) try:
perc = tab.zoom(count)
except ValueError as e:
raise cmdexc.CommandError(e)
message.info(self._win_id, "Zoom level: {}%".format(perc))
@cmdutils.register(instance='command-dispatcher', scope='window', @cmdutils.register(instance='command-dispatcher', scope='window',
count='count') count='count')
@ -740,7 +752,11 @@ class CommandDispatcher:
count: How many steps to zoom out. count: How many steps to zoom out.
""" """
tab = self._current_widget() tab = self._current_widget()
tab.zoom(-count) try:
perc = tab.zoom(-count)
except ValueError as e:
raise cmdexc.CommandError(e)
message.info(self._win_id, "Zoom level: {}%".format(perc))
@cmdutils.register(instance='command-dispatcher', scope='window', @cmdutils.register(instance='command-dispatcher', scope='window',
count='count') count='count')
@ -760,7 +776,12 @@ class CommandDispatcher:
except ValueError as e: except ValueError as e:
raise cmdexc.CommandError(e) raise cmdexc.CommandError(e)
tab = self._current_widget() tab = self._current_widget()
tab.zoom_perc(level)
try:
tab.zoom_perc(level)
except ValueError as e:
raise cmdexc.CommandError(e)
message.info(self._win_id, "Zoom level: {}%".format(level))
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
def tab_only(self, left=False, right=False): def tab_only(self, left=False, right=False):
@ -913,38 +934,39 @@ class CommandDispatcher:
self._tabbed_browser.setUpdatesEnabled(True) self._tabbed_browser.setUpdatesEnabled(True)
@cmdutils.register(instance='command-dispatcher', scope='window', @cmdutils.register(instance='command-dispatcher', scope='window',
win_id='win_id') maxsplit=0)
def spawn(self, win_id, userscript=False, quiet=False, *args): def spawn(self, cmdline, userscript=False, verbose=False, detach=False):
"""Spawn a command in a shell. """Spawn a command in a shell.
Note the {url} variable which gets replaced by the current URL might be Note the {url} variable which gets replaced by the current URL might be
useful here. useful here.
//
We use subprocess rather than Qt's QProcess here because we really
don't care about the process anymore as soon as it's spawned.
Args: Args:
userscript: Run the command as an userscript. userscript: Run the command as an userscript.
quiet: Don't print the commandline being executed. verbose: Show notifications when the command started/exited.
*args: The commandline to execute. detach: Whether the command should be detached from qutebrowser.
cmdline: The commandline to execute.
""" """
log.procs.debug("Executing: {}, userscript={}".format( try:
args, userscript)) cmd, *args = shlex.split(cmdline)
if not quiet: except ValueError as e:
fake_cmdline = ' '.join(shlex.quote(arg) for arg in args) raise cmdexc.CommandError("Error while splitting command: "
message.info(win_id, 'Executing: ' + fake_cmdline) "{}".format(e))
args = runners.replace_variables(self._win_id, args)
log.procs.debug("Executing {} with args {}, userscript={}".format(
cmd, args, userscript))
if userscript: if userscript:
cmd = args[0] self.run_userscript(cmd, *args, verbose=verbose)
args = [] if not args else args[1:]
self.run_userscript(cmd, *args)
else: else:
try: proc = guiprocess.GUIProcess(self._win_id, what='command',
subprocess.Popen(args) verbose=verbose,
except OSError as e: parent=self._tabbed_browser)
raise cmdexc.CommandError("Error while spawning command: " if detach:
"{}".format(e)) proc.start_detached(cmd, args)
else:
proc.start(cmd, args)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
def home(self): def home(self):
@ -953,12 +975,13 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window', @cmdutils.register(instance='command-dispatcher', scope='window',
deprecated='Use :spawn --userscript instead!') deprecated='Use :spawn --userscript instead!')
def run_userscript(self, cmd, *args: {'nargs': '*'}): def run_userscript(self, cmd, *args: {'nargs': '*'}, verbose=False):
"""Run an userscript given as argument. """Run an userscript given as argument.
Args: Args:
cmd: The userscript to run. cmd: The userscript to run.
args: Arguments to pass to the userscript. args: Arguments to pass to the userscript.
verbose: Show notifications when the command started/exited.
""" """
cmd = os.path.expanduser(cmd) cmd = os.path.expanduser(cmd)
env = { env = {
@ -986,7 +1009,8 @@ class CommandDispatcher:
env['QUTE_URL'] = url.toString(QUrl.FullyEncoded) env['QUTE_URL'] = url.toString(QUrl.FullyEncoded)
env.update(userscripts.store_source(mainframe)) env.update(userscripts.store_source(mainframe))
userscripts.run(cmd, *args, win_id=self._win_id, env=env) userscripts.run(cmd, *args, win_id=self._win_id, env=env,
verbose=verbose)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
def quickmark_save(self): def quickmark_save(self):
@ -1157,12 +1181,6 @@ class CommandDispatcher:
The editor which should be launched can be configured via the The editor which should be launched can be configured via the
`general -> editor` config option. `general -> editor` config option.
//
We use QProcess rather than subprocess here because it makes it a lot
easier to execute some code as soon as the process has been finished
and do everything async.
""" """
frame = self._current_widget().page().currentFrame() frame = self._current_widget().page().currentFrame()
try: try:
@ -1184,7 +1202,7 @@ class CommandDispatcher:
def on_editing_finished(self, elem, text): def on_editing_finished(self, elem, text):
"""Write the editor text into the form field and clean up tempfile. """Write the editor text into the form field and clean up tempfile.
Callback for QProcess when the editor was closed. Callback for GUIProcess when the editor was closed.
Args: Args:
elem: The WebElementWrapper which was modified. elem: The WebElementWrapper which was modified.
@ -1567,3 +1585,33 @@ class CommandDispatcher:
view = self._current_widget() view = self._current_widget()
for _ in range(count): for _ in range(count):
view.triggerPageAction(member) view.triggerPageAction(member)
@cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0, no_cmd_split=True)
def jseval(self, js_code, quiet=False):
"""Evaluate a JavaScript string.
Args:
js_code: The string to evaluate.
quiet: Don't show resulting JS object.
"""
frame = self._current_widget().page().mainFrame()
out = frame.evaluateJavaScript(js_code)
if quiet:
return
if out is None:
# Getting the actual error (if any) seems to be difficult. The
# error does end up in BrowserPage.javaScriptConsoleMessage(), but
# distinguishing between :jseval errors and errors from the webpage
# is not trivial...
message.info(self._win_id, 'No output or error')
else:
# The output can be a string, number, dict, array, etc. But *don't*
# output too much data, as this will make qutebrowser hang
out = str(out)
if len(out) > 5000:
message.info(self._win_id, out[:5000] + ' [...trimmed...]')
else:
message.info(self._win_id, out)

View File

@ -84,7 +84,7 @@ class CookieJar(RAMCookieJar):
def purge_old_cookies(self): def purge_old_cookies(self):
"""Purge expired cookies from the cookie jar.""" """Purge expired cookies from the cookie jar."""
# Based on: # Based on:
# http://qt-project.org/doc/qt-5/qtwebkitexamples-webkitwidgets-browser-cookiejar-cpp.html # http://doc.qt.io/qt-5/qtwebkitexamples-webkitwidgets-browser-cookiejar-cpp.html
now = QDateTime.currentDateTime() now = QDateTime.currentDateTime()
cookies = [c for c in self.allCookies() cookies = [c for c in self.allCookies()
if c.isSessionCookie() or c.expirationDate() >= now] if c.isSessionCookie() or c.expirationDate() >= now]

View File

@ -148,7 +148,7 @@ class DownloadItemStats(QObject):
@pyqtSlot(int, int) @pyqtSlot(int, int)
def on_download_progress(self, bytes_done, bytes_total): def on_download_progress(self, bytes_done, bytes_total):
"""Upload local variables when the download progress changed. """Update local variables when the download progress changed.
Args: Args:
bytes_done: How many bytes are downloaded. bytes_done: How many bytes are downloaded.
@ -158,7 +158,6 @@ class DownloadItemStats(QObject):
bytes_total = None bytes_total = None
self.done = bytes_done self.done = bytes_done
self.total = bytes_total self.total = bytes_total
self.updated.emit()
class DownloadItem(QObject): class DownloadItem(QObject):
@ -356,12 +355,19 @@ class DownloadItem(QObject):
if reply.error() != QNetworkReply.NoError: if reply.error() != QNetworkReply.NoError:
QTimer.singleShot(0, lambda: self.error.emit(reply.errorString())) QTimer.singleShot(0, lambda: self.error.emit(reply.errorString()))
def bg_color(self): def get_status_color(self, position):
"""Background color to be shown.""" """Choose an appropriate color for presenting the download's status.
start = config.get('colors', 'downloads.bg.start')
stop = config.get('colors', 'downloads.bg.stop') Args:
system = config.get('colors', 'downloads.bg.system') position: The color type requested, can be 'fg' or 'bg'.
error = config.get('colors', 'downloads.bg.error') """
# pylint: disable=bad-config-call
# WORKAROUND for https://bitbucket.org/logilab/astroid/issue/104/
assert position in ("fg", "bg")
start = config.get('colors', 'downloads.{}.start'.format(position))
stop = config.get('colors', 'downloads.{}.stop'.format(position))
system = config.get('colors', 'downloads.{}.system'.format(position))
error = config.get('colors', 'downloads.{}.error'.format(position))
if self.error_msg is not None: if self.error_msg is not None:
assert not self.successful assert not self.successful
return error return error
@ -679,7 +685,7 @@ class DownloadManager(QAbstractListModel):
if fileobj is not None and filename is not None: if fileobj is not None and filename is not None:
raise TypeError("Only one of fileobj/filename may be given!") raise TypeError("Only one of fileobj/filename may be given!")
# WORKAROUND for Qt corrupting data loaded from cache: # WORKAROUND for Qt corrupting data loaded from cache:
# https://bugreports.qt-project.org/browse/QTBUG-42757 # https://bugreports.qt.io/browse/QTBUG-42757
request.setAttribute(QNetworkRequest.CacheLoadControlAttribute, request.setAttribute(QNetworkRequest.CacheLoadControlAttribute,
QNetworkRequest.AlwaysNetwork) QNetworkRequest.AlwaysNetwork)
suggested_fn = urlutils.filename_from_url(request.url()) suggested_fn = urlutils.filename_from_url(request.url())
@ -1023,9 +1029,9 @@ class DownloadManager(QAbstractListModel):
if role == Qt.DisplayRole: if role == Qt.DisplayRole:
data = str(item) data = str(item)
elif role == Qt.ForegroundRole: elif role == Qt.ForegroundRole:
data = config.get('colors', 'downloads.fg') data = item.get_status_color('fg')
elif role == Qt.BackgroundRole: elif role == Qt.BackgroundRole:
data = item.bg_color() data = item.get_status_color('bg')
elif role == ModelRole.item: elif role == ModelRole.item:
data = item data = item
elif role == Qt.ToolTipRole: elif role == Qt.ToolTipRole:

View File

@ -21,7 +21,6 @@
import math import math
import functools import functools
import subprocess
import collections import collections
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl, from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl,
@ -36,6 +35,7 @@ from qutebrowser.keyinput import modeman, modeparsers
from qutebrowser.browser import webelem from qutebrowser.browser import webelem
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
from qutebrowser.utils import usertypes, log, qtutils, message, objreg from qutebrowser.utils import usertypes, log, qtutils, message, objreg
from qutebrowser.misc import guiprocess
ElemTuple = collections.namedtuple('ElemTuple', ['elem', 'label']) ElemTuple = collections.namedtuple('ElemTuple', ['elem', 'label'])
@ -548,11 +548,9 @@ class HintManager(QObject):
""" """
urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword) urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
args = context.get_args(urlstr) args = context.get_args(urlstr)
try: cmd, *args = args
subprocess.Popen(args) proc = guiprocess.GUIProcess(self._win_id, what='command', parent=self)
except OSError as e: proc.start(cmd, args)
msg = "Error while spawning command: {}".format(e)
message.error(self._win_id, msg, immediately=True)
def _resolve_url(self, elem, baseurl): def _resolve_url(self, elem, baseurl):
"""Resolve a URL and check if we want to keep it. """Resolve a URL and check if we want to keep it.

View File

@ -67,23 +67,30 @@ class WebHistory(QWebHistoryInterface):
_history_dict: An OrderedDict of URLs read from the on-disk history. _history_dict: An OrderedDict of URLs read from the on-disk history.
_new_history: A list of HistoryEntry items of the current session. _new_history: A list of HistoryEntry items of the current session.
_saved_count: How many HistoryEntries have been written to disk. _saved_count: How many HistoryEntries have been written to disk.
_initial_read_started: Whether async_read was called.
_initial_read_done: Whether async_read has completed.
_temp_history: OrderedDict of temporary history entries before
async_read was called.
Signals: Signals:
item_about_to_be_added: Emitted before a new HistoryEntry is added. add_completion_item: Emitted before a new HistoryEntry is added.
arg: The new HistoryEntry. arg: The new HistoryEntry.
item_added: Emitted after a new HistoryEntry is added. item_added: Emitted after a new HistoryEntry is added.
arg: The new HistoryEntry. arg: The new HistoryEntry.
""" """
item_about_to_be_added = pyqtSignal(HistoryEntry) add_completion_item = pyqtSignal(HistoryEntry)
item_added = pyqtSignal(HistoryEntry) item_added = pyqtSignal(HistoryEntry)
async_read_done = pyqtSignal()
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self._initial_read_started = False
self._initial_read_done = False
self._lineparser = lineparser.AppendLineParser( self._lineparser = lineparser.AppendLineParser(
standarddir.data(), 'history', parent=self) standarddir.data(), 'history', parent=self)
self._history_dict = collections.OrderedDict() self._history_dict = collections.OrderedDict()
self._read_history() self._temp_history = collections.OrderedDict()
self._new_history = [] self._new_history = []
self._saved_count = 0 self._saved_count = 0
objreg.get('save-manager').add_saveable( objreg.get('save-manager').add_saveable(
@ -101,12 +108,21 @@ class WebHistory(QWebHistoryInterface):
def __len__(self): def __len__(self):
return len(self._history_dict) return len(self._history_dict)
def _read_history(self): def async_read(self):
"""Read the initial history.""" """Read the initial history."""
if standarddir.data() is None: if self._initial_read_started:
log.init.debug("Ignoring async_read() because reading is started.")
return return
self._initial_read_started = True
if standarddir.data() is None:
self._initial_read_done = True
self.async_read_done.emit()
return
with self._lineparser.open(): with self._lineparser.open():
for line in self._lineparser: for line in self._lineparser:
yield
data = line.rstrip().split(maxsplit=1) data = line.rstrip().split(maxsplit=1)
if not data: if not data:
# empty line # empty line
@ -128,8 +144,23 @@ class WebHistory(QWebHistoryInterface):
# information about previous hits change the items in # information about previous hits change the items in
# old_urls to be lists or change HistoryEntry to have a # old_urls to be lists or change HistoryEntry to have a
# list of atimes. # list of atimes.
self._history_dict[url] = HistoryEntry(atime, url) entry = HistoryEntry(atime, url)
self._history_dict.move_to_end(url) self._add_entry(entry)
self._initial_read_done = True
self.async_read_done.emit()
for url, entry in self._temp_history.items():
self._new_history.append(entry)
self._add_entry(entry)
self.add_completion_item.emit(entry)
def _add_entry(self, entry, target=None):
"""Add an entry to self._history_dict or another given OrderedDict."""
if target is None:
target = self._history_dict
target[entry.url_string] = entry
target.move_to_end(entry.url_string)
def get_recent(self): def get_recent(self):
"""Get the most recent history entries.""" """Get the most recent history entries."""
@ -151,13 +182,16 @@ class WebHistory(QWebHistoryInterface):
""" """
if not url_string: if not url_string:
return return
if not config.get('general', 'private-browsing'): if config.get('general', 'private-browsing'):
entry = HistoryEntry(time.time(), url_string) return
self.item_about_to_be_added.emit(entry) entry = HistoryEntry(time.time(), url_string)
if self._initial_read_done:
self.add_completion_item.emit(entry)
self._new_history.append(entry) self._new_history.append(entry)
self._history_dict[url_string] = entry self._add_entry(entry)
self._history_dict.move_to_end(url_string)
self.item_added.emit(entry) self.item_added.emit(entry)
else:
self._add_entry(entry, target=self._temp_history)
def historyContains(self, url_string): def historyContains(self, url_string):
"""Called by WebKit to determine if an URL is contained in the history. """Called by WebKit to determine if an URL is contained in the history.

View File

@ -21,16 +21,9 @@
import collections import collections
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication, from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication)
QUrl, QByteArray) from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslError,
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QSslError QSslSocket, QUrl, QByteArray)
try:
from PyQt5.QtNetwork import QSslSocket
except ImportError:
SSL_AVAILABLE = False
else:
SSL_AVAILABLE = QSslSocket.supportsSsl()
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, qtutils,
@ -46,13 +39,12 @@ _proxy_auth_cache = {}
def init(): def init():
"""Disable insecure SSL ciphers on old Qt versions.""" """Disable insecure SSL ciphers on old Qt versions."""
if SSL_AVAILABLE: if not qtutils.version_check('5.3.0'):
if not qtutils.version_check('5.3.0'): # Disable weak SSL ciphers.
# Disable weak SSL ciphers. # See https://codereview.qt-project.org/#/c/75943/
# See https://codereview.qt-project.org/#/c/75943/ good_ciphers = [c for c in QSslSocket.supportedCiphers()
good_ciphers = [c for c in QSslSocket.supportedCiphers() if c.usedBits() >= 128]
if c.usedBits() >= 128] QSslSocket.setDefaultCiphers(good_ciphers)
QSslSocket.setDefaultCiphers(good_ciphers)
class SslError(QSslError): class SslError(QSslError):
@ -107,10 +99,9 @@ class NetworkManager(QNetworkAccessManager):
} }
self._set_cookiejar() self._set_cookiejar()
self._set_cache() self._set_cache()
if SSL_AVAILABLE: self.sslErrors.connect(self.on_ssl_errors)
self.sslErrors.connect(self.on_ssl_errors) self._rejected_ssl_errors = collections.defaultdict(list)
self._rejected_ssl_errors = collections.defaultdict(list) self._accepted_ssl_errors = collections.defaultdict(list)
self._accepted_ssl_errors = collections.defaultdict(list)
self.authenticationRequired.connect(self.on_authentication_required) self.authenticationRequired.connect(self.on_authentication_required)
self.proxyAuthenticationRequired.connect( self.proxyAuthenticationRequired.connect(
self.on_proxy_authentication_required) self.on_proxy_authentication_required)
@ -181,76 +172,67 @@ class NetworkManager(QNetworkAccessManager):
request.deleteLater() request.deleteLater()
self.shutting_down.emit() self.shutting_down.emit()
if SSL_AVAILABLE: # pragma: no mccabe @pyqtSlot('QNetworkReply*', 'QList<QSslError>')
@pyqtSlot('QNetworkReply*', 'QList<QSslError>') def on_ssl_errors(self, reply, errors): # pragma: no mccabe
def on_ssl_errors(self, reply, errors): """Decide if SSL errors should be ignored or not.
"""Decide if SSL errors should be ignored or not.
This slot is called on SSL/TLS errors by the self.sslErrors signal. This slot is called on SSL/TLS errors by the self.sslErrors signal.
Args: Args:
reply: The QNetworkReply that is encountering the errors. reply: The QNetworkReply that is encountering the errors.
errors: A list of errors. errors: A list of errors.
""" """
errors = [SslError(e) for e in errors] errors = [SslError(e) for e in errors]
ssl_strict = config.get('network', 'ssl-strict') ssl_strict = config.get('network', 'ssl-strict')
if ssl_strict == 'ask': if ssl_strict == 'ask':
try: try:
host_tpl = urlutils.host_tuple(reply.url()) host_tpl = urlutils.host_tuple(reply.url())
except ValueError: except ValueError:
host_tpl = None host_tpl = None
is_accepted = False is_accepted = False
is_rejected = False is_rejected = False
else: else:
is_accepted = set(errors).issubset( is_accepted = set(errors).issubset(
self._accepted_ssl_errors[host_tpl]) self._accepted_ssl_errors[host_tpl])
is_rejected = set(errors).issubset( is_rejected = set(errors).issubset(
self._rejected_ssl_errors[host_tpl]) self._rejected_ssl_errors[host_tpl])
if is_accepted: if is_accepted:
reply.ignoreSslErrors() reply.ignoreSslErrors()
elif is_rejected: elif is_rejected:
pass
else:
err_string = '\n'.join('- ' + err.errorString() for err in
errors)
answer = self._ask('SSL errors - continue?\n{}'.format(
err_string), mode=usertypes.PromptMode.yesno,
owner=reply)
if answer:
reply.ignoreSslErrors()
d = self._accepted_ssl_errors
else:
d = self._rejected_ssl_errors
if host_tpl is not None:
d[host_tpl] += errors
elif ssl_strict:
pass pass
else: else:
for err in errors: err_string = '\n'.join('- ' + err.errorString() for err in
# FIXME we might want to use warn here (non-fatal error) errors)
# https://github.com/The-Compiler/qutebrowser/issues/114 answer = self._ask('SSL errors - continue?\n{}'.format(
message.error(self._win_id, err_string), mode=usertypes.PromptMode.yesno,
'SSL error: {}'.format(err.errorString())) owner=reply)
reply.ignoreSslErrors() if answer:
reply.ignoreSslErrors()
d = self._accepted_ssl_errors
else:
d = self._rejected_ssl_errors
if host_tpl is not None:
d[host_tpl] += errors
elif ssl_strict:
pass
else:
for err in errors:
# FIXME we might want to use warn here (non-fatal error)
# https://github.com/The-Compiler/qutebrowser/issues/114
message.error(self._win_id,
'SSL error: {}'.format(err.errorString()))
reply.ignoreSslErrors()
@pyqtSlot(QUrl) @pyqtSlot(QUrl)
def clear_rejected_ssl_errors(self, url): def clear_rejected_ssl_errors(self, url):
"""Clear the rejected SSL errors on a reload. """Clear the rejected SSL errors on a reload.
Args: Args:
url: The URL to remove. url: The URL to remove.
""" """
try: try:
del self._rejected_ssl_errors[url] del self._rejected_ssl_errors[url]
except KeyError: except KeyError:
pass
else:
@pyqtSlot(QUrl)
def clear_rejected_ssl_errors(self, _url):
"""Clear the rejected SSL errors on a reload.
Does nothing because SSL is unavailable.
"""
pass pass
@pyqtSlot('QNetworkReply', 'QAuthenticator') @pyqtSlot('QNetworkReply', 'QAuthenticator')
@ -334,11 +316,7 @@ class NetworkManager(QNetworkAccessManager):
A QNetworkReply. A QNetworkReply.
""" """
scheme = req.url().scheme() scheme = req.url().scheme()
if scheme == 'https' and not SSL_AVAILABLE: if scheme in self._scheme_handlers:
return networkreply.ErrorNetworkReply(
req, "SSL is not supported by the installed Qt library!",
QNetworkReply.ProtocolUnknownError, self)
elif scheme in self._scheme_handlers:
return self._scheme_handlers[scheme].createRequest( return self._scheme_handlers[scheme].createRequest(
op, req, outgoing_data) op, req, outgoing_data)

View File

@ -34,7 +34,6 @@ import configparser
from PyQt5.QtCore import pyqtSlot, QObject from PyQt5.QtCore import pyqtSlot, QObject
from PyQt5.QtNetwork import QNetworkReply from PyQt5.QtNetwork import QNetworkReply
from PyQt5.QtWebKit import QWebSettings
import qutebrowser import qutebrowser
from qutebrowser.browser.network import schemehandler, networkreply from qutebrowser.browser.network import schemehandler, networkreply
@ -98,7 +97,7 @@ class JSBridge(QObject):
def set(self, win_id, sectname, optname, value): def set(self, win_id, sectname, optname, value):
"""Slot to set a setting from qute:settings.""" """Slot to set a setting from qute:settings."""
# https://github.com/The-Compiler/qutebrowser/issues/727 # https://github.com/The-Compiler/qutebrowser/issues/727
if (sectname, optname == 'content', 'allow-javascript' and if ((sectname, optname) == ('content', 'allow-javascript') and
value == 'false'): value == 'false'):
message.error(win_id, "Refusing to disable javascript via " message.error(win_id, "Refusing to disable javascript via "
"qute:settings as it needs javascript support.") "qute:settings as it needs javascript support.")
@ -160,7 +159,7 @@ def qute_help(win_id, request):
url=request.url().toDisplayString(), url=request.url().toDisplayString(),
error="This most likely means the documentation was not generated " error="This most likely means the documentation was not generated "
"properly. If you are running qutebrowser from the git " "properly. If you are running qutebrowser from the git "
"repository, please run scripts/asciidoc2html.py." "repository, please run scripts/asciidoc2html.py. "
"If you're running a released version this is a bug, please " "If you're running a released version this is a bug, please "
"use :report to report it.", "use :report to report it.",
icon='') icon='')
@ -179,18 +178,10 @@ def qute_help(win_id, request):
def qute_settings(win_id, _request): def qute_settings(win_id, _request):
"""Handler for qute:settings. View/change qute configuration.""" """Handler for qute:settings. View/change qute configuration."""
if not QWebSettings.globalSettings().testAttribute( config_getter = functools.partial(objreg.get('config').get, raw=True)
QWebSettings.JavascriptEnabled): html = jinja.env.get_template('settings.html').render(
# https://github.com/The-Compiler/qutebrowser/issues/727 win_id=win_id, title='settings', config=configdata,
template = jinja.env.get_template('pre.html') confget=config_getter)
html = template.render(
title='Failed to open qute:settings.',
content="qute:settings needs javascript enabled to work.")
else:
config_getter = functools.partial(objreg.get('config').get, raw=True)
html = jinja.env.get_template('settings.html').render(
win_id=win_id, title='settings', config=configdata,
confget=config_getter)
return html.encode('UTF-8', errors='xmlcharrefreplace') return html.encode('UTF-8', errors='xmlcharrefreplace')

View File

@ -312,7 +312,7 @@ def javascript_escape(text):
def get_child_frames(startframe): def get_child_frames(startframe):
"""Get all children recursively of a given QWebFrame. """Get all children recursively of a given QWebFrame.
Loosly based on http://blog.nextgenetics.net/?e=64 Loosely based on http://blog.nextgenetics.net/?e=64
Args: Args:
startframe: The QWebFrame to start with. startframe: The QWebFrame to start with.

View File

@ -109,7 +109,7 @@ class BrowserPage(QWebPage):
def _handle_errorpage(self, info, errpage): def _handle_errorpage(self, info, errpage):
"""Display an error page if needed. """Display an error page if needed.
Loosly based on Helpviewer/HelpBrowserWV.py from eric5 Loosely based on Helpviewer/HelpBrowserWV.py from eric5
(line 260 @ 5d937eb378dd) (line 260 @ 5d937eb378dd)
Args: Args:
@ -178,7 +178,7 @@ class BrowserPage(QWebPage):
def _handle_multiple_files(self, info, files): def _handle_multiple_files(self, info, files):
"""Handle uploading of multiple files. """Handle uploading of multiple files.
Loosly based on Helpviewer/HelpBrowserWV.py from eric5. Loosely based on Helpviewer/HelpBrowserWV.py from eric5.
Args: Args:
info: The ChooseMultipleFilesExtensionOption instance. info: The ChooseMultipleFilesExtensionOption instance.
@ -241,7 +241,7 @@ class BrowserPage(QWebPage):
if cur_data is not None: if cur_data is not None:
frame = self.mainFrame() frame = self.mainFrame()
if 'zoom' in cur_data: if 'zoom' in cur_data:
frame.setZoomFactor(cur_data['zoom']) frame.page().view().zoom_perc(cur_data['zoom'] * 100)
if ('scroll-pos' in cur_data and if ('scroll-pos' in cur_data and
frame.scrollPosition() == QPoint(0, 0)): frame.scrollPosition() == QPoint(0, 0)):
QTimer.singleShot(0, functools.partial( QTimer.singleShot(0, functools.partial(
@ -418,7 +418,7 @@ class BrowserPage(QWebPage):
if data is None: if data is None:
return return
if 'zoom' in data: if 'zoom' in data:
frame.setZoomFactor(data['zoom']) frame.page().view().zoom_perc(data['zoom'] * 100)
if 'scroll-pos' in data and frame.scrollPosition() == QPoint(0, 0): if 'scroll-pos' in data and frame.scrollPosition() == QPoint(0, 0):
frame.setScrollPosition(data['scroll-pos']) frame.setScrollPosition(data['scroll-pos'])

View File

@ -33,7 +33,6 @@ from qutebrowser.config import config
from qutebrowser.keyinput import modeman from qutebrowser.keyinput import modeman
from qutebrowser.utils import message, log, usertypes, utils, qtutils, objreg from qutebrowser.utils import message, log, usertypes, utils, qtutils, objreg
from qutebrowser.browser import webpage, hints, webelem from qutebrowser.browser import webpage, hints, webelem
from qutebrowser.commands import cmdexc
LoadStatus = usertypes.enum('LoadStatus', ['none', 'success', 'error', 'warn', LoadStatus = usertypes.enum('LoadStatus', ['none', 'success', 'error', 'warn',
@ -163,7 +162,7 @@ class WebView(QWebView):
return utils.get_repr(self, tab_id=self.tab_id, url=url) return utils.get_repr(self, tab_id=self.tab_id, url=url)
def __del__(self): def __del__(self):
# Explicitely releasing the page here seems to prevent some segfaults # Explicitly releasing the page here seems to prevent some segfaults
# when quitting. # when quitting.
# Copied from: # Copied from:
# https://code.google.com/p/webscraping/source/browse/webkit.py#325 # https://code.google.com/p/webscraping/source/browse/webkit.py#325
@ -369,9 +368,8 @@ class WebView(QWebView):
if fuzzyval: if fuzzyval:
self._zoom.fuzzyval = int(perc) self._zoom.fuzzyval = int(perc)
if perc < 0: if perc < 0:
raise cmdexc.CommandError("Can't zoom {}%!".format(perc)) raise ValueError("Can't zoom {}%!".format(perc))
self.setZoomFactor(float(perc) / 100) self.setZoomFactor(float(perc) / 100)
message.info(self.win_id, "Zoom level: {}%".format(perc))
self._default_zoom_changed = True self._default_zoom_changed = True
def zoom(self, offset): def zoom(self, offset):
@ -379,9 +377,13 @@ class WebView(QWebView):
Args: Args:
offset: The offset in the zoom level list. offset: The offset in the zoom level list.
Return:
The new zoom percentage.
""" """
level = self._zoom.getitem(offset) level = self._zoom.getitem(offset)
self.zoom_perc(level, fuzzyval=False) self.zoom_perc(level, fuzzyval=False)
return level
@pyqtSlot('QUrl') @pyqtSlot('QUrl')
def on_url_changed(self, url): def on_url_changed(self, url):
@ -460,15 +462,22 @@ class WebView(QWebView):
elif mode == usertypes.KeyMode.caret: elif mode == usertypes.KeyMode.caret:
settings = self.settings() settings = self.settings()
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True) settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True)
self.selection_enabled = False self.selection_enabled = bool(self.page().selectedText())
if self.isVisible(): if self.isVisible():
# Sometimes the caret isn't immediately visible, but unfocusing # Sometimes the caret isn't immediately visible, but unfocusing
# and refocusing it fixes that. # and refocusing it fixes that.
self.clearFocus() self.clearFocus()
self.setFocus(Qt.OtherFocusReason) self.setFocus(Qt.OtherFocusReason)
self.page().currentFrame().evaluateJavaScript(
utils.read_file('javascript/position_caret.js')) # Move the caret to the first element in the viewport if there
# isn't any text which is already selected.
#
# Note: We can't use hasSelection() here, as that's always
# true in caret mode.
if not self.page().selectedText():
self.page().currentFrame().evaluateJavaScript(
utils.read_file('javascript/position_caret.js'))
@pyqtSlot(usertypes.KeyMode) @pyqtSlot(usertypes.KeyMode)
def on_mode_left(self, mode): def on_mode_left(self, mode):
@ -620,6 +629,7 @@ class WebView(QWebView):
"""Save a reference to the context menu so we can close it.""" """Save a reference to the context menu so we can close it."""
menu = self.page().createStandardContextMenu() menu = self.page().createStandardContextMenu()
self.shutting_down.connect(menu.close) self.shutting_down.connect(menu.close)
modeman.instance(self.win_id).entered.connect(menu.close)
menu.exec_(e.globalPos()) menu.exec_(e.globalPos())
def wheelEvent(self, e): def wheelEvent(self, e):

View File

@ -29,6 +29,11 @@ from qutebrowser.utils import log, utils, message, docutils, objreg, usertypes
from qutebrowser.utils import debug as debug_utils from qutebrowser.utils import debug as debug_utils
def arg_name(name):
"""Get the name an argument should have based on its Python name."""
return name.rstrip('_').replace('_', '-')
class Command: class Command:
"""Base skeleton for a command. """Base skeleton for a command.
@ -257,10 +262,12 @@ class Command:
except KeyError: except KeyError:
pass pass
kwargs['dest'] = param.name
if isinstance(typ, tuple): if isinstance(typ, tuple):
kwargs['metavar'] = annotation_info.metavar or param.name kwargs['metavar'] = annotation_info.metavar or param.name
elif utils.is_enum(typ): elif utils.is_enum(typ):
kwargs['choices'] = [e.name.replace('_', '-') for e in typ] kwargs['choices'] = [arg_name(e.name) for e in typ]
kwargs['metavar'] = annotation_info.metavar or param.name kwargs['metavar'] = annotation_info.metavar or param.name
elif typ is bool: elif typ is bool:
kwargs['action'] = 'store_true' kwargs['action'] = 'store_true'
@ -288,7 +295,7 @@ class Command:
A list of args. A list of args.
""" """
args = [] args = []
name = param.name.rstrip('_').replace('_', '-') name = arg_name(param.name)
shortname = annotation_info.flag or name[0] shortname = annotation_info.flag or name[0]
if len(shortname) != 1: if len(shortname) != 1:
raise ValueError("Flag '{}' of parameter {} (command {}) must be " raise ValueError("Flag '{}' of parameter {} (command {}) must be "
@ -304,7 +311,6 @@ class Command:
if typ is not bool: if typ is not bool:
self.flags_with_args += [short_flag, long_flag] self.flags_with_args += [short_flag, long_flag]
else: else:
args.append(name)
if not annotation_info.hide: if not annotation_info.hide:
self.pos_args.append((param.name, name)) self.pos_args.append((param.name, name))
return args return args
@ -408,17 +414,16 @@ class Command:
raise TypeError("{}: invalid parameter type {} for argument " raise TypeError("{}: invalid parameter type {} for argument "
"{!r}!".format(self.name, param.kind, param.name)) "{!r}!".format(self.name, param.kind, param.name))
def _get_param_name_and_value(self, param): def _get_param_value(self, param):
"""Get the converted name and value for an inspect.Parameter.""" """Get the converted value for an inspect.Parameter."""
name = param.name.rstrip('_') value = getattr(self.namespace, param.name)
value = getattr(self.namespace, name)
if param.name in self._type_conv: if param.name in self._type_conv:
# We convert enum types after getting the values from # We convert enum types after getting the values from
# argparse, because argparse's choices argument is # argparse, because argparse's choices argument is
# processed after type conversation, which is not what we # processed after type conversation, which is not what we
# want. # want.
value = self._type_conv[param.name](value) value = self._type_conv[param.name](value)
return name, value return value
def _get_call_args(self, win_id): def _get_call_args(self, win_id):
"""Get arguments for a function call. """Get arguments for a function call.
@ -452,14 +457,14 @@ class Command:
# Special case for win_id parameter. # Special case for win_id parameter.
self._get_win_id_arg(win_id, param, args, kwargs) self._get_win_id_arg(win_id, param, args, kwargs)
continue continue
name, value = self._get_param_name_and_value(param) value = self._get_param_value(param)
if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
args.append(value) args.append(value)
elif param.kind == inspect.Parameter.VAR_POSITIONAL: elif param.kind == inspect.Parameter.VAR_POSITIONAL:
if value is not None: if value is not None:
args += value args += value
elif param.kind == inspect.Parameter.KEYWORD_ONLY: elif param.kind == inspect.Parameter.KEYWORD_ONLY:
kwargs[name] = value kwargs[param.name] = value
else: else:
raise TypeError("{}: Invalid parameter type {} for argument " raise TypeError("{}: Invalid parameter type {} for argument "
"'{}'!".format( "'{}'!".format(

View File

@ -23,12 +23,12 @@ import os
import os.path import os.path
import tempfile import tempfile
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QSocketNotifier, from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QSocketNotifier
QProcessEnvironment, QProcess)
from qutebrowser.utils import message, log, objreg, standarddir from qutebrowser.utils import message, log, objreg, standarddir
from qutebrowser.commands import runners, cmdexc from qutebrowser.commands import runners, cmdexc
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.misc import guiprocess
class _QtFIFOReader(QObject): class _QtFIFOReader(QObject):
@ -70,13 +70,9 @@ class _BaseUserscriptRunner(QObject):
Attributes: Attributes:
_filepath: The path of the file/FIFO which is being read. _filepath: The path of the file/FIFO which is being read.
_proc: The QProcess which is being executed. _proc: The GUIProcess which is being executed.
_win_id: The window ID this runner is associated with. _win_id: The window ID this runner is associated with.
Class attributes:
PROCESS_MESSAGES: A mapping of QProcess::ProcessError members to
human-readable error strings.
Signals: Signals:
got_cmd: Emitted when a new command arrived and should be executed. got_cmd: Emitted when a new command arrived and should be executed.
finished: Emitted when the userscript finished running. finished: Emitted when the userscript finished running.
@ -85,17 +81,6 @@ class _BaseUserscriptRunner(QObject):
got_cmd = pyqtSignal(str) got_cmd = pyqtSignal(str)
finished = pyqtSignal() finished = pyqtSignal()
PROCESS_MESSAGES = {
QProcess.FailedToStart: "The process failed to start.",
QProcess.Crashed: "The process crashed.",
QProcess.Timedout: "The last waitFor...() function timed out.",
QProcess.WriteError: ("An error occurred when attempting to write to "
"the process."),
QProcess.ReadError: ("An error occurred when attempting to read from "
"the process."),
QProcess.UnknownError: "An unknown error occurred.",
}
def __init__(self, win_id, parent=None): def __init__(self, win_id, parent=None):
super().__init__(parent) super().__init__(parent)
self._win_id = win_id self._win_id = win_id
@ -103,22 +88,20 @@ class _BaseUserscriptRunner(QObject):
self._proc = None self._proc = None
self._env = None self._env = None
def _run_process(self, cmd, *args, env): def _run_process(self, cmd, *args, env, verbose):
"""Start the given command via QProcess. """Start the given command.
Args: Args:
cmd: The command to be started. cmd: The command to be started.
*args: The arguments to hand to the command *args: The arguments to hand to the command
env: A dictionary of environment variables to add. env: A dictionary of environment variables to add.
verbose: Show notifications when the command started/exited.
""" """
self._env = env self._env = {'QUTE_FIFO': self._filepath}
self._proc = QProcess(self) self._env.update(env)
procenv = QProcessEnvironment.systemEnvironment() self._proc = guiprocess.GUIProcess(self._win_id, 'userscript',
procenv.insert('QUTE_FIFO', self._filepath) additional_env=self._env,
if env is not None: verbose=verbose, parent=self)
for k, v in env.items():
procenv.insert(k, v)
self._proc.setProcessEnvironment(procenv)
self._proc.error.connect(self.on_proc_error) self._proc.error.connect(self.on_proc_error)
self._proc.finished.connect(self.on_proc_finished) self._proc.finished.connect(self.on_proc_finished)
self._proc.start(cmd, args) self._proc.start(cmd, args)
@ -126,11 +109,10 @@ class _BaseUserscriptRunner(QObject):
def _cleanup(self): def _cleanup(self):
"""Clean up temporary files.""" """Clean up temporary files."""
tempfiles = [self._filepath] tempfiles = [self._filepath]
if self._env is not None: if 'QUTE_HTML' in self._env:
if 'QUTE_HTML' in self._env: tempfiles.append(self._env['QUTE_HTML'])
tempfiles.append(self._env['QUTE_HTML']) if 'QUTE_TEXT' in self._env:
if 'QUTE_TEXT' in self._env: tempfiles.append(self._env['QUTE_TEXT'])
tempfiles.append(self._env['QUTE_TEXT'])
for fn in tempfiles: for fn in tempfiles:
log.procs.debug("Deleting temporary file {}.".format(fn)) log.procs.debug("Deleting temporary file {}.".format(fn))
try: try:
@ -145,7 +127,7 @@ class _BaseUserscriptRunner(QObject):
self._proc = None self._proc = None
self._env = None self._env = None
def run(self, cmd, *args, env=None): def run(self, cmd, *args, env=None, verbose=False):
"""Run the userscript given. """Run the userscript given.
Needs to be overridden by subclasses. Needs to be overridden by subclasses.
@ -154,6 +136,7 @@ class _BaseUserscriptRunner(QObject):
cmd: The command to be started. cmd: The command to be started.
*args: The arguments to hand to the command *args: The arguments to hand to the command
env: A dictionary of environment variables to add. env: A dictionary of environment variables to add.
verbose: Show notifications when the command started/exited.
""" """
raise NotImplementedError raise NotImplementedError
@ -166,12 +149,7 @@ class _BaseUserscriptRunner(QObject):
def on_proc_error(self, error): def on_proc_error(self, error):
"""Called when the process encountered an error.""" """Called when the process encountered an error."""
msg = self.PROCESS_MESSAGES[error] raise NotImplementedError
# NOTE: Do not replace this with "raise CommandError" as it's
# executed async.
message.error(self._win_id,
"Error while calling userscript: {}".format(msg))
log.procs.debug("Userscript process error: {} - {}".format(error, msg))
class _POSIXUserscriptRunner(_BaseUserscriptRunner): class _POSIXUserscriptRunner(_BaseUserscriptRunner):
@ -188,7 +166,7 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner):
super().__init__(win_id, parent) super().__init__(win_id, parent)
self._reader = None self._reader = None
def run(self, cmd, *args, env=None): def run(self, cmd, *args, env=None, verbose=False):
try: try:
# tempfile.mktemp is deprecated and discouraged, but we use it here # tempfile.mktemp is deprecated and discouraged, but we use it here
# to create a FIFO since the only other alternative would be to # to create a FIFO since the only other alternative would be to
@ -206,16 +184,14 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner):
self._reader = _QtFIFOReader(self._filepath) self._reader = _QtFIFOReader(self._filepath)
self._reader.got_line.connect(self.got_cmd) self._reader.got_line.connect(self.got_cmd)
self._run_process(cmd, *args, env=env) self._run_process(cmd, *args, env=env, verbose=verbose)
def on_proc_finished(self): def on_proc_finished(self):
"""Interrupt the reader when the process finished.""" """Interrupt the reader when the process finished."""
log.procs.debug("Userscript process finished.")
self.finish() self.finish()
def on_proc_error(self, error): def on_proc_error(self, error):
"""Interrupt the reader when the process had an error.""" """Interrupt the reader when the process had an error."""
super().on_proc_error(error)
self.finish() self.finish()
def finish(self): def finish(self):
@ -260,7 +236,6 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner):
def on_proc_finished(self): def on_proc_finished(self):
"""Read back the commands when the process finished.""" """Read back the commands when the process finished."""
log.procs.debug("Userscript process finished.")
try: try:
with open(self._filepath, 'r', encoding='utf-8') as f: with open(self._filepath, 'r', encoding='utf-8') as f:
for line in f: for line in f:
@ -272,18 +247,17 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner):
def on_proc_error(self, error): def on_proc_error(self, error):
"""Clean up when the process had an error.""" """Clean up when the process had an error."""
super().on_proc_error(error)
self._cleanup() self._cleanup()
self.finished.emit() self.finished.emit()
def run(self, cmd, *args, env=None): def run(self, cmd, *args, env=None, verbose=False):
try: try:
self._oshandle, self._filepath = tempfile.mkstemp(text=True) self._oshandle, self._filepath = tempfile.mkstemp(text=True)
except OSError as e: except OSError as e:
message.error(self._win_id, "Error while creating tempfile: " message.error(self._win_id, "Error while creating tempfile: "
"{}".format(e)) "{}".format(e))
return return
self._run_process(cmd, *args, env=env) self._run_process(cmd, *args, env=env, verbose=verbose)
class _DummyUserscriptRunner: class _DummyUserscriptRunner:
@ -299,8 +273,9 @@ class _DummyUserscriptRunner:
finished = pyqtSignal() finished = pyqtSignal()
def run(self, _cmd, *_args, _env=None): def run(self, cmd, *args, env=None, verbose=False):
"""Print an error as userscripts are not supported.""" """Print an error as userscripts are not supported."""
# pylint: disable=unused-argument,unused-variable
self.finished.emit() self.finished.emit()
raise cmdexc.CommandError( raise cmdexc.CommandError(
"Userscripts are not supported on this platform!") "Userscripts are not supported on this platform!")
@ -347,7 +322,7 @@ def store_source(frame):
return env return env
def run(cmd, *args, win_id, env): def run(cmd, *args, win_id, env, verbose=False):
"""Convenience method to run an userscript. """Convenience method to run an userscript.
Args: Args:
@ -355,6 +330,7 @@ def run(cmd, *args, win_id, env):
*args: The arguments to pass to the userscript. *args: The arguments to pass to the userscript.
win_id: The window id the userscript is executed in. win_id: The window id the userscript is executed in.
env: A dictionary of variables to add to the process environment. env: A dictionary of variables to add to the process environment.
verbose: Show notifications when the command started/exited.
""" """
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id) window=win_id)
@ -367,6 +343,6 @@ def run(cmd, *args, win_id, env):
user_agent = config.get('network', 'user-agent') user_agent = config.get('network', 'user-agent')
if user_agent is not None: if user_agent is not None:
env['QUTE_USER_AGENT'] = user_agent env['QUTE_USER_AGENT'] = user_agent
runner.run(cmd, *args, env=env) runner.run(cmd, *args, env=env, verbose=verbose)
runner.finished.connect(commandrunner.deleteLater) runner.finished.connect(commandrunner.deleteLater)
runner.finished.connect(runner.deleteLater) runner.finished.connect(runner.deleteLater)

View File

@ -19,7 +19,7 @@
"""Completer attached to a CompletionView.""" """Completer attached to a CompletionView."""
from PyQt5.QtCore import pyqtSlot, QObject, QTimer from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.commands import cmdutils, runners from qutebrowser.commands import cmdutils, runners
@ -40,14 +40,22 @@ class Completer(QObject):
_last_cursor_pos: The old cursor position so we avoid double completion _last_cursor_pos: The old cursor position so we avoid double completion
updates. updates.
_last_text: The old command text so we avoid double completion updates. _last_text: The old command text so we avoid double completion updates.
_signals_connected: Whether the signals are connected to update the
completion when the command widget requests that.
Signals:
next_prev_item: Emitted to select the next/previous item in the
completion.
arg0: True for the previous item, False for the next.
""" """
next_prev_item = pyqtSignal(bool)
def __init__(self, cmd, win_id, parent=None): def __init__(self, cmd, win_id, parent=None):
super().__init__(parent) super().__init__(parent)
self._win_id = win_id self._win_id = win_id
self._cmd = cmd self._cmd = cmd
self._cmd.update_completion.connect(self.schedule_completion_update) self._signals_connected = False
self._cmd.textEdited.connect(self.on_text_edited)
self._ignore_change = False self._ignore_change = False
self._empty_item_idx = None self._empty_item_idx = None
self._timer = QTimer() self._timer = QTimer()
@ -58,9 +66,63 @@ class Completer(QObject):
self._last_cursor_pos = None self._last_cursor_pos = None
self._last_text = None self._last_text = None
objreg.get('config').changed.connect(self.on_auto_open_changed)
self.handle_signal_connections()
self._cmd.clear_completion_selection.connect(
self.handle_signal_connections)
def __repr__(self): def __repr__(self):
return utils.get_repr(self) return utils.get_repr(self)
@config.change_filter('completion', 'auto-open')
def on_auto_open_changed(self):
self.handle_signal_connections()
@pyqtSlot()
def handle_signal_connections(self):
self._connect_signals(config.get('completion', 'auto-open'))
def _connect_signals(self, connect=True):
"""Connect or disconnect the completion signals.
Args:
connect: Whether to connect (True) or disconnect (False) the
signals.
Return:
True if the signals were connected (connect=True and aren't
connected yet) - otherwise False.
"""
connections = [
(self._cmd.update_completion, self.schedule_completion_update),
(self._cmd.textChanged, self.on_text_edited),
]
if connect and not self._signals_connected:
for sender, receiver in connections:
sender.connect(receiver)
self._signals_connected = True
return True
elif not connect:
for sender, receiver in connections:
try:
sender.disconnect(receiver)
except TypeError:
# Don't fail if not connected
pass
self._signals_connected = False
return False
def _open_completion_if_needed(self):
"""If auto-open is false, temporarily connect signals.
Also opens the completion.
"""
if not config.get('completion', 'auto-open'):
connected = self._connect_signals(True)
if connected:
self.update_completion()
def _model(self): def _model(self):
"""Convienience method to get the current completion model.""" """Convienience method to get the current completion model."""
completion = objreg.get('completion', scope='window', completion = objreg.get('completion', scope='window',
@ -272,7 +334,7 @@ class Completer(QObject):
pattern = parts[self._cursor_part].strip() pattern = parts[self._cursor_part].strip()
except IndexError: except IndexError:
pattern = '' pattern = ''
self._model().set_pattern(pattern) completion.set_pattern(pattern)
log.completion.debug( log.completion.debug(
"New completion for {}: {}, with pattern '{}'".format( "New completion for {}: {}, with pattern '{}'".format(
@ -328,7 +390,7 @@ class Completer(QObject):
cursor_pos)) cursor_pos))
skip = 0 skip = 0
for i, part in enumerate(parts): for i, part in enumerate(parts):
log.completion.vdebug("Checking part {}: {}".format(i, parts[i])) log.completion.vdebug("Checking part {}: {!r}".format(i, parts[i]))
if not part: if not part:
skip += 1 skip += 1
continue continue
@ -350,7 +412,11 @@ class Completer(QObject):
"Removing len({!r}) -> {} from cursor_pos -> {}".format( "Removing len({!r}) -> {} from cursor_pos -> {}".format(
part, len(part), cursor_pos)) part, len(part), cursor_pos))
else: else:
self._cursor_part = i - skip if i == 0:
# Initial `:` press without any text.
self._cursor_part = 0
else:
self._cursor_part = i - skip
if spaces: if spaces:
self._empty_item_idx = i - skip self._empty_item_idx = i - skip
else: else:
@ -401,3 +467,17 @@ class Completer(QObject):
# We also want to update the cursor part and emit update_completion # We also want to update the cursor part and emit update_completion
# here, but that's already done for us by cursorPositionChanged # here, but that's already done for us by cursorPositionChanged
# anyways, so we don't need to do it twice. # anyways, so we don't need to do it twice.
@cmdutils.register(instance='completer', hide=True,
modes=[usertypes.KeyMode.command], scope='window')
def completion_item_prev(self):
"""Select the previous completion item."""
self._open_completion_if_needed()
self.next_prev_item.emit(True)
@cmdutils.register(instance='completer', hide=True,
modes=[usertypes.KeyMode.command], scope='window')
def completion_item_next(self):
"""Select the next completion item."""
self._open_completion_if_needed()
self.next_prev_item.emit(False)

View File

@ -145,7 +145,6 @@ class CompletionItemDelegate(QStyledItemDelegate):
rect: The QRect to clip the drawing to. rect: The QRect to clip the drawing to.
""" """
# We can't use drawContents because then the color would be ignored. # We can't use drawContents because then the color would be ignored.
# See: https://qt-project.org/forums/viewthread/21492
clip = QRectF(0, 0, rect.width(), rect.height()) clip = QRectF(0, 0, rect.width(), rect.height())
self._painter.save() self._painter.save()
if self._opt.state & QStyle.State_Selected: if self._opt.state & QStyle.State_Selected:

View File

@ -26,10 +26,9 @@ subclasses to provide completions.
from PyQt5.QtWidgets import QStyle, QTreeView, QSizePolicy from PyQt5.QtWidgets import QStyle, QTreeView, QSizePolicy
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel
from qutebrowser.commands import cmdutils
from qutebrowser.config import config, style from qutebrowser.config import config, style
from qutebrowser.completion import completiondelegate, completer from qutebrowser.completion import completiondelegate, completer
from qutebrowser.utils import usertypes, qtutils, objreg, utils from qutebrowser.utils import qtutils, objreg, utils
class CompletionView(QTreeView): class CompletionView(QTreeView):
@ -96,12 +95,13 @@ class CompletionView(QTreeView):
objreg.register('completion', self, scope='window', window=win_id) objreg.register('completion', self, scope='window', window=win_id)
cmd = objreg.get('status-command', scope='window', window=win_id) cmd = objreg.get('status-command', scope='window', window=win_id)
completer_obj = completer.Completer(cmd, win_id, self) completer_obj = completer.Completer(cmd, win_id, self)
completer_obj.next_prev_item.connect(self.on_next_prev_item)
objreg.register('completer', completer_obj, scope='window', objreg.register('completer', completer_obj, scope='window',
window=win_id) window=win_id)
self.enabled = config.get('completion', 'show') self.enabled = config.get('completion', 'show')
objreg.get('config').changed.connect(self.set_enabled) objreg.get('config').changed.connect(self.set_enabled)
# FIXME handle new aliases. # FIXME handle new aliases.
#objreg.get('config').changed.connect(self.init_command_completion) # objreg.get('config').changed.connect(self.init_command_completion)
self._delegate = completiondelegate.CompletionItemDelegate(self) self._delegate = completiondelegate.CompletionItemDelegate(self)
self.setItemDelegate(self._delegate) self.setItemDelegate(self._delegate)
@ -168,12 +168,15 @@ class CompletionView(QTreeView):
# Item is a real item, not a category header -> success # Item is a real item, not a category header -> success
return idx return idx
def _next_prev_item(self, prev): @pyqtSlot(bool)
def on_next_prev_item(self, prev):
"""Handle a tab press for the CompletionView. """Handle a tab press for the CompletionView.
Select the previous/next item and write the new text to the Select the previous/next item and write the new text to the
statusbar. statusbar.
Called from the Completer's next_prev_item signal.
Args: Args:
prev: True for prev item, False for next one. prev: True for prev item, False for next one.
""" """
@ -201,8 +204,17 @@ class CompletionView(QTreeView):
for i in range(model.rowCount()): for i in range(model.rowCount()):
self.expand(model.index(i, 0)) self.expand(model.index(i, 0))
self._resize_columns() self._resize_columns()
model.rowsRemoved.connect(self.maybe_resize_completion) self.maybe_resize_completion()
model.rowsInserted.connect(self.maybe_resize_completion)
def set_pattern(self, pattern):
"""Set the completion pattern for the current model.
Called from on_update_completion().
Args:
pattern: The filter pattern to set (what the user entered).
"""
self.model().set_pattern(pattern)
self.maybe_resize_completion() self.maybe_resize_completion()
@pyqtSlot() @pyqtSlot()
@ -224,18 +236,6 @@ class CompletionView(QTreeView):
selmod.clearSelection() selmod.clearSelection()
selmod.clearCurrentIndex() selmod.clearCurrentIndex()
@cmdutils.register(instance='completion', hide=True,
modes=[usertypes.KeyMode.command], scope='window')
def completion_item_prev(self):
"""Select the previous completion item."""
self._next_prev_item(prev=True)
@cmdutils.register(instance='completion', hide=True,
modes=[usertypes.KeyMode.command], scope='window')
def completion_item_next(self):
"""Select the next completion item."""
self._next_prev_item(prev=False)
def selectionChanged(self, selected, deselected): def selectionChanged(self, selected, deselected):
"""Extend selectionChanged to call completers selection_changed.""" """Extend selectionChanged to call completers selection_changed."""
super().selectionChanged(selected, deselected) super().selectionChanged(selected, deselected)

View File

@ -165,6 +165,11 @@ def init():
quickmark_manager.changed.connect( quickmark_manager.changed.connect(
functools.partial(update, [usertypes.Completion.quickmark_by_url, functools.partial(update, [usertypes.Completion.quickmark_by_url,
usertypes.Completion.quickmark_by_name])) usertypes.Completion.quickmark_by_name]))
session_manager = objreg.get('session-manager') session_manager = objreg.get('session-manager')
session_manager.update_completion.connect( session_manager.update_completion.connect(
functools.partial(update, [usertypes.Completion.sessions])) functools.partial(update, [usertypes.Completion.sessions]))
history = objreg.get('web-history')
history.async_read_done.connect(
functools.partial(update, [usertypes.Completion.url]))

View File

@ -54,7 +54,7 @@ class UrlCompletionModel(base.BaseCompletionModel):
history = utils.newest_slice(self._history, max_history) history = utils.newest_slice(self._history, max_history)
for entry in history: for entry in history:
self._add_history_entry(entry) self._add_history_entry(entry)
self._history.item_about_to_be_added.connect( self._history.add_completion_item.connect(
self.on_history_item_added) self.on_history_item_added)
objreg.get('config').changed.connect(self.reformat_timestamps) objreg.get('config').changed.connect(self.reformat_timestamps)

View File

@ -252,6 +252,25 @@ def init(parent=None):
_init_misc() _init_misc()
def _get_value_transformer(old, new):
"""Get a function which transforms a value for CHANGED_OPTIONS.
Args:
old: The old value - if the supplied value doesn't match this, it's
returned untransformed.
new: The new value.
Return:
A function which takes a value and transforms it.
"""
def transformer(val):
if val == old:
return new
else:
return val
return transformer
class ConfigManager(QObject): class ConfigManager(QObject):
"""Configuration manager for qutebrowser. """Configuration manager for qutebrowser.
@ -263,6 +282,10 @@ class ConfigManager(QObject):
RENAMED_SECTIONS: A mapping of renamed sections, {'oldname': 'newname'} RENAMED_SECTIONS: A mapping of renamed sections, {'oldname': 'newname'}
RENAMED_OPTIONS: A mapping of renamed options, RENAMED_OPTIONS: A mapping of renamed options,
{('section', 'oldname'): 'newname'} {('section', 'oldname'): 'newname'}
CHANGED_OPTIONS: A mapping of arbitrarily changed options,
{('section', 'option'): callable}.
The callable takes the old value and returns the new
one.
DELETED_OPTIONS: A (section, option) list of deleted options. DELETED_OPTIONS: A (section, option) list of deleted options.
Attributes: Attributes:
@ -298,12 +321,17 @@ class ConfigManager(QObject):
('colors', 'tab.indicator.system'): 'tabs.indicator.system', ('colors', 'tab.indicator.system'): 'tabs.indicator.system',
('tabs', 'auto-hide'): 'hide-auto', ('tabs', 'auto-hide'): 'hide-auto',
('completion', 'history-length'): 'cmd-history-max-items', ('completion', 'history-length'): 'cmd-history-max-items',
('colors', 'downloads.fg'): 'downloads.fg.start',
} }
DELETED_OPTIONS = [ DELETED_OPTIONS = [
('colors', 'tab.separator'), ('colors', 'tab.separator'),
('colors', 'tabs.separator'), ('colors', 'tabs.separator'),
('colors', 'completion.item.bg'), ('colors', 'completion.item.bg'),
] ]
CHANGED_OPTIONS = {
('content', 'cookies-accept'):
_get_value_transformer('default', 'no-3rdparty'),
}
changed = pyqtSignal(str, str) changed = pyqtSignal(str, str)
style_changed = pyqtSignal(str, str) style_changed = pyqtSignal(str, str)
@ -462,10 +490,15 @@ class ConfigManager(QObject):
for k, v in cp[real_sectname].items(): for k, v in cp[real_sectname].items():
if k.startswith(self.ESCAPE_CHAR): if k.startswith(self.ESCAPE_CHAR):
k = k[1:] k = k[1:]
if (sectname, k) in self.DELETED_OPTIONS: if (sectname, k) in self.DELETED_OPTIONS:
return return
elif (sectname, k) in self.RENAMED_OPTIONS: if (sectname, k) in self.RENAMED_OPTIONS:
k = self.RENAMED_OPTIONS[sectname, k] k = self.RENAMED_OPTIONS[sectname, k]
if (sectname, k) in self.CHANGED_OPTIONS:
func = self.CHANGED_OPTIONS[(sectname, k)]
v = func(v)
try: try:
self.set('conf', sectname, k, v, validate=False) self.set('conf', sectname, k, v, validate=False)
except configexc.NoOptionError: except configexc.NoOptionError:

View File

@ -100,9 +100,13 @@ SECTION_DESC = {
" * `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or " " * `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or "
"percentages)\n" "percentages)\n"
" * `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359)\n" " * `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359)\n"
" * A gradient as explained in http://qt-project.org/doc/qt-4.8/" " * A gradient as explained in http://doc.qt.io/qt-5/"
"stylesheet-reference.html#list-of-property-types[the Qt " "stylesheet-reference.html#list-of-property-types[the Qt "
"documentation] under ``Gradient''.\n\n" "documentation] under ``Gradient''.\n\n"
"A *.system value determines the color system to use for color "
"interpolation between similarly-named *.start and *.stop entries, "
"regardless of how they are defined in the options. "
"Valid values are 'rgb', 'hsv', and 'hsl'.\n\n"
"The `hints.*` values are a special case as they're real CSS " "The `hints.*` values are a special case as they're real CSS "
"colors, not Qt-CSS colors. There, for a gradient, you need to use " "colors, not Qt-CSS colors. There, for a gradient, you need to use "
"`-webkit-gradient`, see https://www.webkit.org/blog/175/introducing-" "`-webkit-gradient`, see https://www.webkit.org/blog/175/introducing-"
@ -204,7 +208,7 @@ def data(readonly=False):
"be used."), "be used."),
('new-instance-open-target', ('new-instance-open-target',
SettingValue(typ.NewInstanceOpenTarget(), 'window'), SettingValue(typ.NewInstanceOpenTarget(), 'tab'),
"How to open links in an existing instance if a new one is " "How to open links in an existing instance if a new one is "
"launched."), "launched."),
@ -269,8 +273,9 @@ def data(readonly=False):
('user-stylesheet', ('user-stylesheet',
SettingValue(typ.UserStyleSheet(), SettingValue(typ.UserStyleSheet(),
'::-webkit-scrollbar { width: 0px; height: 0px; }'), '::-webkit-scrollbar { width: 0px; height: 0px; }'),
"User stylesheet to use (absolute filename or CSS string). Will " "User stylesheet to use (absolute filename, filename relative to "
"expand environment variables."), "the config directory or CSS string). Will expand environment "
"variables."),
('css-media-type', ('css-media-type',
SettingValue(typ.String(none_ok=True), ''), SettingValue(typ.String(none_ok=True), ''),
@ -351,6 +356,10 @@ def data(readonly=False):
)), )),
('completion', sect.KeyValue( ('completion', sect.KeyValue(
('auto-open',
SettingValue(typ.Bool(), 'true'),
"Automatically open completion when typing."),
('download-path-suggestion', ('download-path-suggestion',
SettingValue(typ.DownloadPathSuggestion(), 'path'), SettingValue(typ.DownloadPathSuggestion(), 'path'),
"What to display in the download filename input."), "What to display in the download filename input."),
@ -468,7 +477,7 @@ def data(readonly=False):
('last-close', ('last-close',
SettingValue(typ.LastClose(), 'ignore'), SettingValue(typ.LastClose(), 'ignore'),
"Behaviour when the last tab is closed."), "Behavior when the last tab is closed."),
('hide-auto', ('hide-auto',
SettingValue(typ.Bool(), 'false'), SettingValue(typ.Bool(), 'false'),
@ -678,8 +687,8 @@ def data(readonly=False):
"local urls."), "local urls."),
('cookies-accept', ('cookies-accept',
SettingValue(typ.AcceptCookies(), 'default'), SettingValue(typ.AcceptCookies(), 'no-3rdparty'),
"Whether to accept cookies."), "Control which cookies to accept."),
('cookies-store', ('cookies-store',
SettingValue(typ.Bool(), 'true'), SettingValue(typ.Bool(), 'true'),
@ -744,7 +753,8 @@ def data(readonly=False):
('next-regexes', ('next-regexes',
SettingValue(typ.RegexList(flags=re.IGNORECASE), SettingValue(typ.RegexList(flags=re.IGNORECASE),
r'\bnext\b,\bmore\b,\bnewer\b,\b[>→≫]\b,\b(>>|»)\b'), r'\bnext\b,\bmore\b,\bnewer\b,\b[>→≫]\b,\b(>>|»)\b,'
r'\bcontinue\b'),
"A comma-separated list of regexes to use for 'next' links."), "A comma-separated list of regexes to use for 'next' links."),
('prev-regexes', ('prev-regexes',
@ -820,34 +830,67 @@ def data(readonly=False):
SettingValue(typ.QssColor(), '#ff4444'), SettingValue(typ.QssColor(), '#ff4444'),
"Foreground color of the matched text in the completion."), "Foreground color of the matched text in the completion."),
('statusbar.fg',
SettingValue(typ.QssColor(), 'white'),
"Foreground color of the statusbar."),
('statusbar.bg', ('statusbar.bg',
SettingValue(typ.QssColor(), 'black'), SettingValue(typ.QssColor(), 'black'),
"Foreground color of the statusbar."), "Foreground color of the statusbar."),
('statusbar.fg', ('statusbar.fg.error',
SettingValue(typ.QssColor(), 'white'), SettingValue(typ.QssColor(), '${statusbar.fg}'),
"Foreground color of the statusbar."), "Foreground color of the statusbar if there was an error."),
('statusbar.bg.error', ('statusbar.bg.error',
SettingValue(typ.QssColor(), 'red'), SettingValue(typ.QssColor(), 'red'),
"Background color of the statusbar if there was an error."), "Background color of the statusbar if there was an error."),
('statusbar.fg.warning',
SettingValue(typ.QssColor(), '${statusbar.fg}'),
"Foreground color of the statusbar if there is a warning."),
('statusbar.bg.warning', ('statusbar.bg.warning',
SettingValue(typ.QssColor(), 'darkorange'), SettingValue(typ.QssColor(), 'darkorange'),
"Background color of the statusbar if there is a warning."), "Background color of the statusbar if there is a warning."),
('statusbar.fg.prompt',
SettingValue(typ.QssColor(), '${statusbar.fg}'),
"Foreground color of the statusbar if there is a prompt."),
('statusbar.bg.prompt', ('statusbar.bg.prompt',
SettingValue(typ.QssColor(), 'darkblue'), SettingValue(typ.QssColor(), 'darkblue'),
"Background color of the statusbar if there is a prompt."), "Background color of the statusbar if there is a prompt."),
('statusbar.fg.insert',
SettingValue(typ.QssColor(), '${statusbar.fg}'),
"Foreground color of the statusbar in insert mode."),
('statusbar.bg.insert', ('statusbar.bg.insert',
SettingValue(typ.QssColor(), 'darkgreen'), SettingValue(typ.QssColor(), 'darkgreen'),
"Background color of the statusbar in insert mode."), "Background color of the statusbar in insert mode."),
('statusbar.fg.command',
SettingValue(typ.QssColor(), '${statusbar.fg}'),
"Foreground color of the statusbar in command mode."),
('statusbar.bg.command',
SettingValue(typ.QssColor(), '${statusbar.bg}'),
"Background color of the statusbar in command mode."),
('statusbar.fg.caret',
SettingValue(typ.QssColor(), '${statusbar.fg}'),
"Foreground color of the statusbar in caret mode."),
('statusbar.bg.caret', ('statusbar.bg.caret',
SettingValue(typ.QssColor(), 'purple'), SettingValue(typ.QssColor(), 'purple'),
"Background color of the statusbar in caret mode."), "Background color of the statusbar in caret mode."),
('statusbar.fg.caret-selection',
SettingValue(typ.QssColor(), '${statusbar.fg}'),
"Foreground color of the statusbar in caret mode with a "
"selection"),
('statusbar.bg.caret-selection', ('statusbar.bg.caret-selection',
SettingValue(typ.QssColor(), '#a12dff'), SettingValue(typ.QssColor(), '#a12dff'),
"Background color of the statusbar in caret mode with a " "Background color of the statusbar in caret mode with a "
@ -884,22 +927,22 @@ def data(readonly=False):
SettingValue(typ.QtColor(), 'white'), SettingValue(typ.QtColor(), 'white'),
"Foreground color of unselected odd tabs."), "Foreground color of unselected odd tabs."),
('tabs.fg.even',
SettingValue(typ.QtColor(), 'white'),
"Foreground color of unselected even tabs."),
('tabs.fg.selected',
SettingValue(typ.QtColor(), 'white'),
"Foreground color of selected tabs."),
('tabs.bg.odd', ('tabs.bg.odd',
SettingValue(typ.QtColor(), 'grey'), SettingValue(typ.QtColor(), 'grey'),
"Background color of unselected odd tabs."), "Background color of unselected odd tabs."),
('tabs.fg.even',
SettingValue(typ.QtColor(), 'white'),
"Foreground color of unselected even tabs."),
('tabs.bg.even', ('tabs.bg.even',
SettingValue(typ.QtColor(), 'darkgrey'), SettingValue(typ.QtColor(), 'darkgrey'),
"Background color of unselected even tabs."), "Background color of unselected even tabs."),
('tabs.fg.selected',
SettingValue(typ.QtColor(), 'white'),
"Foreground color of selected tabs."),
('tabs.bg.selected', ('tabs.bg.selected',
SettingValue(typ.QtColor(), 'black'), SettingValue(typ.QtColor(), 'black'),
"Background color of selected tabs."), "Background color of selected tabs."),
@ -928,10 +971,6 @@ def data(readonly=False):
SettingValue(typ.CssColor(), 'black'), SettingValue(typ.CssColor(), 'black'),
"Font color for hints."), "Font color for hints."),
('hints.fg.match',
SettingValue(typ.CssColor(), 'green'),
"Font color for the matched part of hints."),
('hints.bg', ('hints.bg',
SettingValue( SettingValue(
typ.CssColor(), '-webkit-gradient(linear, left top, ' typ.CssColor(), '-webkit-gradient(linear, left top, '
@ -939,25 +978,41 @@ def data(readonly=False):
'color-stop(100%,#FFC542))'), 'color-stop(100%,#FFC542))'),
"Background color for hints."), "Background color for hints."),
('downloads.fg', ('hints.fg.match',
SettingValue(typ.QtColor(), '#ffffff'), SettingValue(typ.CssColor(), 'green'),
"Foreground color for downloads."), "Font color for the matched part of hints."),
('downloads.bg.bar', ('downloads.bg.bar',
SettingValue(typ.QssColor(), 'black'), SettingValue(typ.QssColor(), 'black'),
"Background color for the download bar."), "Background color for the download bar."),
('downloads.fg.start',
SettingValue(typ.QtColor(), 'white'),
"Color gradient start for download text."),
('downloads.bg.start', ('downloads.bg.start',
SettingValue(typ.QtColor(), '#0000aa'), SettingValue(typ.QtColor(), '#0000aa'),
"Color gradient start for downloads."), "Color gradient start for download backgrounds."),
('downloads.fg.stop',
SettingValue(typ.QtColor(), '${downloads.fg.start}'),
"Color gradient end for download text."),
('downloads.bg.stop', ('downloads.bg.stop',
SettingValue(typ.QtColor(), '#00aa00'), SettingValue(typ.QtColor(), '#00aa00'),
"Color gradient end for downloads."), "Color gradient stop for download backgrounds."),
('downloads.fg.system',
SettingValue(typ.ColorSystem(), 'rgb'),
"Color gradient interpolation system for download text."),
('downloads.bg.system', ('downloads.bg.system',
SettingValue(typ.ColorSystem(), 'rgb'), SettingValue(typ.ColorSystem(), 'rgb'),
"Color gradient interpolation system for downloads."), "Color gradient interpolation system for download backgrounds."),
('downloads.fg.error',
SettingValue(typ.QtColor(), 'white'),
"Foreground color for downloads with errors."),
('downloads.bg.error', ('downloads.bg.error',
SettingValue(typ.QtColor(), 'red'), SettingValue(typ.QtColor(), 'red'),
@ -1147,7 +1202,7 @@ KEY_DATA = collections.OrderedDict([
])), ])),
('normal', collections.OrderedDict([ ('normal', collections.OrderedDict([
('search', ['<Escape>']), ('search ;; clear-keychain', ['<Escape>']),
('set-cmd-text -s :open', ['o']), ('set-cmd-text -s :open', ['o']),
('set-cmd-text :open {url}', ['go']), ('set-cmd-text :open {url}', ['go']),
('set-cmd-text -s :open -t', ['O']), ('set-cmd-text -s :open -t', ['O']),
@ -1208,6 +1263,8 @@ KEY_DATA = collections.OrderedDict([
('yank -s', ['yY']), ('yank -s', ['yY']),
('yank -t', ['yt']), ('yank -t', ['yt']),
('yank -ts', ['yT']), ('yank -ts', ['yT']),
('yank -d', ['yd']),
('yank -ds', ['yD']),
('paste', ['pp']), ('paste', ['pp']),
('paste -s', ['pP']), ('paste -s', ['pP']),
('paste -t', ['Pp']), ('paste -t', ['Pp']),
@ -1299,7 +1356,7 @@ KEY_DATA = collections.OrderedDict([
('rl-unix-line-discard', ['<Ctrl-U>']), ('rl-unix-line-discard', ['<Ctrl-U>']),
('rl-kill-line', ['<Ctrl-K>']), ('rl-kill-line', ['<Ctrl-K>']),
('rl-kill-word', ['<Alt-D>']), ('rl-kill-word', ['<Alt-D>']),
('rl-unix-word-rubout', ['<Ctrl-W>']), ('rl-unix-word-rubout', ['<Ctrl-W>', '<Alt-Backspace>']),
('rl-yank', ['<Ctrl-Y>']), ('rl-yank', ['<Ctrl-Y>']),
('rl-delete-char', ['<Ctrl-?>']), ('rl-delete-char', ['<Ctrl-?>']),
('rl-backward-delete-char', ['<Ctrl-H>']), ('rl-backward-delete-char', ['<Ctrl-H>']),
@ -1342,8 +1399,8 @@ CHANGED_KEY_COMMANDS = [
(re.compile(r'^download-page$'), r'download'), (re.compile(r'^download-page$'), r'download'),
(re.compile(r'^cancel-download$'), r'download-cancel'), (re.compile(r'^cancel-download$'), r'download-cancel'),
(re.compile(r'^search ""$'), r'search'), (re.compile(r"""^search (''|"")$"""), r'search ;; clear-keychain'),
(re.compile(r"^search ''$"), r'search'), (re.compile(r'^search$'), r'search ;; clear-keychain'),
(re.compile(r"""^set-cmd-text ['"](.*) ['"]$"""), r'set-cmd-text -s \1'), (re.compile(r"""^set-cmd-text ['"](.*) ['"]$"""), r'set-cmd-text -s \1'),
(re.compile(r"""^set-cmd-text ['"](.*)['"]$"""), r'set-cmd-text \1'), (re.compile(r"""^set-cmd-text ['"](.*)['"]$"""), r'set-cmd-text \1'),

View File

@ -34,6 +34,7 @@ from PyQt5.QtWidgets import QTabWidget, QTabBar
from qutebrowser.commands import cmdutils from qutebrowser.commands import cmdutils
from qutebrowser.config import configexc from qutebrowser.config import configexc
from qutebrowser.utils import standarddir
SYSTEM_PROXY = object() # Return value for Proxy type SYSTEM_PROXY = object() # Return value for Proxy type
@ -798,6 +799,17 @@ class File(BaseType):
typestr = 'file' typestr = 'file'
def transform(self, value):
if not value:
return None
value = os.path.expanduser(value)
value = os.path.expandvars(value)
if not os.path.isabs(value):
cfgdir = standarddir.config()
if cfgdir is not None:
return os.path.join(cfgdir, value)
return value
def validate(self, value): def validate(self, value):
if not value: if not value:
if self._none_ok: if self._none_ok:
@ -805,20 +817,26 @@ class File(BaseType):
else: else:
raise configexc.ValidationError(value, "may not be empty!") raise configexc.ValidationError(value, "may not be empty!")
value = os.path.expanduser(value) value = os.path.expanduser(value)
value = os.path.expandvars(value)
try: try:
if not os.path.isfile(value):
raise configexc.ValidationError(value, "must be a valid file!")
if not os.path.isabs(value): if not os.path.isabs(value):
cfgdir = standarddir.config()
if cfgdir is None:
raise configexc.ValidationError(
value, "must be an absolute path when not using a "
"config directory!")
elif not os.path.isfile(os.path.join(cfgdir, value)):
raise configexc.ValidationError(
value, "must be a valid path relative to the config "
"directory!")
else:
return
elif not os.path.isfile(value):
raise configexc.ValidationError( raise configexc.ValidationError(
value, "must be an absolute path!") value, "must be a valid file!")
except UnicodeEncodeError as e: except UnicodeEncodeError as e:
raise configexc.ValidationError(value, e) raise configexc.ValidationError(value, e)
def transform(self, value):
if not value:
return None
return os.path.expanduser(value)
class Directory(BaseType): class Directory(BaseType):
@ -1092,8 +1110,15 @@ class SearchEngineUrl(BaseType):
return return
else: else:
raise configexc.ValidationError(value, "may not be empty!") raise configexc.ValidationError(value, "may not be empty!")
if '{}' not in value: if '{}' not in value:
raise configexc.ValidationError(value, "must contain \"{}\"") raise configexc.ValidationError(value, "must contain \"{}\"")
try:
value.format("")
except KeyError:
raise configexc.ValidationError(
value, "may not contain {...} (use {{ and }} for literal {/})")
url = QUrl(value.replace('{}', 'foobar')) url = QUrl(value.replace('{}', 'foobar'))
if not url.isValid(): if not url.isValid():
raise configexc.ValidationError(value, "invalid url, {}".format( raise configexc.ValidationError(value, "invalid url, {}".format(
@ -1151,6 +1176,16 @@ class UserStyleSheet(File):
def __init__(self): def __init__(self):
super().__init__(none_ok=True) super().__init__(none_ok=True)
def transform(self, value):
if not value:
return None
path = super().transform(value)
if os.path.exists(path):
return QUrl.fromLocalFile(path)
else:
data = base64.b64encode(value.encode('utf-8')).decode('ascii')
return QUrl("data:text/css;charset=utf-8;base64,{}".format(data))
def validate(self, value): def validate(self, value):
if not value: if not value:
if self._none_ok: if self._none_ok:
@ -1160,31 +1195,17 @@ class UserStyleSheet(File):
value = os.path.expandvars(value) value = os.path.expandvars(value)
value = os.path.expanduser(value) value = os.path.expanduser(value)
try: try:
if not os.path.isabs(value): super().validate(value)
# probably a CSS, so we don't handle it as filename. except configexc.ValidationError:
# FIXME We just try if it is encodable, maybe we should try:
# validate CSS? if not os.path.isabs(value):
# https://github.com/The-Compiler/qutebrowser/issues/115 # probably a CSS, so we don't handle it as filename.
try: # FIXME We just try if it is encodable, maybe we should
# validate CSS?
# https://github.com/The-Compiler/qutebrowser/issues/115
value.encode('utf-8') value.encode('utf-8')
except UnicodeEncodeError as e: except UnicodeEncodeError as e:
raise configexc.ValidationError(value, str(e)) raise configexc.ValidationError(value, str(e))
return
elif not os.path.isfile(value):
raise configexc.ValidationError(value, "must be a valid file!")
except UnicodeEncodeError as e:
raise configexc.ValidationError(value, e)
def transform(self, value):
path = os.path.expandvars(value)
path = os.path.expanduser(path)
if not value:
return None
elif os.path.isabs(path):
return QUrl.fromLocalFile(path)
else:
data = base64.b64encode(value.encode('utf-8')).decode('ascii')
return QUrl("data:text/css;charset=utf-8;base64,{}".format(data))
class AutoSearch(BaseType): class AutoSearch(BaseType):
@ -1312,7 +1333,7 @@ class SelectOnRemove(BaseType):
class LastClose(BaseType): class LastClose(BaseType):
"""Behaviour when the last tab is closed.""" """Behavior when the last tab is closed."""
valid_values = ValidValues(('ignore', "Don't do anything."), valid_values = ValidValues(('ignore', "Don't do anything."),
('blank', "Load a blank page."), ('blank', "Load a blank page."),
@ -1323,9 +1344,14 @@ class LastClose(BaseType):
class AcceptCookies(BaseType): class AcceptCookies(BaseType):
"""Whether to accept a cookie.""" """Control which cookies to accept."""
valid_values = ValidValues(('default', "Default QtWebKit behavior."), valid_values = ValidValues(('all', "Accept all cookies."),
('no-3rdparty', "Accept cookies from the same"
" origin only."),
('no-unknown-3rdparty', "Accept cookies from "
"the same origin only, unless a cookie is "
"already set for the domain."),
('never', "Don't accept cookies at all.")) ('never', "Don't accept cookies at all."))

View File

@ -238,6 +238,25 @@ class GlobalSetter(Setter):
self._setter(*args) self._setter(*args)
class CookiePolicy(Base):
"""The ThirdPartyCookiePolicy setting is different from other settings."""
MAPPING = {
'all': QWebSettings.AlwaysAllowThirdPartyCookies,
'no-3rdparty': QWebSettings.AlwaysBlockThirdPartyCookies,
'never': QWebSettings.AlwaysBlockThirdPartyCookies,
'no-unknown-3rdparty': QWebSettings.AllowThirdPartyWithExistingCookies,
}
def get(self, qws=None):
return config.get('content', 'cookies-accept')
def _set(self, value, qws=None):
QWebSettings.globalSettings().setThirdPartyCookiePolicy(
self.MAPPING[value])
MAPPINGS = { MAPPINGS = {
'content': { 'content': {
'allow-images': 'allow-images':
@ -264,6 +283,8 @@ MAPPINGS = {
Attribute(QWebSettings.LocalContentCanAccessRemoteUrls), Attribute(QWebSettings.LocalContentCanAccessRemoteUrls),
'local-content-can-access-file-urls': 'local-content-can-access-file-urls':
Attribute(QWebSettings.LocalContentCanAccessFileUrls), Attribute(QWebSettings.LocalContentCanAccessFileUrls),
'cookies-accept':
CookiePolicy(),
}, },
'network': { 'network': {
'dns-prefetch': 'dns-prefetch':

View File

@ -14,10 +14,12 @@ pre { margin: 2px; }
th, td { border: 1px solid grey; padding: 0px 5px; } th, td { border: 1px solid grey; padding: 0px 5px; }
th { background: lightgrey; } th { background: lightgrey; }
th pre { color: grey; text-align: left; } th pre { color: grey; text-align: left; }
.noscript, .noscript-text { color:red; }
.noscript-text { margin-bottom: 5cm; }
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<noscript><h1>View Only</h1><p>Changing settings requires javascript to be enabled</p></noscript> <noscript><h1 class="noscript">View Only</h1><p class="noscript-text">Changing settings requires javascript to be enabled!</p></noscript>
<header><h1>{{ title }}</h1></header> <header><h1>{{ title }}</h1></header>
<table> <table>
{% for section in config.DATA %} {% for section in config.DATA %}

View File

@ -23,7 +23,7 @@ import re
import functools import functools
import unicodedata import unicodedata
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QObject from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import usertypes, log, utils, objreg from qutebrowser.utils import usertypes, log, utils, objreg
@ -49,6 +49,8 @@ class BaseKeyParser(QObject):
special: execute() was called via a special key binding special: execute() was called via a special key binding
do_log: Whether to log keypresses or not. do_log: Whether to log keypresses or not.
passthrough: Whether unbound keys should be passed through with this
handler.
Attributes: Attributes:
bindings: Bound key bindings bindings: Bound key bindings
@ -69,6 +71,7 @@ class BaseKeyParser(QObject):
keystring_updated = pyqtSignal(str) keystring_updated = pyqtSignal(str)
do_log = True do_log = True
passthrough = False
Match = usertypes.enum('Match', ['partial', 'definitive', 'ambiguous', Match = usertypes.enum('Match', ['partial', 'definitive', 'ambiguous',
'other', 'none']) 'other', 'none'])
@ -162,12 +165,6 @@ class BaseKeyParser(QObject):
key = e.key() key = e.key()
self._debug_log("Got key: 0x{:x} / text: '{}'".format(key, txt)) self._debug_log("Got key: 0x{:x} / text: '{}'".format(key, txt))
if key == Qt.Key_Escape:
self._debug_log("Escape pressed, discarding '{}'.".format(
self._keystring))
self._keystring = ''
return self.Match.none
if len(txt) == 1: if len(txt) == 1:
category = unicodedata.category(txt) category = unicodedata.category(txt)
is_control_char = (category == 'Cc') is_control_char = (category == 'Cc')
@ -198,7 +195,7 @@ class BaseKeyParser(QObject):
self._keystring = '' self._keystring = ''
self.execute(binding, self.Type.chain, count) self.execute(binding, self.Type.chain, count)
elif match == self.Match.ambiguous: elif match == self.Match.ambiguous:
self._debug_log("Ambigious match for '{}'.".format( self._debug_log("Ambiguous match for '{}'.".format(
self._keystring)) self._keystring))
self._handle_ambiguous_match(binding, count) self._handle_ambiguous_match(binding, count)
elif match == self.Match.partial: elif match == self.Match.partial:
@ -303,6 +300,7 @@ class BaseKeyParser(QObject):
True if the event was handled, False otherwise. True if the event was handled, False otherwise.
""" """
handled = self._handle_special_key(e) handled = self._handle_special_key(e)
if handled or not self._supports_chains: if handled or not self._supports_chains:
return handled return handled
match = self._handle_single_key(e) match = self._handle_single_key(e)
@ -359,3 +357,9 @@ class BaseKeyParser(QObject):
"defined!") "defined!")
if mode == self._modename: if mode == self._modename:
self.read_config() self.read_config()
def clear_keystring(self):
"""Clear the currently entered key sequence."""
self._debug_log("discarding keystring '{}'.".format(self._keystring))
self._keystring = ''
self.keystring_updated.emit(self._keystring)

View File

@ -55,6 +55,7 @@ class PassthroughKeyParser(CommandKeyParser):
""" """
do_log = False do_log = False
passthrough = True
def __init__(self, win_id, mode, parent=None, warn=True): def __init__(self, win_id, mode, parent=None, warn=True):
"""Constructor. """Constructor.

View File

@ -84,38 +84,30 @@ def init(win_id, parent):
modeman.destroyed.connect( modeman.destroyed.connect(
functools.partial(objreg.delete, 'keyparsers', scope='window', functools.partial(objreg.delete, 'keyparsers', scope='window',
window=win_id)) window=win_id))
modeman.register(KM.normal, keyparsers[KM.normal].handle) for mode, parser in keyparsers.items():
modeman.register(KM.hint, keyparsers[KM.hint].handle) modeman.register(mode, parser)
modeman.register(KM.insert, keyparsers[KM.insert].handle, passthrough=True)
modeman.register(KM.passthrough, keyparsers[KM.passthrough].handle,
passthrough=True)
modeman.register(KM.command, keyparsers[KM.command].handle,
passthrough=True)
modeman.register(KM.prompt, keyparsers[KM.prompt].handle, passthrough=True)
modeman.register(KM.yesno, keyparsers[KM.yesno].handle)
modeman.register(KM.caret, keyparsers[KM.caret].handle, passthrough=True)
return modeman return modeman
def _get_modeman(win_id): def instance(win_id):
"""Get a modemanager object.""" """Get a modemanager object."""
return objreg.get('mode-manager', scope='window', window=win_id) return objreg.get('mode-manager', scope='window', window=win_id)
def enter(win_id, mode, reason=None, only_if_normal=False): def enter(win_id, mode, reason=None, only_if_normal=False):
"""Enter the mode 'mode'.""" """Enter the mode 'mode'."""
_get_modeman(win_id).enter(mode, reason, only_if_normal) instance(win_id).enter(mode, reason, only_if_normal)
def leave(win_id, mode, reason=None): def leave(win_id, mode, reason=None):
"""Leave the mode 'mode'.""" """Leave the mode 'mode'."""
_get_modeman(win_id).leave(mode, reason) instance(win_id).leave(mode, reason)
def maybe_leave(win_id, mode, reason=None): def maybe_leave(win_id, mode, reason=None):
"""Convenience method to leave 'mode' without exceptions.""" """Convenience method to leave 'mode' without exceptions."""
try: try:
_get_modeman(win_id).leave(mode, reason) instance(win_id).leave(mode, reason)
except NotInModeError as e: except NotInModeError as e:
# This is rather likely to happen, so we only log to debug log. # This is rather likely to happen, so we only log to debug log.
log.modes.debug("{} (leave reason: {})".format(e, reason)) log.modes.debug("{} (leave reason: {})".format(e, reason))
@ -126,10 +118,9 @@ class ModeManager(QObject):
"""Manager for keyboard modes. """Manager for keyboard modes.
Attributes: Attributes:
passthrough: A list of modes in which to pass through events.
mode: The mode we're currently in. mode: The mode we're currently in.
_win_id: The window ID of this ModeManager _win_id: The window ID of this ModeManager
_handlers: A dictionary of modes and their handlers. _parsers: A dictionary of modes and their keyparsers.
_forward_unbound_keys: If we should forward unbound keys. _forward_unbound_keys: If we should forward unbound keys.
_releaseevents_to_pass: A set of KeyEvents where the keyPressEvent was _releaseevents_to_pass: A set of KeyEvents where the keyPressEvent was
passed through, so the release event should as passed through, so the release event should as
@ -151,8 +142,7 @@ class ModeManager(QObject):
def __init__(self, win_id, parent=None): def __init__(self, win_id, parent=None):
super().__init__(parent) super().__init__(parent)
self._win_id = win_id self._win_id = win_id
self._handlers = {} self._parsers = {}
self.passthrough = []
self.mode = usertypes.KeyMode.normal self.mode = usertypes.KeyMode.normal
self._releaseevents_to_pass = set() self._releaseevents_to_pass = set()
self._forward_unbound_keys = config.get( self._forward_unbound_keys = config.get(
@ -160,8 +150,7 @@ class ModeManager(QObject):
objreg.get('config').changed.connect(self.set_forward_unbound_keys) objreg.get('config').changed.connect(self.set_forward_unbound_keys)
def __repr__(self): def __repr__(self):
return utils.get_repr(self, mode=self.mode, return utils.get_repr(self, mode=self.mode)
passthrough=self.passthrough)
def _eventFilter_keypress(self, event): def _eventFilter_keypress(self, event):
"""Handle filtering of KeyPress events. """Handle filtering of KeyPress events.
@ -173,11 +162,11 @@ class ModeManager(QObject):
True if event should be filtered, False otherwise. True if event should be filtered, False otherwise.
""" """
curmode = self.mode curmode = self.mode
handler = self._handlers[curmode] parser = self._parsers[curmode]
if curmode != usertypes.KeyMode.insert: if curmode != usertypes.KeyMode.insert:
log.modes.debug("got keypress in mode {} - calling handler " log.modes.debug("got keypress in mode {} - delegating to "
"{}".format(curmode, utils.qualname(handler))) "{}".format(curmode, utils.qualname(parser)))
handled = handler(event) if handler is not None else False handled = parser.handle(event)
is_non_alnum = bool(event.modifiers()) or not event.text().strip() is_non_alnum = bool(event.modifiers()) or not event.text().strip()
focus_widget = QApplication.instance().focusWidget() focus_widget = QApplication.instance().focusWidget()
@ -187,7 +176,7 @@ class ModeManager(QObject):
filter_this = True filter_this = True
elif is_tab and not isinstance(focus_widget, QWebView): elif is_tab and not isinstance(focus_widget, QWebView):
filter_this = True filter_this = True
elif (curmode in self.passthrough or elif (parser.passthrough or
self._forward_unbound_keys == 'all' or self._forward_unbound_keys == 'all' or
(self._forward_unbound_keys == 'auto' and is_non_alnum)): (self._forward_unbound_keys == 'auto' and is_non_alnum)):
filter_this = False filter_this = False
@ -202,8 +191,8 @@ class ModeManager(QObject):
"passthrough: {}, is_non_alnum: {}, is_tab {} --> " "passthrough: {}, is_non_alnum: {}, is_tab {} --> "
"filter: {} (focused: {!r})".format( "filter: {} (focused: {!r})".format(
handled, self._forward_unbound_keys, handled, self._forward_unbound_keys,
curmode in self.passthrough, is_non_alnum, parser.passthrough, is_non_alnum, is_tab,
is_tab, filter_this, focus_widget)) filter_this, focus_widget))
return filter_this return filter_this
def _eventFilter_keyrelease(self, event): def _eventFilter_keyrelease(self, event):
@ -226,20 +215,16 @@ class ModeManager(QObject):
log.modes.debug("filter: {}".format(filter_this)) log.modes.debug("filter: {}".format(filter_this))
return filter_this return filter_this
def register(self, mode, handler, passthrough=False): def register(self, mode, parser):
"""Register a new mode. """Register a new mode.
Args: Args:
mode: The name of the mode. mode: The name of the mode.
handler: Handler for keyPressEvents. parser: The KeyParser which should be used.
passthrough: Whether to pass key bindings in this mode through to
the widgets.
""" """
if not isinstance(mode, usertypes.KeyMode): assert isinstance(mode, usertypes.KeyMode)
raise TypeError("Mode {} is no KeyMode member!".format(mode)) assert parser is not None
self._handlers[mode] = handler self._parsers[mode] = parser
if passthrough:
self.passthrough.append(mode)
def enter(self, mode, reason=None, only_if_normal=False): def enter(self, mode, reason=None, only_if_normal=False):
"""Enter a new mode. """Enter a new mode.
@ -253,8 +238,8 @@ class ModeManager(QObject):
raise TypeError("Mode {} is no KeyMode member!".format(mode)) raise TypeError("Mode {} is no KeyMode member!".format(mode))
log.modes.debug("Entering mode {}{}".format( log.modes.debug("Entering mode {}{}".format(
mode, '' if reason is None else ' (reason: {})'.format(reason))) mode, '' if reason is None else ' (reason: {})'.format(reason)))
if mode not in self._handlers: if mode not in self._parsers:
raise ValueError("No handler for mode {}".format(mode)) raise ValueError("No keyparser for mode {}".format(mode))
prompt_modes = (usertypes.KeyMode.prompt, usertypes.KeyMode.yesno) prompt_modes = (usertypes.KeyMode.prompt, usertypes.KeyMode.yesno)
if self.mode == mode or (self.mode in prompt_modes and if self.mode == mode or (self.mode in prompt_modes and
mode in prompt_modes): mode in prompt_modes):
@ -332,3 +317,8 @@ class ModeManager(QObject):
return self._eventFilter_keypress(event) return self._eventFilter_keypress(event)
else: else:
return self._eventFilter_keyrelease(event) return self._eventFilter_keyrelease(event)
@cmdutils.register(instance='mode-manager', scope='window', hide=True)
def clear_keychain(self):
"""Clear the currently entered key chain."""
self._parsers[self.mode].clear_keystring()

View File

@ -224,6 +224,8 @@ class CaretKeyParser(keyparser.CommandKeyParser):
"""KeyParser for caret mode.""" """KeyParser for caret mode."""
passthrough = True
def __init__(self, win_id, parent=None): def __init__(self, win_id, parent=None):
super().__init__(win_id, parent, supports_count=True, super().__init__(win_id, parent, supports_count=True,
supports_chains=True) supports_chains=True)

View File

@ -120,14 +120,6 @@ class MainWindow(QWidget):
window=self.win_id) window=self.win_id)
self.setWindowTitle('qutebrowser') self.setWindowTitle('qutebrowser')
if geometry is not None:
self._load_geometry(geometry)
elif self.win_id == 0:
self._load_state_geometry()
else:
self._set_default_geometry()
log.init.debug("Initial main window geometry: {}".format(
self.geometry()))
self._vbox = QVBoxLayout(self) self._vbox = QVBoxLayout(self)
self._vbox.setContentsMargins(0, 0, 0, 0) self._vbox.setContentsMargins(0, 0, 0, 0)
self._vbox.setSpacing(0) self._vbox.setSpacing(0)
@ -165,6 +157,15 @@ class MainWindow(QWidget):
log.init.debug("Initializing modes...") log.init.debug("Initializing modes...")
modeman.init(self.win_id, self) modeman.init(self.win_id, self)
if geometry is not None:
self._load_geometry(geometry)
elif self.win_id == 0:
self._load_state_geometry()
else:
self._set_default_geometry()
log.init.debug("Initial main window geometry: {}".format(
self.geometry()))
self._connect_signals() self._connect_signals()
# When we're here the statusbar might not even really exist yet, so # When we're here the statusbar might not even really exist yet, so

View File

@ -78,6 +78,11 @@ class StatusBar(QWidget):
For some reason we need to have this as class attribute For some reason we need to have this as class attribute
so pyqtProperty works correctly. so pyqtProperty works correctly.
_command_active: If we're currently in command mode.
For some reason we need to have this as class
attribute so pyqtProperty works correctly.
_caret_mode: The current caret mode (off/on/selection). _caret_mode: The current caret mode (off/on/selection).
For some reason we need to have this as class attribute For some reason we need to have this as class attribute
@ -97,41 +102,68 @@ class StatusBar(QWidget):
_severity = None _severity = None
_prompt_active = False _prompt_active = False
_insert_active = False _insert_active = False
_command_active = False
_caret_mode = CaretMode.off _caret_mode = CaretMode.off
STYLESHEET = """ STYLESHEET = """
QWidget#StatusBar {
QWidget#StatusBar,
QWidget#StatusBar QLabel,
QWidget#StatusBar QLineEdit {
{{ font['statusbar'] }}
{{ color['statusbar.bg'] }} {{ color['statusbar.bg'] }}
{{ color['statusbar.fg'] }}
} }
QWidget#StatusBar[insert_active="true"] { QWidget#StatusBar[caret_mode="on"],
{{ color['statusbar.bg.insert'] }} QWidget#StatusBar[caret_mode="on"] QLabel,
} QWidget#StatusBar[caret_mode="on"] QLineEdit {
{{ color['statusbar.fg.caret'] }}
QWidget#StatusBar[caret_mode="on"] {
{{ color['statusbar.bg.caret'] }} {{ color['statusbar.bg.caret'] }}
} }
QWidget#StatusBar[caret_mode="selection"] { QWidget#StatusBar[caret_mode="selection"],
QWidget#StatusBar[caret_mode="selection"] QLabel,
QWidget#StatusBar[caret_mode="selection"] QLineEdit {
{{ color['statusbar.fg.caret-selection'] }}
{{ color['statusbar.bg.caret-selection'] }} {{ color['statusbar.bg.caret-selection'] }}
} }
QWidget#StatusBar[prompt_active="true"] { QWidget#StatusBar[severity="error"],
{{ color['statusbar.bg.prompt'] }} QWidget#StatusBar[severity="error"] QLabel,
} QWidget#StatusBar[severity="error"] QLineEdit {
{{ color['statusbar.fg.error'] }}
QWidget#StatusBar[severity="error"] {
{{ color['statusbar.bg.error'] }} {{ color['statusbar.bg.error'] }}
} }
QWidget#StatusBar[severity="warning"] { QWidget#StatusBar[severity="warning"],
QWidget#StatusBar[severity="warning"] QLabel,
QWidget#StatusBar[severity="warning"] QLineEdit {
{{ color['statusbar.fg.warning'] }}
{{ color['statusbar.bg.warning'] }} {{ color['statusbar.bg.warning'] }}
} }
QLabel, QLineEdit { QWidget#StatusBar[prompt_active="true"],
{{ color['statusbar.fg'] }} QWidget#StatusBar[prompt_active="true"] QLabel,
{{ font['statusbar'] }} QWidget#StatusBar[prompt_active="true"] QLineEdit {
{{ color['statusbar.fg.prompt'] }}
{{ color['statusbar.bg.prompt'] }}
} }
QWidget#StatusBar[insert_active="true"],
QWidget#StatusBar[insert_active="true"] QLabel,
QWidget#StatusBar[insert_active="true"] QLineEdit {
{{ color['statusbar.fg.insert'] }}
{{ color['statusbar.bg.insert'] }}
}
QWidget#StatusBar[command_active="true"],
QWidget#StatusBar[command_active="true"] QLabel,
QWidget#StatusBar[command_active="true"] QLineEdit {
{{ color['statusbar.fg.command'] }}
{{ color['statusbar.bg.command'] }}
}
""" """
def __init__(self, win_id, parent=None): def __init__(self, win_id, parent=None):
@ -263,6 +295,11 @@ class StatusBar(QWidget):
self._prompt_active = val self._prompt_active = val
self.setStyleSheet(style.get_stylesheet(self.STYLESHEET)) self.setStyleSheet(style.get_stylesheet(self.STYLESHEET))
@pyqtProperty(bool)
def command_active(self):
"""Getter for self.command_active, so it can be used as Qt property."""
return self._command_active
@pyqtProperty(bool) @pyqtProperty(bool)
def insert_active(self): def insert_active(self):
"""Getter for self.insert_active, so it can be used as Qt property.""" """Getter for self.insert_active, so it can be used as Qt property."""
@ -274,7 +311,7 @@ class StatusBar(QWidget):
return self._caret_mode.name return self._caret_mode.name
def set_mode_active(self, mode, val): def set_mode_active(self, mode, val):
"""Setter for self.{insert,caret}_active. """Setter for self.{insert,command,caret}_active.
Re-set the stylesheet after setting the value, so everything gets Re-set the stylesheet after setting the value, so everything gets
updated by Qt properly. updated by Qt properly.
@ -282,6 +319,9 @@ class StatusBar(QWidget):
if mode == usertypes.KeyMode.insert: if mode == usertypes.KeyMode.insert:
log.statusbar.debug("Setting insert_active to {}".format(val)) log.statusbar.debug("Setting insert_active to {}".format(val))
self._insert_active = val self._insert_active = val
if mode == usertypes.KeyMode.command:
log.statusbar.debug("Setting command_active to {}".format(val))
self._command_active = val
elif mode == usertypes.KeyMode.caret: elif mode == usertypes.KeyMode.caret:
webview = objreg.get('tabbed-browser', scope='window', webview = objreg.get('tabbed-browser', scope='window',
window=self._win_id).currentWidget() window=self._win_id).currentWidget()
@ -469,24 +509,28 @@ class StatusBar(QWidget):
@pyqtSlot(usertypes.KeyMode) @pyqtSlot(usertypes.KeyMode)
def on_mode_entered(self, mode): def on_mode_entered(self, mode):
"""Mark certain modes in the commandline.""" """Mark certain modes in the commandline."""
mode_manager = objreg.get('mode-manager', scope='window', keyparsers = objreg.get('keyparsers', scope='window',
window=self._win_id) window=self._win_id)
if mode in mode_manager.passthrough: if keyparsers[mode].passthrough:
self._set_mode_text(mode.name) self._set_mode_text(mode.name)
if mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret): if mode in (usertypes.KeyMode.insert,
usertypes.KeyMode.command,
usertypes.KeyMode.caret):
self.set_mode_active(mode, True) self.set_mode_active(mode, True)
@pyqtSlot(usertypes.KeyMode, usertypes.KeyMode) @pyqtSlot(usertypes.KeyMode, usertypes.KeyMode)
def on_mode_left(self, old_mode, new_mode): def on_mode_left(self, old_mode, new_mode):
"""Clear marked mode.""" """Clear marked mode."""
mode_manager = objreg.get('mode-manager', scope='window', keyparsers = objreg.get('keyparsers', scope='window',
window=self._win_id) window=self._win_id)
if old_mode in mode_manager.passthrough: if keyparsers[old_mode].passthrough:
if new_mode in mode_manager.passthrough: if keyparsers[new_mode].passthrough:
self._set_mode_text(new_mode.name) self._set_mode_text(new_mode.name)
else: else:
self.txt.set_text(self.txt.Text.normal, '') self.txt.set_text(self.txt.Text.normal, '')
if old_mode in (usertypes.KeyMode.insert, usertypes.KeyMode.caret): if old_mode in (usertypes.KeyMode.insert,
usertypes.KeyMode.command,
usertypes.KeyMode.caret):
self.set_mode_active(old_mode, False) self.set_mode_active(old_mode, False)
@config.change_filter('ui', 'message-timeout') @config.change_filter('ui', 'message-timeout')

View File

@ -70,7 +70,7 @@ class TextBase(QLabel):
More info: More info:
http://stackoverflow.com/q/21890462/2085149 http://stackoverflow.com/q/21890462/2085149
https://bugreports.qt-project.org/browse/QTBUG-36945 https://bugreports.qt.io/browse/QTBUG-36945
https://codereview.qt-project.org/#/c/79181/ https://codereview.qt-project.org/#/c/79181/
Args: Args:

View File

@ -332,7 +332,7 @@ class TabbedBrowser(tabwidget.TabWidget):
the default settings we handle it like Chromium does: the default settings we handle it like Chromium does:
- Tabs from clicked links etc. are to the right of - Tabs from clicked links etc. are to the right of
the current. the current.
- Explicitely opened tabs are at the very right. - Explicitly opened tabs are at the very right.
Return: Return:
The opened WebView instance. The opened WebView instance.

View File

@ -391,9 +391,9 @@ class TabBar(QTabBar):
def paintEvent(self, _e): def paintEvent(self, _e):
"""Override paintEvent to draw the tabs like we want to.""" """Override paintEvent to draw the tabs like we want to."""
p = QStylePainter(self) p = QStylePainter(self)
tab = QStyleOptionTab()
selected = self.currentIndex() selected = self.currentIndex()
for idx in range(self.count()): for idx in range(self.count()):
tab = QStyleOptionTab()
self.initStyleOption(tab, idx) self.initStyleOption(tab, idx)
if idx == selected: if idx == selected:
bg_color = config.get('colors', 'tabs.bg.selected') bg_color = config.get('colors', 'tabs.bg.selected')

View File

@ -24,9 +24,8 @@ import sys
import html import html
import getpass import getpass
import traceback import traceback
import distutils.version # pylint: disable=no-name-in-module,import-error
# https://bitbucket.org/logilab/pylint/issue/73/
import pkg_resources
from PyQt5.QtCore import pyqtSlot, Qt, QSize, qVersion from PyQt5.QtCore import pyqtSlot, Qt, QSize, qVersion
from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton, from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton,
QVBoxLayout, QHBoxLayout, QCheckBox, QVBoxLayout, QHBoxLayout, QCheckBox,
@ -328,8 +327,8 @@ class _CrashDialog(QDialog):
""" """
# pylint: disable=no-member # pylint: disable=no-member
# https://bitbucket.org/logilab/pylint/issue/73/ # https://bitbucket.org/logilab/pylint/issue/73/
new_version = distutils.version.StrictVersion(newest) new_version = pkg_resources.parse_version(newest)
cur_version = distutils.version.StrictVersion(qutebrowser.__version__) cur_version = pkg_resources.parse_version(qutebrowser.__version__)
lines = ['The report has been sent successfully. Thanks!'] lines = ['The report has been sent successfully. Thanks!']
if new_version > cur_version: if new_version > cur_version:
lines.append("<b>Note:</b> The newest available version is v{}, " lines.append("<b>Note:</b> The newest available version is v{}, "

View File

@ -300,6 +300,7 @@ class SignalHandler(QObject):
signal.SIGTERM, self.interrupt) signal.SIGTERM, self.interrupt)
if os.name == 'posix' and hasattr(signal, 'set_wakeup_fd'): if os.name == 'posix' and hasattr(signal, 'set_wakeup_fd'):
# pylint: disable=import-error,no-member
import fcntl import fcntl
read_fd, write_fd = os.pipe() read_fd, write_fd = os.pipe()
for fd in (read_fd, write_fd): for fd in (read_fd, write_fd):

View File

@ -137,10 +137,10 @@ def fix_harfbuzz(args):
- On Qt 5.2 (and probably earlier) the new engine probably has more - On Qt 5.2 (and probably earlier) the new engine probably has more
crashes and is also experimental. crashes and is also experimental.
e.g. https://bugreports.qt-project.org/browse/QTBUG-36099 e.g. https://bugreports.qt.io/browse/QTBUG-36099
- On Qt 5.3.0 there's a bug that affects a lot of websites: - On Qt 5.3.0 there's a bug that affects a lot of websites:
https://bugreports.qt-project.org/browse/QTBUG-39278 https://bugreports.qt.io/browse/QTBUG-39278
So the new engine will be more stable. 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 - On Qt 5.3.1 this bug is fixed and the old engine will be the more stable
@ -213,6 +213,19 @@ def check_qt_version():
_die(text) _die(text)
def check_ssl_support():
"""Check if SSL support is available."""
try:
from PyQt5.QtNetwork import QSslSocket
except ImportError:
ok = False
else:
ok = QSslSocket.supportsSsl()
if not ok:
text = "Fatal error: Your Qt is built without SSL support."
_die(text)
def check_libraries(): def check_libraries():
"""Check if all needed Python libraries are installed.""" """Check if all needed Python libraries are installed."""
modules = { modules = {
@ -288,6 +301,7 @@ def earlyinit(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.
check_qt_version() check_qt_version()
check_ssl_support()
remove_inputhook() remove_inputhook()
check_libraries() check_libraries()
init_log(args) init_log(args)

View File

@ -22,10 +22,11 @@
import os import os
import tempfile import tempfile
from PyQt5.QtCore import pyqtSignal, QProcess, QObject from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QProcess
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import message, log from qutebrowser.utils import message, log
from qutebrowser.misc import guiprocess
class ExternalEditor(QObject): class ExternalEditor(QObject):
@ -36,7 +37,7 @@ class ExternalEditor(QObject):
_text: The current text before the editor is opened. _text: The current text before the editor is opened.
_oshandle: The OS level handle to the tmpfile. _oshandle: The OS level handle to the tmpfile.
_filehandle: The file handle to the tmpfile. _filehandle: The file handle to the tmpfile.
_proc: The QProcess of the editor. _proc: The GUIProcess of the editor.
_win_id: The window ID the ExternalEditor is associated with. _win_id: The window ID the ExternalEditor is associated with.
""" """
@ -69,15 +70,10 @@ class ExternalEditor(QObject):
log.procs.debug("Editor closed") log.procs.debug("Editor closed")
if exitstatus != QProcess.NormalExit: if exitstatus != QProcess.NormalExit:
# No error/cleanup here, since we already handle this in # No error/cleanup here, since we already handle this in
# on_proc_error # on_proc_error.
return return
try: try:
if exitcode != 0: if exitcode != 0:
# NOTE: Do not replace this with "raise CommandError" as it's
# executed async.
message.error(
self._win_id, "Editor did quit abnormally (status "
"{})!".format(exitcode))
return return
encoding = config.get('general', 'editor-encoding') encoding = config.get('general', 'editor-encoding')
try: try:
@ -94,22 +90,8 @@ class ExternalEditor(QObject):
finally: finally:
self._cleanup() self._cleanup()
def on_proc_error(self, error): @pyqtSlot(QProcess.ProcessError)
"""Display an error message and clean up when editor crashed.""" def on_proc_error(self, _err):
messages = {
QProcess.FailedToStart: "The process failed to start.",
QProcess.Crashed: "The process crashed.",
QProcess.Timedout: "The last waitFor...() function timed out.",
QProcess.WriteError: ("An error occurred when attempting to write "
"to the process."),
QProcess.ReadError: ("An error occurred when attempting to read "
"from the process."),
QProcess.UnknownError: "An unknown error occurred.",
}
# NOTE: Do not replace this with "raise CommandError" as it's
# executed async.
message.error(self._win_id,
"Error while calling editor: {}".format(messages[error]))
self._cleanup() self._cleanup()
def edit(self, text): def edit(self, text):
@ -132,7 +114,8 @@ class ExternalEditor(QObject):
message.error(self._win_id, "Failed to create initial file: " message.error(self._win_id, "Failed to create initial file: "
"{}".format(e)) "{}".format(e))
return return
self._proc = QProcess(self) self._proc = guiprocess.GUIProcess(self._win_id, what='editor',
parent=self)
self._proc.finished.connect(self.on_proc_closed) self._proc.finished.connect(self.on_proc_closed)
self._proc.error.connect(self.on_proc_error) self._proc.error.connect(self.on_proc_error)
editor = config.get('general', 'editor') editor = config.get('general', 'editor')

View File

@ -0,0 +1,152 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""A QProcess which shows notifications in the GUI."""
import shlex
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QObject, QProcess,
QProcessEnvironment)
from qutebrowser.utils import message, log
# A mapping of QProcess::ErrorCode's to human-readable strings.
ERROR_STRINGS = {
QProcess.FailedToStart: "The process failed to start.",
QProcess.Crashed: "The process crashed.",
QProcess.Timedout: "The last waitFor...() function timed out.",
QProcess.WriteError: ("An error occurred when attempting to write to the "
"process."),
QProcess.ReadError: ("An error occurred when attempting to read from the "
"process."),
QProcess.UnknownError: "An unknown error occurred.",
}
class GUIProcess(QObject):
"""An external process which shows notifications in the GUI.
Args:
cmd: The command which was started.
args: A list of arguments which gets passed.
_started: Whether the underlying process is started.
_proc: The underlying QProcess.
_win_id: The window ID this process is used in.
_what: What kind of thing is spawned (process/editor/userscript/...).
Used in messages.
_verbose: Whether to show more messages.
Signals:
error/finished/started signals proxied from QProcess.
"""
error = pyqtSignal(QProcess.ProcessError)
finished = pyqtSignal(int, QProcess.ExitStatus)
started = pyqtSignal()
def __init__(self, win_id, what, *, verbose=False, additional_env=None,
parent=None):
super().__init__(parent)
self._win_id = win_id
self._what = what
self._verbose = verbose
self._started = False
self.cmd = None
self.args = None
self._proc = QProcess(self)
self._proc.error.connect(self.on_error)
self._proc.error.connect(self.error)
self._proc.finished.connect(self.on_finished)
self._proc.finished.connect(self.finished)
self._proc.started.connect(self.on_started)
self._proc.started.connect(self.started)
if additional_env is not None:
procenv = QProcessEnvironment.systemEnvironment()
for k, v in additional_env.items():
procenv.insert(k, v)
self._proc.setProcessEnvironment(procenv)
@pyqtSlot(QProcess.ProcessError)
def on_error(self, error):
"""Show a message if there was an error while spawning."""
msg = ERROR_STRINGS[error]
message.error(self._win_id, "Error while spawning {}: {}".format(
self._what, msg), immediately=True)
@pyqtSlot(int, QProcess.ExitStatus)
def on_finished(self, code, status):
"""Show a message when the process finished."""
self._started = False
log.procs.debug("Process finished with code {}, status {}.".format(
code, status))
if status == QProcess.CrashExit:
message.error(self._win_id,
"{} crashed!".format(self._what.capitalize()),
immediately=True)
elif status == QProcess.NormalExit and code == 0:
if self._verbose:
message.info(self._win_id, "{} exited successfully.".format(
self._what.capitalize()))
else:
assert status == QProcess.NormalExit
message.error(self._win_id, "{} exited with status {}.".format(
self._what.capitalize(), code))
@pyqtSlot()
def on_started(self):
"""Called when the process started successfully."""
log.procs.debug("Process started.")
assert not self._started
self._started = True
def _pre_start(self, cmd, args):
"""Prepare starting of a QProcess."""
if self._started:
raise ValueError("Trying to start a running QProcess!")
self.cmd = cmd
self.args = args
if self._verbose:
fake_cmdline = ' '.join(shlex.quote(e) for e in [cmd] + list(args))
message.info(self._win_id, 'Executing: ' + fake_cmdline)
def start(self, cmd, args, mode=None):
"""Convenience wrapper around QProcess::start."""
log.procs.debug("Starting process.")
self._pre_start(cmd, args)
if mode is None:
self._proc.start(cmd, args)
else:
self._proc.start(cmd, args, mode)
def start_detached(self, cmd, args, cwd=None):
"""Convenience wrapper around QProcess::startDetached."""
log.procs.debug("Starting detached.")
self._pre_start(cmd, args)
ok, _pid = self._proc.startDetached(cmd, args, cwd)
if ok:
log.procs.debug("Process started.")
self._started = True
else:
message.error(self._win_id, "Error while spawning {}: {}.".format(
self._what, self._proc.error()), immediately=True)

View File

@ -145,21 +145,23 @@ class SessionManager(QObject):
if item.originalUrl() != item.url(): if item.originalUrl() != item.url():
encoded = item.originalUrl().toEncoded() encoded = item.originalUrl().toEncoded()
item_data['original-url'] = bytes(encoded).decode('ascii') item_data['original-url'] = bytes(encoded).decode('ascii')
user_data = item.userData()
if history.currentItemIndex() == idx: if history.currentItemIndex() == idx:
item_data['active'] = True item_data['active'] = True
if user_data is None:
pos = tab.page().mainFrame().scrollPosition()
data['zoom'] = tab.zoomFactor()
data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()}
data['history'].append(item_data)
if user_data is not None: user_data = item.userData()
if history.currentItemIndex() == idx:
pos = tab.page().mainFrame().scrollPosition()
item_data['zoom'] = tab.zoomFactor()
item_data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()}
elif user_data is not None:
if 'zoom' in user_data: if 'zoom' in user_data:
data['zoom'] = user_data['zoom'] item_data['zoom'] = user_data['zoom']
if 'scroll-pos' in user_data: if 'scroll-pos' in user_data:
pos = user_data['scroll-pos'] pos = user_data['scroll-pos']
data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()} item_data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()}
data['history'].append(item_data)
return data return data
def _save_all(self): def _save_all(self):
@ -235,11 +237,25 @@ class SessionManager(QObject):
entries = [] entries = []
for histentry in data['history']: for histentry in data['history']:
user_data = {} user_data = {}
if 'zoom' in data: if 'zoom' in data:
# The zoom was accidentally stored in 'data' instead of per-tab
# earlier.
# See https://github.com/The-Compiler/qutebrowser/issues/728
user_data['zoom'] = data['zoom'] user_data['zoom'] = data['zoom']
elif 'zoom' in histentry:
user_data['zoom'] = histentry['zoom']
if 'scroll-pos' in data: if 'scroll-pos' in data:
# The scroll position was accidentally stored in 'data' instead
# of per-tab earlier.
# See https://github.com/The-Compiler/qutebrowser/issues/728
pos = data['scroll-pos'] pos = data['scroll-pos']
user_data['scroll-pos'] = QPoint(pos['x'], pos['y']) user_data['scroll-pos'] = QPoint(pos['x'], pos['y'])
elif 'scroll-pos' in histentry:
pos = histentry['scroll-pos']
user_data['scroll-pos'] = QPoint(pos['x'], pos['y'])
active = histentry.get('active', False) active = histentry.get('active', False)
url = QUrl.fromEncoded(histentry['url'].encode('ascii')) url = QUrl.fromEncoded(histentry['url'].encode('ascii'))
if 'original-url' in histentry: if 'original-url' in histentry:

View File

@ -230,10 +230,12 @@ def log_time(logger, action='operation'):
action: A description of what's being done. action: A description of what's being done.
""" """
started = datetime.datetime.now() started = datetime.datetime.now()
yield try:
finished = datetime.datetime.now() yield
delta = (finished - started).total_seconds() finally:
logger.debug("{} took {} seconds.".format(action.capitalize(), delta)) finished = datetime.datetime.now()
delta = (finished - started).total_seconds()
logger.debug("{} took {} seconds.".format(action.capitalize(), delta))
def _get_widgets(): def _get_widgets():

View File

@ -159,8 +159,10 @@ def init_log(args):
def disable_qt_msghandler(): def disable_qt_msghandler():
"""Contextmanager which temporarily disables the Qt message handler.""" """Contextmanager which temporarily disables the Qt message handler."""
old_handler = QtCore.qInstallMessageHandler(None) old_handler = QtCore.qInstallMessageHandler(None)
yield try:
QtCore.qInstallMessageHandler(old_handler) yield
finally:
QtCore.qInstallMessageHandler(old_handler)
def _init_handlers(level, color, ram_capacity): def _init_handlers(level, color, ram_capacity):
@ -259,23 +261,23 @@ def qt_message_handler(msg_type, context, msg):
# suppressed_msgs is a list of regexes matching the message texts to hide. # suppressed_msgs is a list of regexes matching the message texts to hide.
suppressed_msgs = ( suppressed_msgs = (
# PNGs in Qt with broken color profile # PNGs in Qt with broken color profile
# https://bugreports.qt-project.org/browse/QTBUG-39788 # https://bugreports.qt.io/browse/QTBUG-39788
"libpng warning: iCCP: Not recognizing known sRGB profile that has " "libpng warning: iCCP: Not recognizing known sRGB profile that has "
"been edited", "been edited",
# Hopefully harmless warning # Hopefully harmless warning
"OpenType support missing for script ", "OpenType support missing for script ",
# Error if a QNetworkReply gets two different errors set. Harmless Qt # Error if a QNetworkReply gets two different errors set. Harmless Qt
# bug on some pages. # bug on some pages.
# https://bugreports.qt-project.org/browse/QTBUG-30298 # https://bugreports.qt.io/browse/QTBUG-30298
"QNetworkReplyImplPrivate::error: Internal problem, this method must " "QNetworkReplyImplPrivate::error: Internal problem, this method must "
"only be called once.", "only be called once.",
# Sometimes indicates missing text, but most of the time harmless # Sometimes indicates missing text, but most of the time harmless
"load glyph failed ", "load glyph failed ",
# Harmless, see https://bugreports.qt-project.org/browse/QTBUG-42479 # Harmless, see https://bugreports.qt.io/browse/QTBUG-42479
"content-type missing in HTTP POST, defaulting to " "content-type missing in HTTP POST, defaulting to "
"application/x-www-form-urlencoded. Use QNetworkRequest::setHeader() " "application/x-www-form-urlencoded. Use QNetworkRequest::setHeader() "
"to fix this problem.", "to fix this problem.",
# https://bugreports.qt-project.org/browse/QTBUG-43118 # https://bugreports.qt.io/browse/QTBUG-43118
"Using blocking call!", "Using blocking call!",
# Hopefully harmless # Hopefully harmless
'"Method "GetAll" with signature "s" on interface ' '"Method "GetAll" with signature "s" on interface '
@ -285,6 +287,8 @@ def qt_message_handler(msg_type, context, msg):
'QXcbWindow: Unhandled client message: "_E_', 'QXcbWindow: Unhandled client message: "_E_',
'QXcbWindow: Unhandled client message: "_ECORE_', 'QXcbWindow: Unhandled client message: "_ECORE_',
'QXcbWindow: Unhandled client message: "_GTK_', 'QXcbWindow: Unhandled client message: "_GTK_',
# Happens on AppVeyor CI
'SetProcessDpiAwareness failed:',
) )
if any(msg.strip().startswith(pattern) for pattern in suppressed_msgs): if any(msg.strip().startswith(pattern) for pattern in suppressed_msgs):
level = logging.DEBUG level = logging.DEBUG
@ -317,8 +321,10 @@ def hide_qt_warning(pattern, logger='qt'):
log_filter = QtWarningFilter(pattern) log_filter = QtWarningFilter(pattern)
logger_obj = logging.getLogger(logger) logger_obj = logging.getLogger(logger)
logger_obj.addFilter(log_filter) logger_obj.addFilter(log_filter)
yield try:
logger_obj.removeFilter(log_filter) yield
finally:
logger_obj.removeFilter(log_filter)
class QtWarningFilter(logging.Filter): class QtWarningFilter(logging.Filter):
@ -377,7 +383,7 @@ class RAMHandler(logging.Handler):
"""Logging handler which keeps the messages in a deque in RAM. """Logging handler which keeps the messages in a deque in RAM.
Loosly based on logging.BufferingHandler which is unsuitable because it Loosely based on logging.BufferingHandler which is unsuitable because it
uses a simple list rather than a deque. uses a simple list rather than a deque.
Attributes: Attributes:

View File

@ -31,10 +31,9 @@ import io
import os import os
import sys import sys
import operator import operator
import distutils.version # pylint: disable=no-name-in-module,import-error
# https://bitbucket.org/logilab/pylint/issue/73/
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) QIODevice, QSaveFile)
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
@ -60,8 +59,8 @@ def version_check(version, op=operator.ge):
""" """
# pylint: disable=no-member # pylint: disable=no-member
# https://bitbucket.org/logilab/pylint/issue/73/ # https://bitbucket.org/logilab/pylint/issue/73/
return op(distutils.version.StrictVersion(qVersion()), return op(pkg_resources.parse_version(qVersion()),
distutils.version.StrictVersion(version)) pkg_resources.parse_version(version))
def check_overflow(arg, ctype, fatal=True): def check_overflow(arg, ctype, fatal=True):

View File

@ -118,7 +118,7 @@ def _get(typ):
Args: Args:
typ: A member of the QStandardPaths::StandardLocation enum, typ: A member of the QStandardPaths::StandardLocation enum,
see http://qt-project.org/doc/qt-5/qstandardpaths.html#StandardLocation-enum see http://doc.qt.io/qt-5/qstandardpaths.html#StandardLocation-enum
""" """
overridden, path = _from_args(typ, _args) overridden, path = _from_args(typ, _args)
if not overridden: if not overridden:
@ -127,7 +127,7 @@ def _get(typ):
if (typ == QStandardPaths.ConfigLocation and if (typ == QStandardPaths.ConfigLocation and
path.split(os.sep)[-1] != appname): path.split(os.sep)[-1] != appname):
# WORKAROUND - see # WORKAROUND - see
# https://bugreports.qt-project.org/browse/QTBUG-38872 # https://bugreports.qt.io/browse/QTBUG-38872
path = os.path.join(path, appname) path = os.path.join(path, appname)
if typ == QStandardPaths.DataLocation and os.name == 'nt': if typ == QStandardPaths.DataLocation and os.name == 'nt':
# Under windows, config/data might end up in the same directory. # Under windows, config/data might end up in the same directory.
@ -170,6 +170,7 @@ def _init_cachedir_tag():
f.write("# This file is a cache directory tag created by " f.write("# This file is a cache directory tag created by "
"qutebrowser.\n") "qutebrowser.\n")
f.write("# For information about cache directory tags, see:\n") f.write("# For information about cache directory tags, see:\n")
f.write("# http://www.brynosaurus.com/cachedir/\n") f.write("# http://www.brynosaurus.com/" # pragma: no branch
"cachedir/\n")
except OSError: except OSError:
log.init.exception("Failed to create CACHEDIR.TAG") log.init.exception("Failed to create CACHEDIR.TAG")

View File

@ -279,7 +279,7 @@ def qurl_from_user_input(urlstr):
IPv6, so we first try to handle it as a valid IPv6, and if that fails we IPv6, so we first try to handle it as a valid IPv6, and if that fails we
use QUrl.fromUserInput. use QUrl.fromUserInput.
WORKAROUND - https://bugreports.qt-project.org/browse/QTBUG-41089 WORKAROUND - https://bugreports.qt.io/browse/QTBUG-41089
FIXME - Maybe https://codereview.qt-project.org/#/c/93851/ has a better way FIXME - Maybe https://codereview.qt-project.org/#/c/93851/ has a better way
to solve this? to solve this?
https://github.com/The-Compiler/qutebrowser/issues/109 https://github.com/The-Compiler/qutebrowser/issues/109

View File

@ -73,7 +73,7 @@ class NeighborList(collections.abc.Sequence):
Args: Args:
items: The list of items to iterate in. items: The list of items to iterate in.
_default: The initially selected value. _default: The initially selected value.
_mode: Behaviour when the first/last item is reached. _mode: Behavior when the first/last item is reached.
Modes.block: Stay on the selected item Modes.block: Stay on the selected item
Modes.wrap: Wrap around to the other end Modes.wrap: Wrap around to the other end
Modes.exception: Raise an IndexError. Modes.exception: Raise an IndexError.
@ -242,7 +242,7 @@ Completion = enum('Completion', ['command', 'section', 'option', 'value',
# Exit statuses for errors. Needs to be an int for sys.exit. # Exit statuses for errors. Needs to be an int for sys.exit.
Exit = enum('Exit', ['ok', 'reserved', 'exception', 'err_ipc', 'err_init', Exit = enum('Exit', ['ok', 'reserved', 'exception', 'err_ipc', 'err_init',
'err_config', 'err_key_config'], is_int=True) 'err_config', 'err_key_config'], is_int=True, start=0)
class Question(QObject): class Question(QObject):

View File

@ -50,8 +50,6 @@ def elide(text, length):
def compact_text(text, elidelength=None): def compact_text(text, elidelength=None):
"""Remove leading whitespace and newlines from a text and maybe elide it. """Remove leading whitespace and newlines from a text and maybe elide it.
FIXME: Add tests.
Args: Args:
text: The text to compact. text: The text to compact.
elidelength: To how many chars to elide. elidelength: To how many chars to elide.
@ -105,12 +103,12 @@ def actute_warning():
try: try:
if qtutils.version_check('5.3.0'): if qtutils.version_check('5.3.0'):
return return
except ValueError: except ValueError: # pragma: no cover
pass pass
try: try:
with open('/usr/share/X11/locale/en_US.UTF-8/Compose', 'r', with open('/usr/share/X11/locale/en_US.UTF-8/Compose', 'r',
encoding='utf-8') as f: encoding='utf-8') as f:
for line in f: for line in f: # pragma: no branch
if '<dead_actute>' in line: if '<dead_actute>' in line:
if sys.stdout is not None: if sys.stdout is not None:
sys.stdout.flush() sys.stdout.flush()
@ -118,7 +116,7 @@ def actute_warning():
"that is not a bug in qutebrowser! See " "that is not a bug in qutebrowser! See "
"https://bugs.freedesktop.org/show_bug.cgi?id=69476 " "https://bugs.freedesktop.org/show_bug.cgi?id=69476 "
"for details.") "for details.")
break break # pragma: no branch
except OSError: except OSError:
log.init.exception("Failed to read Compose file") log.init.exception("Failed to read Compose file")
@ -242,7 +240,7 @@ def key_to_string(key):
""" """
special_names_str = { special_names_str = {
# Some keys handled in a weird way by QKeySequence::toString. # Some keys handled in a weird way by QKeySequence::toString.
# See https://bugreports.qt-project.org/browse/QTBUG-40030 # See https://bugreports.qt.io/browse/QTBUG-40030
# Most are unlikely to be ever needed, but you never know ;) # Most are unlikely to be ever needed, but you never know ;)
# For dead/combining keys, we return the corresponding non-combining # For dead/combining keys, we return the corresponding non-combining
# key, as that's easier to add to the config. # key, as that's easier to add to the config.
@ -290,6 +288,18 @@ def key_to_string(key):
'Key_TouchpadOn': 'Touchpad On', 'Key_TouchpadOn': 'Touchpad On',
'Key_TouchpadToggle': 'Touchpad toggle', 'Key_TouchpadToggle': 'Touchpad toggle',
'Key_Yellow': 'Yellow', 'Key_Yellow': 'Yellow',
'Key_Alt': 'Alt',
'Key_AltGr': 'AltGr',
'Key_Control': 'Control',
'Key_Direction_L': 'Direction L',
'Key_Direction_R': 'Direction R',
'Key_Hyper_L': 'Hyper L',
'Key_Hyper_R': 'Hyper R',
'Key_Meta': 'Meta',
'Key_Shift': 'Shift',
'Key_Super_L': 'Super L',
'Key_Super_R': 'Super R',
'Key_unknown': 'Unknown',
} }
# We now build our real special_names dict from the string mapping above. # We now build our real special_names dict from the string mapping above.
# The reason we don't do this directly is that certain Qt versions don't # The reason we don't do this directly is that certain Qt versions don't
@ -428,11 +438,13 @@ def disabled_excepthook():
"""Run code with the exception hook temporarily disabled.""" """Run code with the exception hook temporarily disabled."""
old_excepthook = sys.excepthook old_excepthook = sys.excepthook
sys.excepthook = sys.__excepthook__ sys.excepthook = sys.__excepthook__
yield try:
# If the code we did run did change sys.excepthook, we leave it yield
# unchanged. Otherwise, we reset it. finally:
if sys.excepthook is sys.__excepthook__: # If the code we did run did change sys.excepthook, we leave it
sys.excepthook = old_excepthook # unchanged. Otherwise, we reset it.
if sys.excepthook is sys.__excepthook__:
sys.excepthook = old_excepthook
class prevent_exceptions: # pylint: disable=invalid-name class prevent_exceptions: # pylint: disable=invalid-name
@ -541,7 +553,7 @@ def qualname(obj):
elif hasattr(obj, '__name__'): elif hasattr(obj, '__name__'):
name = obj.__name__ name = obj.__name__
else: else:
name = '<unknown>' name = repr(obj)
if inspect.isclass(obj) or inspect.isfunction(obj): if inspect.isclass(obj) or inspect.isfunction(obj):
module = obj.__module__ module = obj.__module__

View File

@ -29,10 +29,8 @@ import collections
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, qVersion from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, qVersion
from PyQt5.QtWebKit import qWebKitVersion from PyQt5.QtWebKit import qWebKitVersion
try: from PyQt5.QtNetwork import QSslSocket
from PyQt5.QtNetwork import QSslSocket from PyQt5.QtWidgets import QApplication
except ImportError:
QSslSocket = None
import qutebrowser import qutebrowser
from qutebrowser.utils import log, utils from qutebrowser.utils import log, utils
@ -114,7 +112,7 @@ def _release_info():
for fn in glob.glob("/etc/*-release"): for fn in glob.glob("/etc/*-release"):
try: try:
with open(fn, 'r', encoding='utf-8') as f: with open(fn, 'r', encoding='utf-8') as f:
data.append((fn, ''.join(f.readlines()))) data.append((fn, ''.join(f.readlines()))) # pragma: no branch
except OSError: except OSError:
log.misc.exception("Error while reading {}.".format(fn)) log.misc.exception("Error while reading {}.".format(fn))
return data return data
@ -186,8 +184,12 @@ def _os_info():
return lines return lines
def version(): def version(short=False):
"""Return a string with various version informations.""" """Return a string with various version informations.
Args:
short: Return a shortened output.
"""
lines = ["qutebrowser v{}".format(qutebrowser.__version__)] lines = ["qutebrowser v{}".format(qutebrowser.__version__)]
gitver = _git_str() gitver = _git_str()
if gitver is not None: if gitver is not None:
@ -199,20 +201,24 @@ def version():
'Qt: {}, runtime: {}'.format(QT_VERSION_STR, qVersion()), 'Qt: {}, runtime: {}'.format(QT_VERSION_STR, qVersion()),
'PyQt: {}'.format(PYQT_VERSION_STR), 'PyQt: {}'.format(PYQT_VERSION_STR),
] ]
lines += _module_versions()
if QSslSocket is not None and QSslSocket.supportsSsl(): if not short:
ssl_version = QSslSocket.sslLibraryVersionString() style = QApplication.instance().style()
else: lines += [
ssl_version = 'unavailable' 'Style: {}'.format(style.metaObject().className()),
lines += [ 'Desktop: {}'.format(os.environ.get('DESKTOP_SESSION')),
'Webkit: {}'.format(qWebKitVersion()), ]
'Harfbuzz: {}'.format(os.environ.get('QT_HARFBUZZ', 'system')),
'SSL: {}'.format(ssl_version), lines += _module_versions()
'',
'Frozen: {}'.format(hasattr(sys, 'frozen')), lines += [
'Platform: {}, {}'.format(platform.platform(), 'Webkit: {}'.format(qWebKitVersion()),
platform.architecture()[0]), 'Harfbuzz: {}'.format(os.environ.get('QT_HARFBUZZ', 'system')),
] 'SSL: {}'.format(QSslSocket.sslLibraryVersionString()),
lines += _os_info() '',
'Frozen: {}'.format(hasattr(sys, 'frozen')),
'Platform: {}, {}'.format(platform.platform(),
platform.architecture()[0]),
]
lines += _os_info()
return '\n'.join(lines) return '\n'.join(lines)

View File

@ -46,6 +46,20 @@ def call_script(name, *args, python=sys.executable):
subprocess.check_call([python, path] + list(args)) subprocess.check_call([python, path] + list(args))
def call_freeze(*args, python=sys.executable):
"""Call freeze.py via tox.
Args:
*args: The arguments to pass.
python: The python interpreter to use.
"""
env = os.environ.copy()
env['PYTHON'] = python
subprocess.check_call(
[sys.executable, '-m', 'tox', '-e', 'cxfreeze-windows'] + list(args),
env=env)
def build_common(args): def build_common(args):
"""Common buildsteps used for all OS'.""" """Common buildsteps used for all OS'."""
utils.print_title("Running asciidoc2html.py") utils.print_title("Running asciidoc2html.py")
@ -64,22 +78,33 @@ def _maybe_remove(path):
pass pass
def smoke_test(executable):
"""Try starting the given qutebrowser executable."""
subprocess.check_call([executable, '--no-err-windows', '--nowindow',
'--temp-basedir', 'about:blank', ':later 500 quit'])
def build_windows(): def build_windows():
"""Build windows executables/setups.""" """Build windows executables/setups."""
parts = str(sys.version_info.major), str(sys.version_info.minor) parts = str(sys.version_info.major), str(sys.version_info.minor)
ver = ''.join(parts) ver = ''.join(parts)
dotver = '.'.join(parts) dotver = '.'.join(parts)
python_x86 = r'C:\Python{}_x32\python.exe'.format(ver) python_x86 = r'C:\Python{}_x32'.format(ver)
python_x64 = r'C:\Python{}\python.exe'.format(ver) python_x64 = r'C:\Python{}'.format(ver)
utils.print_title("Running 32bit freeze.py build_exe") utils.print_title("Running 32bit freeze.py build_exe")
call_script('freeze.py', 'build_exe', python=python_x86) call_freeze('build_exe', python=python_x86)
utils.print_title("Running 64bit freeze.py build_exe")
call_script('freeze.py', 'build_exe', python=python_x64)
utils.print_title("Running 32bit freeze.py bdist_msi") utils.print_title("Running 32bit freeze.py bdist_msi")
call_script('freeze.py', 'bdist_msi', python=python_x86) call_freeze('bdist_msi', python=python_x86)
utils.print_title("Running 64bit freeze.py build_exe")
call_freeze('build_exe', python=python_x64)
utils.print_title("Running 64bit freeze.py bdist_msi") utils.print_title("Running 64bit freeze.py bdist_msi")
call_script('freeze.py', 'bdist_msi', python=python_x64) call_freeze('bdist_msi', python=python_x64)
utils.print_title("Running 32bit smoke test")
smoke_test('build/exe.win32-{}/qutebrowser.exe'.format(dotver))
utils.print_title("Running 64bit smoke test")
smoke_test('build/exe.win-amd64-{}/qutebrowser.exe'.format(dotver))
destdir = os.path.join('dist', 'zip') destdir = os.path.join('dist', 'zip')
_maybe_remove(destdir) _maybe_remove(destdir)
@ -126,6 +151,14 @@ def main():
args = parser.parse_args() args = parser.parse_args()
utils.change_cwd() utils.change_cwd()
if os.name == 'nt': if os.name == 'nt':
if sys.maxsize > 2**32:
# WORKAROUND
print("Due to a python/Windows bug, this script needs to be run ")
print("with a 32bit Python.")
print()
print("See http://bugs.python.org/issue24493 and ")
print("https://github.com/pypa/virtualenv/issues/774")
sys.exit(1)
build_common(args) build_common(args)
build_windows() build_windows()
else: else:

101
scripts/ci_install.py Normal file
View File

@ -0,0 +1,101 @@
#!/usr/bin/env python2
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015 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: skip-file
"""Install needed prerequisites on the AppVeyor/Travis CI.
Note this file is written in python2 as this is more readily available on the
CI machines.
"""
from __future__ import print_function
import os
import sys
import subprocess
import urllib
PYQT_VERSION = '5.4.2'
def apt_get(args):
subprocess.check_call(['sudo', 'apt-get', '-y', '-q'] + args)
def brew(args, silent=False):
if silent:
with open(os.devnull, 'w') as f:
subprocess.check_call(['brew'] + args, stdout=f)
else:
subprocess.check_call(['brew'] + args)
if 'APPVEYOR' in os.environ:
print("Getting PyQt5...")
urllib.urlretrieve(
('http://sourceforge.net/projects/pyqt/files/PyQt5/PyQt-{v}/'
'PyQt5-{v}-gpl-Py3.4-Qt{v}-x32.exe'.format(v=PYQT_VERSION)),
r'C:\install-PyQt5.exe')
print("Installing PyQt5...")
subprocess.check_call([r'C:\install-PyQt5.exe', '/S'])
print("Installing tox...")
subprocess.check_call([r'C:\Python34\Scripts\pip', 'install', 'tox'])
print("Linking Python...")
with open(r'C:\Windows\system32\python3.bat', 'w') as f:
f.write(r'@C:\Python34\python %*')
elif os.environ.get('TRAVIS_OS_NAME', None) == 'linux':
print("apt-get update...")
apt_get(['update'])
print("Installing packages...")
pkgs = 'python3-pyqt5 python3-pyqt5.qtwebkit python-tox python3-dev xvfb'
apt_get(['install'] + pkgs.split())
elif os.environ.get('TRAVIS_OS_NAME', None) == 'osx':
print("brew update...")
brew(['update'], silent=True)
print("Installing packages...")
brew(['install', 'python3', 'pyqt5'])
print("Installing tox...")
subprocess.check_call(['sudo', 'pip3.4', 'install', 'tox'])
os.system('ls -l /usr/local/bin/xvfb-run')
print("Creating xvfb-run stub...")
with open('/usr/local/bin/xvfb-run', 'w') as f:
# This will break when xvfb-run is called differently in .travis.yml,
# but I can't be bothered to do it in a nicer way.
f.write('#!/bin/bash\n')
f.write('shift 2\n')
f.write('exec "$@"\n')
os.system('sudo chmod 755 /usr/local/bin/xvfb-run')
os.system('ls -l /usr/local/bin/xvfb-run')
else:
def env(key):
return os.environ.get(key, None)
print("Unknown environment! (CI {}, APPVEYOR {}, TRAVIS {}, "
"TRAVIS_OS_NAME {})".format(env('CI'), env('APPVEYOR'),
env('TRAVIS'), env('TRAVIS_OS_NAME')),
file=sys.stderr)
sys.exit(1)

View File

@ -47,20 +47,41 @@ def get_egl_path():
return os.path.join(distutils.sysconfig.get_python_lib(), return os.path.join(distutils.sysconfig.get_python_lib(),
r'PyQt5\libEGL.dll') r'PyQt5\libEGL.dll')
build_exe_options = {
'include_files': [
('qutebrowser/html', 'html'),
('qutebrowser/html/doc', 'html/doc'),
('qutebrowser/git-commit-id', 'git-commit-id'),
],
'include_msvcr': True,
'excludes': ['tkinter'],
'packages': ['pygments'],
}
egl_path = get_egl_path() def get_build_exe_options(skip_html=False):
if egl_path is not None: """Get the options passed as build_exe_options to cx_Freeze.
build_exe_options['include_files'].append((egl_path, 'libEGL.dll'))
If either skip_html or --qute-skip-html as argument is given, doesn't
freeze the documentation.
"""
if '--qute-skip-html' in sys.argv:
skip_html = True
sys.argv.remove('--qute-skip-html')
include_files = [
('qutebrowser/javascript', 'javascript'),
('qutebrowser/git-commit-id', 'git-commit-id'),
('qutebrowser/utils/testfile', 'utils/testfile'),
]
if not skip_html:
include_files += [
('qutebrowser/html', 'html'),
('qutebrowser/html/doc', 'html/doc'),
]
egl_path = get_egl_path()
if egl_path is not None:
include_files.append((egl_path, 'libEGL.dll'))
return {
'include_files': include_files,
'include_msvcr': True,
'includes': [],
'excludes': ['tkinter'],
'packages': ['pygments'],
}
bdist_msi_options = { bdist_msi_options = {
# random GUID generated by uuid.uuid4() # random GUID generated by uuid.uuid4()
@ -92,19 +113,21 @@ executable = cx.Executable('qutebrowser/__main__.py', base=base,
icon=os.path.join(BASEDIR, 'icons', icon=os.path.join(BASEDIR, 'icons',
'qutebrowser.ico')) 'qutebrowser.ico'))
try:
setupcommon.write_git_file() if __name__ == '__main__':
cx.setup( try:
executables=[executable], setupcommon.write_git_file()
options={ cx.setup(
'build_exe': build_exe_options, executables=[executable],
'bdist_msi': bdist_msi_options, options={
'bdist_mac': bdist_mac_options, 'build_exe': get_build_exe_options(),
'bdist_dmg': bdist_dmg_options, 'bdist_msi': bdist_msi_options,
}, 'bdist_mac': bdist_mac_options,
**setupcommon.setupdata 'bdist_dmg': bdist_dmg_options,
) },
finally: **setupcommon.setupdata
path = os.path.join(BASEDIR, 'qutebrowser', 'git-commit-id') )
if os.path.exists(path): finally:
os.remove(path) path = os.path.join(BASEDIR, 'qutebrowser', 'git-commit-id')
if os.path.exists(path):
os.remove(path)

70
scripts/freeze_tests.py Executable file
View File

@ -0,0 +1,70 @@
#!/usr/bin/env python3
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015 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/>.
"""cx_Freeze script to freeze qutebrowser and its tests."""
import os
import os.path
import sys
import contextlib
import cx_Freeze as cx # pylint: disable=import-error
# cx_Freeze is hard to install (needs C extensions) so we don't check for it.
import pytest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from scripts import setupcommon, freeze
@contextlib.contextmanager
def temp_git_commit_file():
"""Context manager to temporarily create a fake git-commit-id file."""
basedir = os.path.join(os.path.dirname(os.path.realpath(__file__)),
os.path.pardir)
path = os.path.join(basedir, 'qutebrowser', 'git-commit-id')
with open(path, 'wb') as f:
f.write(b'fake-frozen-git-commit')
yield
os.remove(path)
def get_build_exe_options():
"""Get build_exe options with additional includes."""
opts = freeze.get_build_exe_options(skip_html=True)
opts['includes'] += pytest.freeze_includes() # pylint: disable=no-member
opts['includes'] += ['unittest.mock', 'PyQt5.QtTest']
opts['packages'].append('qutebrowser')
return opts
def main():
"""Main entry point."""
with temp_git_commit_file():
cx.setup(
executables=[cx.Executable('scripts/run_frozen_tests.py',
targetName='run-frozen-tests')],
options={'build_exe': get_build_exe_options()},
**setupcommon.setupdata
)
if __name__ == '__main__':
main()

View File

@ -40,7 +40,8 @@ class Error(Exception):
def verbose_copy(src, dst, *, follow_symlinks=True): def verbose_copy(src, dst, *, follow_symlinks=True):
"""Copy function for shutil.copytree which prints copied files.""" """Copy function for shutil.copytree which prints copied files."""
print('{} -> {}'.format(src, dst)) if '-v' in sys.argv:
print('{} -> {}'.format(src, dst))
shutil.copy(src, dst, follow_symlinks=follow_symlinks) shutil.copy(src, dst, follow_symlinks=follow_symlinks)
@ -112,6 +113,7 @@ def copy_or_link(source, dest):
"""Copy or symlink source to dest.""" """Copy or symlink source to dest."""
if os.name == 'nt': if os.name == 'nt':
if os.path.isdir(source): if os.path.isdir(source):
print('{} -> {}'.format(source, dest))
shutil.copytree(source, dest, ignore=get_ignored_files, shutil.copytree(source, dest, ignore=get_ignored_files,
copy_function=verbose_copy) copy_function=verbose_copy)
else: else:
@ -138,7 +140,10 @@ def get_python_lib(executable, venv=False):
treatments for Windows/Ubuntu shouldn't take place. treatments for Windows/Ubuntu shouldn't take place.
""" """
distribution = platform.linux_distribution(full_distribution_name=False) distribution = platform.linux_distribution(full_distribution_name=False)
if os.name == 'nt' and not venv: if 'PYTHON' in os.environ and not venv:
# e.g. on AppVeyor
return os.path.join(os.environ['PYTHON'], 'Lib', 'site-packages')
elif os.name == 'nt' and not venv:
# For some reason, we get an empty string from get_python_lib() on # For some reason, we get an empty string from get_python_lib() on
# Windows when running via tox, and sys.prefix is empty too... # Windows when running via tox, and sys.prefix is empty too...
return os.path.join(os.path.dirname(executable), '..', 'Lib', return os.path.join(os.path.dirname(executable), '..', 'Lib',

View File

@ -35,9 +35,13 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from scripts import utils from scripts import utils
def _py_files(target): def _py_files():
"""Iterate over all python files and yield filenames.""" """Iterate over all python files and yield filenames."""
for (dirpath, _dirnames, filenames) in os.walk(target): for (dirpath, _dirnames, filenames) in os.walk('.'):
parts = dirpath.split(os.sep)
if len(parts) >= 2 and parts[1].startswith('.'):
# ignore hidden dirs
continue
for name in (e for e in filenames if e.endswith('.py')): for name in (e for e in filenames if e.endswith('.py')):
yield os.path.join(dirpath, name) yield os.path.join(dirpath, name)
@ -64,31 +68,32 @@ def check_git():
return status return status
def check_spelling(target): def check_spelling():
"""Check commonly misspelled words.""" """Check commonly misspelled words."""
# Words which I often misspell # Words which I often misspell
words = {'behaviour', 'quitted', 'likelyhood', 'sucessfully', words = {'[Bb]ehaviour', '[Qq]uitted', 'Ll]ikelyhood', '[Ss]ucessfully',
'occur[^r .]', 'seperator', 'explicitely', 'resetted', '[Oo]ccur[^r .]', '[Ss]eperator', '[Ee]xplicitely', '[Rr]esetted',
'auxillary', 'accidentaly', 'ambigious', 'loosly', '[Aa]uxillary', '[Aa]ccidentaly', '[Aa]mbigious', '[Ll]oosly',
'initialis', 'convienence', 'similiar', 'uncommited', '[Ii]nitialis', '[Cc]onvienence', '[Ss]imiliar', '[Uu]ncommited',
'reproducable'} '[Rr]eproducable'}
# Words which look better when splitted, but might need some fine tuning. # Words which look better when splitted, but might need some fine tuning.
words |= {'keystrings', 'webelements', 'mouseevent', 'keysequence', words |= {'[Kk]eystrings', '[Ww]ebelements', '[Mm]ouseevent',
'normalmode', 'eventloops', 'sizehint', 'statemachine', '[Kk]eysequence', '[Nn]ormalmode', '[Ee]ventloops',
'metaobject', 'logrecord', 'filetype'} '[Ss]izehint', '[Ss]tatemachine', '[Mm]etaobject',
'[Ll]ogrecord', '[Ff]iletype'}
seen = collections.defaultdict(list) seen = collections.defaultdict(list)
try: try:
ok = True ok = True
for fn in _py_files(target): for fn in _py_files():
with tokenize.open(fn) as f: with tokenize.open(fn) as f:
if fn == os.path.join('scripts', 'misc_checks.py'): if fn == os.path.join('.', 'scripts', 'misc_checks.py'):
continue continue
for line in f: for line in f:
for w in words: for w in words:
if re.search(w, line) and fn not in seen[w]: if re.search(w, line) and fn not in seen[w]:
print("Found '{}' in {}!".format(w, fn)) print('Found "{}" in {}!'.format(w, fn))
seen[w].append(fn) seen[w].append(fn)
ok = False ok = False
print() print()
@ -98,11 +103,11 @@ def check_spelling(target):
return None return None
def check_vcs_conflict(target): def check_vcs_conflict():
"""Check VCS conflict markers.""" """Check VCS conflict markers."""
try: try:
ok = True ok = True
for fn in _py_files(target): for fn in _py_files():
with tokenize.open(fn) as f: with tokenize.open(fn) as f:
for line in f: for line in f:
if any(line.startswith(c * 7) for c in '<>=|'): if any(line.startswith(c * 7) for c in '<>=|'):
@ -120,25 +125,14 @@ def main():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('checker', choices=('git', 'vcs', 'spelling'), parser.add_argument('checker', choices=('git', 'vcs', 'spelling'),
help="Which checker to run.") help="Which checker to run.")
parser.add_argument('target', help="What to check", nargs='*')
args = parser.parse_args() args = parser.parse_args()
if args.checker == 'git': if args.checker == 'git':
ok = check_git() ok = check_git()
return 0 if ok else 1
elif args.checker == 'vcs': elif args.checker == 'vcs':
is_ok = True ok = check_vcs_conflict()
for target in args.target:
ok = check_vcs_conflict(target)
if not ok:
is_ok = False
return 0 if is_ok else 1
elif args.checker == 'spelling': elif args.checker == 'spelling':
is_ok = True ok = check_spelling()
for target in args.target: return 0 if ok else 1
ok = check_spelling(target)
if not ok:
is_ok = False
return 0 if is_ok else 1
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -1,6 +1,8 @@
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org> #!/usr/bin/env python3
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser. # This file is part of qutebrowser.
# #
# qutebrowser is free software: you can redistribute it and/or modify # qutebrowser is free software: you can redistribute it and/or modify
@ -16,30 +18,16 @@
# 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/>.
"""Checker for CRLF in files.""" # pylint: disable=import-error,no-member
from pylint import interfaces, checkers """cx_Freeze script to run qutebrowser tests on the frozen executable."""
import sys
class CrlfChecker(checkers.BaseChecker): import pytest
import pytestqt.plugin
import pytest_mock
import pytest_capturelog
"""Check for CRLF in files.""" sys.exit(pytest.main(plugins=[pytestqt.plugin, pytest_mock,
pytest_capturelog]))
__implements__ = interfaces.IRawChecker
name = 'crlf'
msgs = {'W9001': ('Uses CRLFs', 'crlf', None)}
options = ()
priority = -1
def process_module(self, node):
"""Process the module."""
for (lineno, line) in enumerate(node.file_stream):
if b'\r\n' in line:
self.add_message('crlf', line=lineno)
return
def register(linter):
"""Register the checker."""
linter.register_checker(CrlfChecker(linter))

View File

@ -70,20 +70,20 @@ def main():
if len(sys.argv) < 2: if len(sys.argv) < 2:
# pages which previously caused problems # pages which previously caused problems
pages = [ pages = [
# ANGLE, https://bugreports.qt-project.org/browse/QTBUG-39723 # ANGLE, https://bugreports.qt.io/browse/QTBUG-39723
('http://www.binpress.com/', False), ('http://www.binpress.com/', False),
('http://david.li/flow/', False), ('http://david.li/flow/', False),
('https://imzdl.com/', False), ('https://imzdl.com/', False),
# not reproducible # not reproducible
# https://bugreports.qt-project.org/browse/QTBUG-39847 # https://bugreports.qt.io/browse/QTBUG-39847
('http://www.20min.ch/', True), ('http://www.20min.ch/', True),
# HarfBuzz, https://bugreports.qt-project.org/browse/QTBUG-39278 # HarfBuzz, https://bugreports.qt.io/browse/QTBUG-39278
('http://www.the-compiler.org/', True), ('http://www.the-compiler.org/', True),
('http://phoronix.com', True), ('http://phoronix.com', True),
('http://twitter.com', True), ('http://twitter.com', True),
# HarfBuzz #2, https://bugreports.qt-project.org/browse/QTBUG-36099 # HarfBuzz #2, https://bugreports.qt.io/browse/QTBUG-36099
('http://lenta.ru/', True), ('http://lenta.ru/', True),
# Unknown, https://bugreports.qt-project.org/browse/QTBUG-41360 # Unknown, https://bugreports.qt.io/browse/QTBUG-41360
('http://salt.readthedocs.org/en/latest/topics/pillar/', True), ('http://salt.readthedocs.org/en/latest/topics/pillar/', True),
] ]
else: else:

View File

@ -37,7 +37,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
import qutebrowser.app import qutebrowser.app
from scripts import asciidoc2html, utils from scripts import asciidoc2html, utils
from qutebrowser import qutebrowser from qutebrowser import qutebrowser
from qutebrowser.commands import cmdutils from qutebrowser.commands import cmdutils, command
from qutebrowser.config import configdata from qutebrowser.config import configdata
from qutebrowser.utils import docutils from qutebrowser.utils import docutils
@ -54,6 +54,14 @@ class UsageFormatter(argparse.HelpFormatter):
"""Override _format_usage to not add the 'usage:' prefix.""" """Override _format_usage to not add the 'usage:' prefix."""
return super()._format_usage(usage, actions, groups, '') return super()._format_usage(usage, actions, groups, '')
def _get_default_metavar_for_optional(self, action):
"""Do name transforming when getting metavar."""
return command.arg_name(action.dest.upper())
def _get_default_metavar_for_positional(self, action):
"""Do name transforming when getting metavar."""
return command.arg_name(action.dest)
def _metavar_formatter(self, action, default_metavar): def _metavar_formatter(self, action, default_metavar):
"""Override _metavar_formatter to add asciidoc markup to metavars. """Override _metavar_formatter to add asciidoc markup to metavars.

View File

@ -38,7 +38,7 @@ except NameError:
try: try:
common.write_git_file() common.write_git_file()
setuptools.setup( setuptools.setup(
packages=setuptools.find_packages(exclude=['qutebrowser.test']), packages=setuptools.find_packages(exclude=['scripts', 'scripts.*']),
include_package_data=True, include_package_data=True,
entry_points={'gui_scripts': entry_points={'gui_scripts':
['qutebrowser = qutebrowser.qutebrowser:main']}, ['qutebrowser = qutebrowser.qutebrowser:main']},

View File

@ -197,8 +197,10 @@ class TestKeyConfigParser:
('download-page', 'download'), ('download-page', 'download'),
('cancel-download', 'download-cancel'), ('cancel-download', 'download-cancel'),
('search ""', 'search'), ('search ""', 'search ;; clear-keychain'),
("search ''", 'search'), ("search ''", 'search ;; clear-keychain'),
("search", 'search ;; clear-keychain'),
("search ;; foobar", None),
('search "foo"', None), ('search "foo"', None),
('set-cmd-text "foo bar"', 'set-cmd-text foo bar'), ('set-cmd-text "foo bar"', 'set-cmd-text foo bar'),

View File

@ -1362,6 +1362,7 @@ class TestFile:
def test_validate_does_not_exist(self, os_path): def test_validate_does_not_exist(self, os_path):
"""Test validate with a file which does not exist.""" """Test validate with a file which does not exist."""
os_path.expanduser.side_effect = lambda x: x os_path.expanduser.side_effect = lambda x: x
os_path.expandvars.side_effect = lambda x: x
os_path.isfile.return_value = False os_path.isfile.return_value = False
with pytest.raises(configexc.ValidationError): with pytest.raises(configexc.ValidationError):
self.t.validate('foobar') self.t.validate('foobar')
@ -1369,26 +1370,50 @@ class TestFile:
def test_validate_exists_abs(self, os_path): def test_validate_exists_abs(self, os_path):
"""Test validate with a file which does exist.""" """Test validate with a file which does exist."""
os_path.expanduser.side_effect = lambda x: x os_path.expanduser.side_effect = lambda x: x
os_path.expandvars.side_effect = lambda x: x
os_path.isfile.return_value = True os_path.isfile.return_value = True
os_path.isabs.return_value = True os_path.isabs.return_value = True
self.t.validate('foobar') self.t.validate('foobar')
def test_validate_exists_not_abs(self, os_path): def test_validate_exists_rel(self, os_path, monkeypatch):
"""Test validate with a file which does exist but is not absolute.""" """Test validate with a relative path to an existing file."""
monkeypatch.setattr(
'qutebrowser.config.configtypes.standarddir.config',
lambda: '/home/foo/.config/')
os_path.expanduser.side_effect = lambda x: x os_path.expanduser.side_effect = lambda x: x
os_path.expandvars.side_effect = lambda x: x
os_path.isfile.return_value = True os_path.isfile.return_value = True
os_path.isabs.return_value = False os_path.isabs.return_value = False
self.t.validate('foobar')
os_path.join.assert_called_once_with('/home/foo/.config/', 'foobar')
def test_validate_rel_config_none(self, os_path, monkeypatch):
"""Test with a relative path and standarddir.config returning None."""
monkeypatch.setattr(
'qutebrowser.config.configtypes.standarddir.config', lambda: None)
os_path.isabs.return_value = False
with pytest.raises(configexc.ValidationError): with pytest.raises(configexc.ValidationError):
self.t.validate('foobar') self.t.validate('foobar')
def test_validate_expanduser(self, os_path): def test_validate_expanduser(self, os_path):
"""Test if validate expands the user correctly.""" """Test if validate expands the user correctly."""
os_path.expanduser.side_effect = lambda x: x.replace('~', '/home/foo') os_path.expanduser.side_effect = lambda x: x.replace('~', '/home/foo')
os_path.expandvars.side_effect = lambda x: x
os_path.isfile.side_effect = lambda path: path == '/home/foo/foobar' os_path.isfile.side_effect = lambda path: path == '/home/foo/foobar'
os_path.isabs.return_value = True os_path.isabs.return_value = True
self.t.validate('~/foobar') self.t.validate('~/foobar')
os_path.expanduser.assert_called_once_with('~/foobar') os_path.expanduser.assert_called_once_with('~/foobar')
def test_validate_expandvars(self, os_path):
"""Test if validate expands the environment vars correctly."""
os_path.expanduser.side_effect = lambda x: x
os_path.expandvars.side_effect = lambda x: x.replace(
'$HOME', '/home/foo')
os_path.isfile.side_effect = lambda path: path == '/home/foo/foobar'
os_path.isabs.return_value = True
self.t.validate('$HOME/foobar')
os_path.expandvars.assert_called_once_with('$HOME/foobar')
def test_validate_invalid_encoding(self, os_path, unicode_encode_err): def test_validate_invalid_encoding(self, os_path, unicode_encode_err):
"""Test validate with an invalid encoding, e.g. LC_ALL=C.""" """Test validate with an invalid encoding, e.g. LC_ALL=C."""
os_path.isfile.side_effect = unicode_encode_err os_path.isfile.side_effect = unicode_encode_err
@ -1399,6 +1424,7 @@ class TestFile:
def test_transform(self, os_path): def test_transform(self, os_path):
"""Test transform.""" """Test transform."""
os_path.expanduser.side_effect = lambda x: x.replace('~', '/home/foo') os_path.expanduser.side_effect = lambda x: x.replace('~', '/home/foo')
os_path.expandvars.side_effect = lambda x: x
assert self.t.transform('~/foobar') == '/home/foo/foobar' assert self.t.transform('~/foobar') == '/home/foo/foobar'
os_path.expanduser.assert_called_once_with('~/foobar') os_path.expanduser.assert_called_once_with('~/foobar')
@ -1855,6 +1881,11 @@ class TestSearchEngineUrl:
with pytest.raises(configexc.ValidationError): with pytest.raises(configexc.ValidationError):
self.t.validate(':{}') self.t.validate(':{}')
def test_validate_format_string(self):
"""Test validate with a {foo} format string."""
with pytest.raises(configexc.ValidationError):
self.t.validate('foo{bar}baz{}')
def test_transform_empty(self): def test_transform_empty(self):
"""Test transform with an empty value.""" """Test transform with an empty value."""
assert self.t.transform('') is None assert self.t.transform('') is None
@ -1930,13 +1961,21 @@ class TestUserStyleSheet:
"""Test transform with an empty value.""" """Test transform with an empty value."""
assert self.t.transform('') is None assert self.t.transform('') is None
def test_transform_file(self): def test_transform_file(self, os_path, mocker):
"""Test transform with a filename.""" """Test transform with a filename."""
qurl = mocker.patch('qutebrowser.config.configtypes.QUrl',
autospec=True)
qurl.fromLocalFile.return_value = QUrl("file:///foo/bar")
os_path.exists.return_value = True
path = os.path.join(os.path.sep, 'foo', 'bar') path = os.path.join(os.path.sep, 'foo', 'bar')
assert self.t.transform(path) == QUrl("file:///foo/bar") assert self.t.transform(path) == QUrl("file:///foo/bar")
def test_transform_file_expandvars(self, monkeypatch): def test_transform_file_expandvars(self, os_path, monkeypatch, mocker):
"""Test transform with a filename (expandvars).""" """Test transform with a filename (expandvars)."""
qurl = mocker.patch('qutebrowser.config.configtypes.QUrl',
autospec=True)
qurl.fromLocalFile.return_value = QUrl("file:///foo/bar")
os_path.exists.return_value = True
monkeypatch.setenv('FOO', 'foo') monkeypatch.setenv('FOO', 'foo')
path = os.path.join(os.path.sep, '$FOO', 'bar') path = os.path.join(os.path.sep, '$FOO', 'bar')
assert self.t.transform(path) == QUrl("file:///foo/bar") assert self.t.transform(path) == QUrl("file:///foo/bar")

View File

@ -28,7 +28,7 @@ import jinja2
from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtWebKitWidgets import QWebView, QWebPage from PyQt5.QtWebKitWidgets import QWebView, QWebPage
import qutebrowser from qutebrowser.utils import utils
class TestWebPage(QWebPage): class TestWebPage(QWebPage):
@ -80,8 +80,10 @@ class JSTester:
def scroll_anchor(self, name): def scroll_anchor(self, name):
"""Scroll the main frame to the given anchor.""" """Scroll the main frame to the given anchor."""
page = self.webview.page() page = self.webview.page()
with self._qtbot.waitSignal(page.scrollRequested): old_pos = page.mainFrame().scrollPosition()
page.mainFrame().scrollToAnchor(name) page.mainFrame().scrollToAnchor(name)
new_pos = page.mainFrame().scrollPosition()
assert old_pos != new_pos
def load(self, path, **kwargs): def load(self, path, **kwargs):
"""Load and display the given test data. """Load and display the given test data.
@ -92,7 +94,7 @@ class JSTester:
**kwargs: Passed to jinja's template.render(). **kwargs: Passed to jinja's template.render().
""" """
template = self._jinja_env.get_template(path) template = self._jinja_env.get_template(path)
with self._qtbot.waitSignal(self.webview.loadFinished): with self._qtbot.waitSignal(self.webview.loadFinished, raising=True):
self.webview.setHtml(template.render(**kwargs)) self.webview.setHtml(template.render(**kwargs))
def run_file(self, filename): def run_file(self, filename):
@ -105,11 +107,7 @@ class JSTester:
Return: Return:
The javascript return value. The javascript return value.
""" """
base_path = os.path.join(os.path.dirname(qutebrowser.__file__), source = utils.read_file(os.path.join('javascript', filename))
'javascript')
full_path = os.path.join(base_path, filename)
with open(full_path, 'r', encoding='utf-8') as f:
source = f.read()
return self.run(source) return self.run(source)
def run(self, source): def run(self, source):

View File

@ -44,9 +44,6 @@ def progress_widget(qtbot, monkeypatch, config_stub):
return widget return widget
@pytest.mark.xfail(
reason='Blacklisted because it could cause random segfaults - see '
'https://github.com/hackebrot/qutebrowser/issues/22', run=False)
def test_load_started(progress_widget): def test_load_started(progress_widget):
"""Ensure the Progress widget reacts properly when the page starts loading. """Ensure the Progress widget reacts properly when the page starts loading.

View File

@ -42,8 +42,8 @@ class TestArg:
@pytest.yield_fixture(autouse=True) @pytest.yield_fixture(autouse=True)
def setup(self, monkeypatch, stubs): def setup(self, monkeypatch, stubs):
monkeypatch.setattr('qutebrowser.misc.editor.QProcess', monkeypatch.setattr('qutebrowser.misc.editor.guiprocess.QProcess',
stubs.FakeQProcess()) stubs.fake_qprocess())
self.editor = editor.ExternalEditor(0) self.editor = editor.ExternalEditor(0)
yield yield
self.editor._cleanup() # pylint: disable=protected-access self.editor._cleanup() # pylint: disable=protected-access
@ -60,7 +60,7 @@ class TestArg:
stubbed_config.data = { stubbed_config.data = {
'general': {'editor': ['bin'], 'editor-encoding': 'utf-8'}} 'general': {'editor': ['bin'], 'editor-encoding': 'utf-8'}}
self.editor.edit("") self.editor.edit("")
self.editor._proc.start.assert_called_with("bin", []) self.editor._proc._proc.start.assert_called_with("bin", [])
def test_start_args(self, stubbed_config): def test_start_args(self, stubbed_config):
"""Test starting editor with static arguments.""" """Test starting editor with static arguments."""
@ -68,7 +68,7 @@ class TestArg:
'general': {'editor': ['bin', 'foo', 'bar'], 'general': {'editor': ['bin', 'foo', 'bar'],
'editor-encoding': 'utf-8'}} 'editor-encoding': 'utf-8'}}
self.editor.edit("") self.editor.edit("")
self.editor._proc.start.assert_called_with("bin", ["foo", "bar"]) self.editor._proc._proc.start.assert_called_with("bin", ["foo", "bar"])
def test_placeholder(self, stubbed_config): def test_placeholder(self, stubbed_config):
"""Test starting editor with placeholder argument.""" """Test starting editor with placeholder argument."""
@ -77,7 +77,7 @@ class TestArg:
'editor-encoding': 'utf-8'}} 'editor-encoding': 'utf-8'}}
self.editor.edit("") self.editor.edit("")
filename = self.editor._filename filename = self.editor._filename
self.editor._proc.start.assert_called_with( self.editor._proc._proc.start.assert_called_with(
"bin", ["foo", filename, "bar"]) "bin", ["foo", filename, "bar"])
def test_in_arg_placeholder(self, stubbed_config): def test_in_arg_placeholder(self, stubbed_config):
@ -86,7 +86,7 @@ class TestArg:
'general': {'editor': ['bin', 'foo{}bar'], 'general': {'editor': ['bin', 'foo{}bar'],
'editor-encoding': 'utf-8'}} 'editor-encoding': 'utf-8'}}
self.editor.edit("") self.editor.edit("")
self.editor._proc.start.assert_called_with("bin", ["foo{}bar"]) self.editor._proc._proc.start.assert_called_with("bin", ["foo{}bar"])
class TestFileHandling: class TestFileHandling:
@ -101,8 +101,8 @@ class TestFileHandling:
def setup(self, monkeypatch, stubs, config_stub): def setup(self, monkeypatch, stubs, config_stub):
monkeypatch.setattr('qutebrowser.misc.editor.message', monkeypatch.setattr('qutebrowser.misc.editor.message',
stubs.MessageModule()) stubs.MessageModule())
monkeypatch.setattr('qutebrowser.misc.editor.QProcess', monkeypatch.setattr('qutebrowser.misc.editor.guiprocess.QProcess',
stubs.FakeQProcess()) stubs.fake_qprocess())
config_stub.data = {'general': {'editor': [''], config_stub.data = {'general': {'editor': [''],
'editor-encoding': 'utf-8'}} 'editor-encoding': 'utf-8'}}
monkeypatch.setattr('qutebrowser.misc.editor.config', config_stub) monkeypatch.setattr('qutebrowser.misc.editor.config', config_stub)
@ -113,7 +113,7 @@ class TestFileHandling:
self.editor.edit("") self.editor.edit("")
filename = self.editor._filename filename = self.editor._filename
assert os.path.exists(filename) assert os.path.exists(filename)
self.editor.on_proc_closed(0, QProcess.NormalExit) self.editor._proc.finished.emit(0, QProcess.NormalExit)
assert not os.path.exists(filename) assert not os.path.exists(filename)
def test_file_handling_closed_error(self, caplog): def test_file_handling_closed_error(self, caplog):
@ -122,7 +122,7 @@ class TestFileHandling:
filename = self.editor._filename filename = self.editor._filename
assert os.path.exists(filename) assert os.path.exists(filename)
with caplog.atLevel(logging.ERROR): with caplog.atLevel(logging.ERROR):
self.editor.on_proc_closed(1, QProcess.NormalExit) self.editor._proc.finished.emit(1, QProcess.NormalExit)
assert len(caplog.records()) == 2 assert len(caplog.records()) == 2
assert not os.path.exists(filename) assert not os.path.exists(filename)
@ -132,9 +132,9 @@ class TestFileHandling:
filename = self.editor._filename filename = self.editor._filename
assert os.path.exists(filename) assert os.path.exists(filename)
with caplog.atLevel(logging.ERROR): with caplog.atLevel(logging.ERROR):
self.editor.on_proc_error(QProcess.Crashed) self.editor._proc.error.emit(QProcess.Crashed)
assert len(caplog.records()) == 2 assert len(caplog.records()) == 2
self.editor.on_proc_closed(0, QProcess.CrashExit) self.editor._proc.finished.emit(0, QProcess.CrashExit)
assert not os.path.exists(filename) assert not os.path.exists(filename)
@ -148,8 +148,8 @@ class TestModifyTests:
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def setup(self, monkeypatch, stubs, config_stub): def setup(self, monkeypatch, stubs, config_stub):
monkeypatch.setattr('qutebrowser.misc.editor.QProcess', monkeypatch.setattr('qutebrowser.misc.editor.guiprocess.QProcess',
stubs.FakeQProcess()) stubs.fake_qprocess())
config_stub.data = {'general': {'editor': [''], config_stub.data = {'general': {'editor': [''],
'editor-encoding': 'utf-8'}} 'editor-encoding': 'utf-8'}}
monkeypatch.setattr('qutebrowser.misc.editor.config', config_stub) monkeypatch.setattr('qutebrowser.misc.editor.config', config_stub)
@ -182,7 +182,7 @@ class TestModifyTests:
self.editor.edit("") self.editor.edit("")
assert self._read() == "" assert self._read() == ""
self._write("Hello") self._write("Hello")
self.editor.on_proc_closed(0, QProcess.NormalExit) self.editor._proc.finished.emit(0, QProcess.NormalExit)
self.editor.editing_finished.emit.assert_called_with("Hello") self.editor.editing_finished.emit.assert_called_with("Hello")
def test_simple_input(self): def test_simple_input(self):
@ -190,7 +190,7 @@ class TestModifyTests:
self.editor.edit("Hello") self.editor.edit("Hello")
assert self._read() == "Hello" assert self._read() == "Hello"
self._write("World") self._write("World")
self.editor.on_proc_closed(0, QProcess.NormalExit) self.editor._proc.finished.emit(0, QProcess.NormalExit)
self.editor.editing_finished.emit.assert_called_with("World") self.editor.editing_finished.emit.assert_called_with("World")
def test_umlaut(self): def test_umlaut(self):
@ -198,7 +198,7 @@ class TestModifyTests:
self.editor.edit("Hällö Wörld") self.editor.edit("Hällö Wörld")
assert self._read() == "Hällö Wörld" assert self._read() == "Hällö Wörld"
self._write("Überprüfung") self._write("Überprüfung")
self.editor.on_proc_closed(0, QProcess.NormalExit) self.editor._proc.finished.emit(0, QProcess.NormalExit)
self.editor.editing_finished.emit.assert_called_with("Überprüfung") self.editor.editing_finished.emit.assert_called_with("Überprüfung")
def test_unicode(self): def test_unicode(self):
@ -220,8 +220,8 @@ class TestErrorMessage:
@pytest.yield_fixture(autouse=True) @pytest.yield_fixture(autouse=True)
def setup(self, monkeypatch, stubs, config_stub): def setup(self, monkeypatch, stubs, config_stub):
monkeypatch.setattr('qutebrowser.misc.editor.QProcess', monkeypatch.setattr('qutebrowser.misc.editor.guiprocess.QProcess',
stubs.FakeQProcess()) stubs.fake_qprocess())
monkeypatch.setattr('qutebrowser.misc.editor.message', monkeypatch.setattr('qutebrowser.misc.editor.message',
stubs.MessageModule()) stubs.MessageModule())
config_stub.data = {'general': {'editor': [''], config_stub.data = {'general': {'editor': [''],

View File

@ -0,0 +1,133 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015 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=protected-access
"""Tests for qutebrowser.misc.guiprocess."""
import sys
import textwrap
import pytest
from PyQt5.QtCore import QProcess
from qutebrowser.misc import guiprocess
# FIXME check statusbar messages
no_frozen = pytest.mark.skipif(
getattr(sys, 'frozen', False), reason="Can't be executed when frozen.")
def _py_proc(code):
"""Get a python executable and args list which executes the given code."""
return (sys.executable, ['-c', textwrap.dedent(code.strip('\n'))])
@pytest.fixture(autouse=True)
def mock_modules(monkeypatch, stubs):
monkeypatch.setattr('qutebrowser.misc.guiprocess.message',
stubs.MessageModule())
@pytest.yield_fixture()
def proc(qtbot):
"""A fixture providing a GUIProcess and cleaning it up after the test."""
p = guiprocess.GUIProcess(0, 'test')
yield p
if p._proc.state() == QProcess.Running:
with qtbot.waitSignal(p.finished, timeout=2000) as blocker:
p._proc.terminate()
if not blocker.signal_triggered:
p._proc.kill()
@pytest.fixture()
def fake_proc(monkeypatch, stubs):
"""A fixture providing a GUIProcess with a mocked QProcess."""
p = guiprocess.GUIProcess(0, 'test')
monkeypatch.setattr(p, '_proc', stubs.fake_qprocess())
return p
@no_frozen
def test_start(proc, qtbot):
"""Test simply starting a process."""
with qtbot.waitSignals([proc.started, proc.finished], raising=True,
timeout=10000):
argv = _py_proc("import sys; print('test'); sys.exit(0)")
proc.start(*argv)
assert bytes(proc._proc.readAll()).rstrip() == b'test'
@pytest.mark.parametrize('argv', [
no_frozen(_py_proc('import sys; sys.exit(0)')),
('does_not', 'exist'),
])
def test_start_detached(fake_proc, argv):
"""Test starting a detached process."""
fake_proc._proc.startDetached.return_value = (True, 0)
fake_proc.start_detached(*argv)
fake_proc._proc.startDetached.assert_called_with(*list(argv) + [None])
@no_frozen
def test_double_start(qtbot, proc):
"""Test starting a GUIProcess twice."""
with qtbot.waitSignal(proc.started, raising=True, timeout=2000):
argv = _py_proc("import time; time.sleep(10)")
proc.start(*argv)
with pytest.raises(ValueError):
proc.start('', [])
@no_frozen
def test_double_start_finished(qtbot, proc):
"""Test starting a GUIProcess twice (with the first call finished)."""
with qtbot.waitSignals([proc.started, proc.finished], raising=True,
timeout=2000):
argv = _py_proc("import sys; sys.exit(0)")
proc.start(*argv)
with qtbot.waitSignals([proc.started, proc.finished], raising=True,
timeout=2000):
argv = _py_proc("import sys; sys.exit(0)")
proc.start(*argv)
def test_cmd_args(proc):
"""Test the cmd and args attributes."""
cmd = 'does_not_exist'
args = ['arg1', 'arg2']
proc.start(cmd, args)
assert (proc.cmd, proc.args) == (cmd, args)
def test_error(qtbot, proc):
"""Test the process emitting an error."""
with qtbot.waitSignal(proc.error, raising=True):
proc.start('this_does_not_exist_either', [])
@no_frozen
def test_exit_unsuccessful(qtbot, proc):
with qtbot.waitSignal(proc.finished, raising=True, timeout=2000):
proc.start(*_py_proc('import sys; sys.exit(0)'))

View File

@ -27,6 +27,7 @@ from unittest import mock
from PyQt5.QtCore import pyqtSignal, QPoint, QProcess, QObject from PyQt5.QtCore import pyqtSignal, QPoint, QProcess, QObject
from PyQt5.QtNetwork import QNetworkRequest from PyQt5.QtNetwork import QNetworkRequest
from PyQt5.QtWidgets import QCommonStyle
class FakeKeyEvent: class FakeKeyEvent:
@ -72,8 +73,10 @@ class FakeQApplication:
"""Stub to insert as QApplication module.""" """Stub to insert as QApplication module."""
def __init__(self): def __init__(self, style=None):
self.instance = mock.Mock(return_value=self) self.instance = mock.Mock(return_value=self)
self.style = mock.Mock(spec=QCommonStyle)
self.style().metaObject().className.return_value = style
class FakeUrl: class FakeUrl:
@ -147,22 +150,13 @@ class FakeNetworkReply:
self.headers[key] = value self.headers[key] = value
class FakeQProcess(mock.Mock): def fake_qprocess():
"""Factory for a QProcess mock which has the QProcess enum values."""
"""QProcess stub. m = mock.Mock(spec=QProcess)
for attr in ['NormalExit', 'CrashExit', 'FailedToStart', 'Crashed',
Gets some enum values from the real QProcess. 'Timedout', 'WriteError', 'ReadError', 'UnknownError']:
""" setattr(m, attr, getattr(QProcess, attr))
return m
NormalExit = QProcess.NormalExit
CrashExit = QProcess.CrashExit
FailedToStart = QProcess.FailedToStart
Crashed = QProcess.Crashed
Timedout = QProcess.Timedout
WriteError = QProcess.WriteError
ReadError = QProcess.ReadError
UnknownError = QProcess.UnknownError
class FakeSignal: class FakeSignal:
@ -267,14 +261,16 @@ class MessageModule:
"""A drop-in replacement for qutebrowser.utils.message.""" """A drop-in replacement for qutebrowser.utils.message."""
def error(self, _win_id, message, _immediately=False): # pylint: disable=unused-argument
def error(self, _win_id, message, immediately=False):
"""Log an error to the message logger.""" """Log an error to the message logger."""
logging.getLogger('message').error(message) logging.getLogger('message').error(message)
def warning(self, _win_id, message, _immediately=False): def warning(self, _win_id, message, immediately=False):
"""Log a warning to the message logger.""" """Log a warning to the message logger."""
logging.getLogger('message').warning(message) logging.getLogger('message').warning(message)
def info(self, _win_id, message, _immediately=True): def info(self, _win_id, message, immediately=True):
"""Log an info message to the message logger.""" """Log an info message to the message logger."""
logging.getLogger('message').info(message) logging.getLogger('message').info(message)

View File

@ -27,7 +27,6 @@ import itertools
import sys import sys
import pytest import pytest
from PyQt5.QtCore import qWarning
from qutebrowser.utils import log from qutebrowser.utils import log
@ -214,7 +213,7 @@ class TestInitLog:
@pytest.fixture @pytest.fixture
def args(self): def args(self):
"""Fixture providing an argparse namespace.""" """Fixture providing an argparse namespace for init_log."""
return argparse.Namespace(debug=True, loglevel=logging.DEBUG, return argparse.Namespace(debug=True, loglevel=logging.DEBUG,
color=True, loglines=10, logfilter="") color=True, loglines=10, logfilter="")
@ -230,33 +229,37 @@ class TestHideQtWarning:
"""Tests for hide_qt_warning/QtWarningFilter.""" """Tests for hide_qt_warning/QtWarningFilter."""
def test_unfiltered(self, caplog): @pytest.fixture()
def logger(self):
return logging.getLogger('qt-tests')
def test_unfiltered(self, logger, caplog):
"""Test a message which is not filtered.""" """Test a message which is not filtered."""
with log.hide_qt_warning("World", logger='qt-tests'): with log.hide_qt_warning("World", logger='qt-tests'):
with caplog.atLevel(logging.WARNING, logger='qt-tests'): with caplog.atLevel(logging.WARNING, logger='qt-tests'):
qWarning("Hello World") logger.warning("Hello World")
assert len(caplog.records()) == 1 assert len(caplog.records()) == 1
record = caplog.records()[0] record = caplog.records()[0]
assert record.levelname == 'WARNING' assert record.levelname == 'WARNING'
assert record.message == "Hello World" assert record.message == "Hello World"
def test_filtered_exact(self, caplog): def test_filtered_exact(self, logger, caplog):
"""Test a message which is filtered (exact match).""" """Test a message which is filtered (exact match)."""
with log.hide_qt_warning("Hello", logger='qt-tests'): with log.hide_qt_warning("Hello", logger='qt-tests'):
with caplog.atLevel(logging.WARNING, logger='qt-tests'): with caplog.atLevel(logging.WARNING, logger='qt-tests'):
qWarning("Hello") logger.warning("Hello")
assert not caplog.records() assert not caplog.records()
def test_filtered_start(self, caplog): def test_filtered_start(self, logger, caplog):
"""Test a message which is filtered (match at line start).""" """Test a message which is filtered (match at line start)."""
with log.hide_qt_warning("Hello", logger='qt-tests'): with log.hide_qt_warning("Hello", logger='qt-tests'):
with caplog.atLevel(logging.WARNING, logger='qt-tests'): with caplog.atLevel(logging.WARNING, logger='qt-tests'):
qWarning("Hello World") logger.warning("Hello World")
assert not caplog.records() assert not caplog.records()
def test_filtered_whitespace(self, caplog): def test_filtered_whitespace(self, logger, caplog):
"""Test a message which is filtered (match with whitespace).""" """Test a message which is filtered (match with whitespace)."""
with log.hide_qt_warning("Hello", logger='qt-tests'): with log.hide_qt_warning("Hello", logger='qt-tests'):
with caplog.atLevel(logging.WARNING, logger='qt-tests'): with caplog.atLevel(logging.WARNING, logger='qt-tests'):
qWarning(" Hello World ") logger.warning(" Hello World ")
assert not caplog.records() assert not caplog.records()

View File

@ -19,15 +19,51 @@
"""Tests for qutebrowser.utils.qtutils.""" """Tests for qutebrowser.utils.qtutils."""
import io
import os
import sys import sys
import operator
import os.path
try:
from test import test_file # pylint: disable=no-name-in-module
except ImportError:
# Debian patches Python to remove the tests...
test_file = None
import pytest import pytest
import unittest
import unittest.mock
from PyQt5.QtCore import (QDataStream, QPoint, QUrl, QByteArray, QIODevice,
QTimer, QBuffer, QFile, QProcess)
from PyQt5.QtWidgets import QApplication
from qutebrowser import qutebrowser from qutebrowser import qutebrowser
from qutebrowser.utils import qtutils from qutebrowser.utils import qtutils
import overflow_test_cases import overflow_test_cases
@pytest.mark.parametrize('qversion, version, op, expected', [
('5.4.0', '5.4.0', operator.ge, True),
('5.4.0', '5.4.0', operator.eq, True),
('5.4.0', '5.4', operator.eq, True),
('5.4.1', '5.4', operator.ge, True),
('5.3.2', '5.4', operator.ge, False),
('5.3.0', '5.3.2', operator.ge, False),
])
def test_version_check(monkeypatch, qversion, version, op, expected):
"""Test for version_check().
Args:
monkeypatch: The pytest monkeypatch fixture.
qversion: The version to set as fake qVersion().
version: The version to compare with.
op: The operator to use when comparing.
expected: The expected result.
"""
monkeypatch.setattr('qutebrowser.utils.qtutils.qVersion', lambda: qversion)
assert qtutils.version_check(version, op) == expected
class TestCheckOverflow: class TestCheckOverflow:
"""Test check_overflow.""" """Test check_overflow."""
@ -68,20 +104,18 @@ class TestGetQtArgs:
mocker.patch.object(parser, 'exit', side_effect=Exception) mocker.patch.object(parser, 'exit', side_effect=Exception)
return parser return parser
def test_no_qt_args(self, parser): @pytest.mark.parametrize('args, expected', [
# No Qt arguments
(['--debug'], [sys.argv[0]]),
# Qt flag
(['--debug', '--qt-reverse', '--nocolor'], [sys.argv[0], '-reverse']),
# Qt argument with value
(['--qt-stylesheet', 'foo'], [sys.argv[0], '-stylesheet', 'foo']),
])
def test_qt_args(self, args, expected, parser):
"""Test commandline with no Qt arguments given.""" """Test commandline with no Qt arguments given."""
args = parser.parse_args(['--debug']) parsed = parser.parse_args(args)
assert qtutils.get_args(args) == [sys.argv[0]] assert qtutils.get_args(parsed) == expected
def test_qt_flag(self, parser):
"""Test commandline with a Qt flag."""
args = parser.parse_args(['--debug', '--qt-reverse', '--nocolor'])
assert qtutils.get_args(args) == [sys.argv[0], '-reverse']
def test_qt_arg(self, parser):
"""Test commandline with a Qt argument."""
args = parser.parse_args(['--qt-stylesheet', 'foobar'])
assert qtutils.get_args(args) == [sys.argv[0], '-stylesheet', 'foobar']
def test_qt_both(self, parser): def test_qt_both(self, parser):
"""Test commandline with a Qt argument and flag.""" """Test commandline with a Qt argument and flag."""
@ -91,3 +125,838 @@ class TestGetQtArgs:
assert '-reverse' in qt_args assert '-reverse' in qt_args
assert '-stylesheet' in qt_args assert '-stylesheet' in qt_args
assert 'foobar' in qt_args assert 'foobar' in qt_args
@pytest.mark.parametrize('os_name, qversion, expected', [
('linux', '5.2.1', True), # unaffected OS
('linux', '5.4.1', True), # unaffected OS
('nt', '5.2.1', False),
('nt', '5.3.0', True), # unaffected Qt version
('nt', '5.4.1', True), # unaffected Qt version
])
def test_check_print_compat(os_name, qversion, expected, monkeypatch):
"""Test check_print_compat.
Args:
os_name: The fake os.name to set.
qversion: The fake qVersion() to set.
expected: The expected return value.
"""
monkeypatch.setattr('qutebrowser.utils.qtutils.os.name', os_name)
monkeypatch.setattr('qutebrowser.utils.qtutils.qVersion', lambda: qversion)
assert qtutils.check_print_compat() == expected
class QtObject:
"""Fake Qt object for test_ensure."""
def __init__(self, valid=True, null=False, error=None):
self._valid = valid
self._null = null
self._error = error
def __repr__(self):
return '<QtObject>'
def errorString(self):
"""Get the fake error, or raise AttributeError if set to None."""
if self._error is None:
raise AttributeError
else:
return self._error
def isValid(self):
return self._valid
def isNull(self):
return self._null
@pytest.mark.parametrize('func_name, obj, raising, exc_reason, exc_str', [
# ensure_valid, good examples
('ensure_valid', QtObject(valid=True, null=True), False, None, None),
('ensure_valid', QtObject(valid=True, null=False), False, None, None),
# ensure_valid, bad examples
('ensure_valid', QtObject(valid=False, null=True), True, None,
'<QtObject> is not valid'),
('ensure_valid', QtObject(valid=False, null=False), True, None,
'<QtObject> is not valid'),
('ensure_valid', QtObject(valid=False, null=True, error='Test'), True,
'Test', '<QtObject> is not valid: Test'),
# ensure_not_null, good examples
('ensure_not_null', QtObject(valid=True, null=False), False, None, None),
('ensure_not_null', QtObject(valid=False, null=False), False, None, None),
# ensure_not_null, bad examples
('ensure_not_null', QtObject(valid=True, null=True), True, None,
'<QtObject> is null'),
('ensure_not_null', QtObject(valid=False, null=True), True, None,
'<QtObject> is null'),
('ensure_not_null', QtObject(valid=False, null=True, error='Test'), True,
'Test', '<QtObject> is null: Test'),
])
def test_ensure(func_name, obj, raising, exc_reason, exc_str):
"""Test ensure_valid and ensure_not_null.
The function is parametrized as they do nearly the same.
Args:
func_name: The name of the function to call.
obj: The object to test with.
raising: Whether QtValueError is expected to be raised.
exc_reason: The expected .reason attribute of the exception.
exc_str: The expected string of the exception.
"""
func = getattr(qtutils, func_name)
if raising:
with pytest.raises(qtutils.QtValueError) as excinfo:
func(obj)
assert excinfo.value.reason == exc_reason
assert str(excinfo.value) == exc_str
else:
func(obj)
@pytest.mark.parametrize('status, raising, message', [
(QDataStream.Ok, False, None),
(QDataStream.ReadPastEnd, True, "The data stream has read past the end of "
"the data in the underlying device."),
(QDataStream.ReadCorruptData, True, "The data stream has read corrupt "
"data."),
(QDataStream.WriteFailed, True, "The data stream cannot write to the "
"underlying device."),
])
def test_check_qdatastream(status, raising, message):
"""Test check_qdatastream.
Args:
status: The status to set on the QDataStream we test with.
raising: Whether check_qdatastream is expected to raise OSError.
message: The expected exception string.
"""
stream = QDataStream()
stream.setStatus(status)
if raising:
with pytest.raises(OSError) as excinfo:
qtutils.check_qdatastream(stream)
assert str(excinfo.value) == message
else:
qtutils.check_qdatastream(stream)
def test_qdatastream_status_count():
"""Make sure no new members are added to QDataStream.Status."""
values = vars(QDataStream).values()
status_vals = [e for e in values if isinstance(e, QDataStream.Status)]
assert len(status_vals) == 4
@pytest.mark.parametrize('obj', [
QPoint(23, 42),
QUrl('http://www.qutebrowser.org/'),
])
def test_serialize(obj):
"""Test a serialize/deserialize round trip.
Args:
obj: The object to test with.
"""
new_obj = type(obj)()
qtutils.deserialize(qtutils.serialize(obj), new_obj)
assert new_obj == obj
class TestSerializeStream:
"""Tests for serialize_stream and deserialize_stream."""
def _set_status(self, stream, status):
"""Helper function so mocks can set an error status when used."""
stream.status.return_value = status
@pytest.fixture
def stream_mock(self):
"""Fixture providing a QDataStream-like mock."""
m = unittest.mock.MagicMock(spec=QDataStream)
m.status.return_value = QDataStream.Ok
return m
def test_serialize_pre_error_mock(self, stream_mock):
"""Test serialize_stream with an error already set."""
stream_mock.status.return_value = QDataStream.ReadCorruptData
with pytest.raises(OSError) as excinfo:
qtutils.serialize_stream(stream_mock, QPoint())
assert not stream_mock.__lshift__.called
assert str(excinfo.value) == "The data stream has read corrupt data."
def test_serialize_post_error_mock(self, stream_mock):
"""Test serialize_stream with an error while serializing."""
obj = QPoint()
stream_mock.__lshift__.side_effect = lambda _other: self._set_status(
stream_mock, QDataStream.ReadCorruptData)
with pytest.raises(OSError) as excinfo:
qtutils.serialize_stream(stream_mock, obj)
assert stream_mock.__lshift__.called_once_with(obj)
assert str(excinfo.value) == "The data stream has read corrupt data."
def test_deserialize_pre_error_mock(self, stream_mock):
"""Test deserialize_stream with an error already set."""
stream_mock.status.return_value = QDataStream.ReadCorruptData
with pytest.raises(OSError) as excinfo:
qtutils.deserialize_stream(stream_mock, QPoint())
assert not stream_mock.__rshift__.called
assert str(excinfo.value) == "The data stream has read corrupt data."
def test_deserialize_post_error_mock(self, stream_mock):
"""Test deserialize_stream with an error while deserializing."""
obj = QPoint()
stream_mock.__rshift__.side_effect = lambda _other: self._set_status(
stream_mock, QDataStream.ReadCorruptData)
with pytest.raises(OSError) as excinfo:
qtutils.deserialize_stream(stream_mock, obj)
assert stream_mock.__rshift__.called_once_with(obj)
assert str(excinfo.value) == "The data stream has read corrupt data."
def test_round_trip_real_stream(self):
"""Test a round trip with a real QDataStream."""
src_obj = QPoint(23, 42)
dest_obj = QPoint()
data = QByteArray()
write_stream = QDataStream(data, QIODevice.WriteOnly)
qtutils.serialize_stream(write_stream, src_obj)
read_stream = QDataStream(data, QIODevice.ReadOnly)
qtutils.deserialize_stream(read_stream, dest_obj)
assert src_obj == dest_obj
@pytest.mark.qt_log_ignore('^QIODevice::write.*: ReadOnly device')
def test_serialize_readonly_stream(self):
"""Test serialize_stream with a read-only stream."""
data = QByteArray()
stream = QDataStream(data, QIODevice.ReadOnly)
with pytest.raises(OSError) as excinfo:
qtutils.serialize_stream(stream, QPoint())
assert str(excinfo.value) == ("The data stream cannot write to the "
"underlying device.")
@pytest.mark.qt_log_ignore('QIODevice::read.*: WriteOnly device')
def test_deserialize_writeonly_stream(self):
"""Test deserialize_stream with a write-only stream."""
data = QByteArray()
obj = QPoint()
stream = QDataStream(data, QIODevice.WriteOnly)
with pytest.raises(OSError) as excinfo:
qtutils.deserialize_stream(stream, obj)
assert str(excinfo.value) == ("The data stream has read past the end "
"of the data in the underlying device.")
class SavefileTestException(Exception):
"""Exception raised in TestSavefileOpen for testing."""
pass
class TestSavefileOpen:
"""Tests for savefile_open."""
## Tests with a mock testing that the needed methods are called.
@pytest.yield_fixture
def qsavefile_mock(self, mocker):
"""Mock for QSaveFile."""
m = mocker.patch('qutebrowser.utils.qtutils.QSaveFile')
instance = m()
yield instance
instance.commit.assert_called_once_with()
def test_mock_open_error(self, qsavefile_mock):
"""Test with a mock and a failing open()."""
qsavefile_mock.open.return_value = False
qsavefile_mock.errorString.return_value = "Hello World"
with pytest.raises(OSError) as excinfo:
with qtutils.savefile_open('filename'):
pass
qsavefile_mock.open.assert_called_once_with(QIODevice.WriteOnly)
qsavefile_mock.cancelWriting.assert_called_once_with()
assert str(excinfo.value) == "Hello World"
def test_mock_exception(self, qsavefile_mock):
"""Test with a mock and an exception in the block."""
qsavefile_mock.open.return_value = True
with pytest.raises(SavefileTestException):
with qtutils.savefile_open('filename'):
raise SavefileTestException
qsavefile_mock.open.assert_called_once_with(QIODevice.WriteOnly)
qsavefile_mock.cancelWriting.assert_called_once_with()
def test_mock_commit_failed(self, qsavefile_mock):
"""Test with a mock and an exception in the block."""
qsavefile_mock.open.return_value = True
qsavefile_mock.commit.return_value = False
with pytest.raises(OSError) as excinfo:
with qtutils.savefile_open('filename'):
pass
qsavefile_mock.open.assert_called_once_with(QIODevice.WriteOnly)
assert not qsavefile_mock.cancelWriting.called
assert not qsavefile_mock.errorString.called
assert str(excinfo.value) == "Commit failed!"
def test_mock_successful(self, qsavefile_mock):
"""Test with a mock and a successful write."""
qsavefile_mock.open.return_value = True
qsavefile_mock.errorString.return_value = "Hello World"
qsavefile_mock.commit.return_value = True
qsavefile_mock.write.side_effect = len
qsavefile_mock.isOpen.return_value = True
with qtutils.savefile_open('filename') as f:
f.write("Hello World")
qsavefile_mock.open.assert_called_once_with(QIODevice.WriteOnly)
assert not qsavefile_mock.cancelWriting.called
qsavefile_mock.write.assert_called_once_with(b"Hello World")
## Tests with real files
@pytest.mark.parametrize('data', ["Hello World", "Snowman! ☃"])
def test_utf8(self, data, tmpdir):
"""Test with UTF8 data."""
filename = tmpdir / 'foo'
filename.write("Old data")
with qtutils.savefile_open(str(filename)) as f:
f.write(data)
assert tmpdir.listdir() == [filename]
assert filename.read_text(encoding='utf-8') == data
def test_binary(self, tmpdir):
"""Test with binary data."""
filename = tmpdir / 'foo'
with qtutils.savefile_open(str(filename), binary=True) as f:
f.write(b'\xde\xad\xbe\xef')
assert tmpdir.listdir() == [filename]
assert filename.read_binary() == b'\xde\xad\xbe\xef'
def test_exception(self, tmpdir):
"""Test with an exception in the block."""
filename = tmpdir / 'foo'
filename.write("Old content")
with pytest.raises(SavefileTestException):
with qtutils.savefile_open(str(filename)) as f:
f.write("Hello World!")
raise SavefileTestException
assert tmpdir.listdir() == [filename]
assert filename.read_text(encoding='utf-8') == "Old content"
def test_existing_dir(self, tmpdir):
"""Test with the filename already occupied by a directory."""
filename = tmpdir / 'foo'
filename.mkdir()
with pytest.raises(OSError) as excinfo:
with qtutils.savefile_open(str(filename)):
pass
errors = ["Filename refers to a directory", # Qt >= 5.4
"Commit failed!"] # older Qt versions
assert str(excinfo.value) in errors
assert tmpdir.listdir() == [filename]
def test_failing_commit(self, tmpdir):
"""Test with the file being closed before comitting."""
filename = tmpdir / 'foo'
with pytest.raises(OSError) as excinfo:
with qtutils.savefile_open(str(filename), binary=True) as f:
f.write(b'Hello')
f.dev.commit() # provoke failing "real" commit
assert str(excinfo.value) == "Commit failed!"
assert tmpdir.listdir() == [filename]
def test_line_endings(self, tmpdir):
"""Make sure line endings are translated correctly.
See https://github.com/The-Compiler/qutebrowser/issues/309
"""
filename = tmpdir / 'foo'
with qtutils.savefile_open(str(filename)) as f:
f.write('foo\nbar\nbaz')
data = filename.read_binary()
if os.name == 'nt':
assert data == b'foo\r\nbar\r\nbaz'
else:
assert data == b'foo\nbar\nbaz'
@pytest.mark.parametrize('orgname, expected', [(None, ''), ('test', 'test')])
def test_unset_organization(orgname, expected):
"""Test unset_organization.
Args:
orgname: The organizationName to set initially.
expected: The organizationName which is expected when reading back.
"""
app = QApplication.instance()
app.setOrganizationName(orgname)
assert app.organizationName() == expected # sanity check
with qtutils.unset_organization():
assert app.organizationName() == ''
assert app.organizationName() == expected
if test_file is not None:
# If we were able to import Python's test_file module, we run some code
# here which defines unittest TestCases to run the python tests over
# PyQIODevice.
@pytest.yield_fixture(scope='session', autouse=True)
def clean_up_python_testfile():
"""Clean up the python testfile after tests if tests didn't."""
yield
try:
os.remove(test_file.TESTFN)
except FileNotFoundError:
pass
class PyIODeviceTestMixin:
"""Some helper code to run Python's tests with PyQIODevice.
Attributes:
_data: A QByteArray containing the data in memory.
f: The opened PyQIODevice.
"""
def setUp(self):
"""Set up self.f using a PyQIODevice instead of a real file."""
self._data = QByteArray()
self.f = self.open(test_file.TESTFN, 'wb')
def open(self, _fname, mode):
"""Open an in-memory PyQIODevice instead of a real file."""
modes = {
'wb': QIODevice.WriteOnly | QIODevice.Truncate,
'w': QIODevice.WriteOnly | QIODevice.Text | QIODevice.Truncate,
'rb': QIODevice.ReadOnly,
'r': QIODevice.ReadOnly | QIODevice.Text,
}
try:
qt_mode = modes[mode]
except KeyError:
raise ValueError("Invalid mode {}!".format(mode))
f = QBuffer(self._data)
f.open(qt_mode)
qiodev = qtutils.PyQIODevice(f)
# Make sure tests using name/mode don't blow up.
qiodev.name = test_file.TESTFN
qiodev.mode = mode
# Create empty TESTFN file because the Python tests try to unlink
# it.after the test.
open(test_file.TESTFN, 'w', encoding='utf-8').close()
return qiodev
class PyAutoFileTests(PyIODeviceTestMixin, test_file.AutoFileTests,
unittest.TestCase):
"""Unittest testcase to run Python's AutoFileTests."""
def testReadinto_text(self):
"""Skip this test as BufferedIOBase seems to fail it."""
pass
class PyOtherFileTests(PyIODeviceTestMixin, test_file.OtherFileTests,
unittest.TestCase):
"""Unittest testcase to run Python's OtherFileTests."""
def testSetBufferSize(self):
"""Skip this test as setting buffer size is unsupported."""
pass
def testTruncateOnWindows(self):
"""Skip this test truncating is unsupported."""
pass
class FailingQIODevice(QIODevice):
"""A fake QIODevice where reads/writes fail."""
def isOpen(self):
return True
def isReadable(self):
return True
def isWritable(self):
return True
def write(self, _data):
"""Simulate failed write."""
self.setErrorString("Writing failed")
return -1
def read(self, _maxsize):
"""Simulate failed read."""
self.setErrorString("Reading failed")
return None
def readAll(self):
return self.read(0)
def readLine(self, maxsize):
return self.read(maxsize)
class TestPyQIODevice:
"""Tests for PyQIODevice."""
@pytest.yield_fixture
def pyqiodev(self):
"""Fixture providing a PyQIODevice with a QByteArray to test."""
data = QByteArray()
f = QBuffer(data)
qiodev = qtutils.PyQIODevice(f)
yield qiodev
qiodev.close()
@pytest.fixture
def pyqiodev_failing(self):
"""Fixture providing a PyQIODevice with a FailingQIODevice to test."""
failing = FailingQIODevice()
return qtutils.PyQIODevice(failing)
@pytest.mark.parametrize('method, args', [
('seek', [0]),
('flush', []),
('isatty', []),
('readline', []),
('tell', []),
('write', [b'']),
('read', []),
])
def test_closed_device(self, pyqiodev, method, args):
"""Test various methods with a closed device.
Args:
method: The name of the method to call.
args: The arguments to pass.
"""
func = getattr(pyqiodev, method)
with pytest.raises(ValueError) as excinfo:
func(*args)
assert str(excinfo.value) == "IO operation on closed device!"
@pytest.mark.parametrize('method', ['readline', 'read'])
def test_unreadable(self, pyqiodev, method):
"""Test methods with an unreadable device.
Args:
method: The name of the method to call.
"""
pyqiodev.open(QIODevice.WriteOnly)
func = getattr(pyqiodev, method)
with pytest.raises(OSError) as excinfo:
func()
assert str(excinfo.value) == "Trying to read unreadable file!"
def test_unwritable(self, pyqiodev):
"""Test writing with a read-only device."""
pyqiodev.open(QIODevice.ReadOnly)
with pytest.raises(OSError) as excinfo:
pyqiodev.write(b'')
assert str(excinfo.value) == "Trying to write to unwritable file!"
@pytest.mark.parametrize('data', [b'12345', b''])
def test_len(self, pyqiodev, data):
"""Test len()/__len__.
Args:
data: The data to write before checking if the length equals
len(data).
"""
pyqiodev.open(QIODevice.WriteOnly)
pyqiodev.write(data)
assert len(pyqiodev) == len(data)
def test_failing_open(self, tmpdir):
"""Test open() which fails (because it's an existant directory)."""
qf = QFile(str(tmpdir))
dev = qtutils.PyQIODevice(qf)
with pytest.raises(OSError) as excinfo:
dev.open(QIODevice.WriteOnly)
errors = ['Access is denied.', # Linux/OS X
'Is a directory'] # Windows
assert str(excinfo.value) in errors
assert dev.closed
def test_fileno(self, pyqiodev):
with pytest.raises(io.UnsupportedOperation):
pyqiodev.fileno()
@pytest.mark.qt_log_ignore('^QBuffer::seek: Invalid pos:')
@pytest.mark.parametrize('offset, whence, pos, data, raising', [
(0, io.SEEK_SET, 0, b'1234567890', False),
(42, io.SEEK_SET, 0, b'1234567890', True),
(8, io.SEEK_CUR, 8, b'90', False),
(-5, io.SEEK_CUR, 0, b'1234567890', True),
(-2, io.SEEK_END, 8, b'90', False),
(2, io.SEEK_END, 0, b'1234567890', True),
(0, io.SEEK_END, 10, b'', False),
])
def test_seek_tell(self, pyqiodev, offset, whence, pos, data, raising):
"""Test seek() and tell().
The initial position when these tests run is 0.
Args:
offset: The offset to pass to .seek().
whence: The whence argument to pass to .seek().
pos: The expected position after seeking.
data: The expected data to read after seeking.
raising: Whether seeking should raise OSError.
"""
with pyqiodev.open(QIODevice.WriteOnly) as f:
f.write(b'1234567890')
pyqiodev.open(QIODevice.ReadOnly)
if raising:
with pytest.raises(OSError) as excinfo:
pyqiodev.seek(offset, whence)
assert str(excinfo.value) == "seek failed!"
else:
pyqiodev.seek(offset, whence)
assert pyqiodev.tell() == pos
assert pyqiodev.read() == data
def test_seek_unsupported(self, pyqiodev):
"""Test seeking with unsupported whence arguments."""
if hasattr(os, 'SEEK_HOLE'):
whence = os.SEEK_HOLE # pylint: disable=no-member
elif hasattr(os, 'SEEK_DATA'):
whence = os.SEEK_DATA # pylint: disable=no-member
else:
pytest.skip("Needs os.SEEK_HOLE or os.SEEK_DATA available.")
pyqiodev.open(QIODevice.ReadOnly)
with pytest.raises(io.UnsupportedOperation):
pyqiodev.seek(0, whence)
@pytest.mark.skipif(getattr(sys, 'frozen', False),
reason="Can't be executed when frozen.")
def test_qprocess(self):
"""Test PyQIODevice with a QProcess which is non-sequential.
This also verifies seek() and tell() behave as expected.
"""
proc = QProcess()
proc.start(sys.executable, ['-c', 'print("Hello World")'])
dev = qtutils.PyQIODevice(proc)
assert not dev.closed
with pytest.raises(OSError) as excinfo:
dev.seek(0)
assert str(excinfo.value) == 'Random access not allowed!'
with pytest.raises(OSError) as excinfo:
dev.tell()
assert str(excinfo.value) == 'Random access not allowed!'
proc.waitForFinished(1000)
proc.kill()
assert bytes(dev.read()).rstrip() == b'Hello World'
def test_truncate(self, pyqiodev):
with pytest.raises(io.UnsupportedOperation):
pyqiodev.truncate()
def test_closed(self, pyqiodev):
"""Test the closed attribute."""
assert pyqiodev.closed
pyqiodev.open(QIODevice.ReadOnly)
assert not pyqiodev.closed
pyqiodev.close()
assert pyqiodev.closed
def test_contextmanager(self, pyqiodev):
"""Make sure using the PyQIODevice as context manager works."""
assert pyqiodev.closed
with pyqiodev.open(QIODevice.ReadOnly) as f:
assert not f.closed
assert f is pyqiodev
assert pyqiodev.closed
def test_flush(self, pyqiodev):
"""Make sure flushing doesn't raise an exception."""
pyqiodev.open(QIODevice.WriteOnly)
pyqiodev.write(b'test')
pyqiodev.flush()
@pytest.mark.parametrize('method, ret', [
('isatty', False),
('seekable', True),
])
def test_bools(self, method, ret, pyqiodev):
"""Make sure simple bool arguments return the right thing.
Args:
method: The name of the method to call.
ret: The return value we expect.
"""
pyqiodev.open(QIODevice.WriteOnly)
func = getattr(pyqiodev, method)
assert func() == ret
@pytest.mark.parametrize('mode, readable, writable', [
(QIODevice.ReadOnly, True, False),
(QIODevice.ReadWrite, True, True),
(QIODevice.WriteOnly, False, True),
])
def test_readable_writable(self, mode, readable, writable, pyqiodev):
"""Test readable() and writable().
Args:
mode: The mode to open the PyQIODevice in.
readable: Whether the device should be readable.
writable: Whether the device should be writable.
"""
assert not pyqiodev.readable()
assert not pyqiodev.writable()
pyqiodev.open(mode)
assert pyqiodev.readable() == readable
assert pyqiodev.writable() == writable
@pytest.mark.parametrize('size, chunks', [
(-1, [b'one\n', b'two\n', b'three', b'']),
(0, [b'', b'', b'', b'']),
(2, [b'on', b'e\n', b'tw', b'o\n', b'th', b're', b'e']),
(10, [b'one\n', b'two\n', b'three', b'']),
])
def test_readline(self, size, chunks, pyqiodev):
"""Test readline() with different sizes.
Args:
size: The size to pass to readline()
chunks: A list of expected chunks to read.
"""
with pyqiodev.open(QIODevice.WriteOnly) as f:
f.write(b'one\ntwo\nthree')
pyqiodev.open(QIODevice.ReadOnly)
for i, chunk in enumerate(chunks, start=1):
print("Expecting chunk {}: {!r}".format(i, chunk))
assert pyqiodev.readline(size) == chunk
def test_write(self, pyqiodev):
"""Make sure writing and re-reading works."""
with pyqiodev.open(QIODevice.WriteOnly) as f:
f.write(b'foo\n')
f.write(b'bar\n')
pyqiodev.open(QIODevice.ReadOnly)
assert pyqiodev.read() == b'foo\nbar\n'
def test_write_error(self, pyqiodev_failing):
"""Test writing with FailingQIODevice."""
with pytest.raises(OSError) as excinfo:
pyqiodev_failing.write(b'x')
assert str(excinfo.value) == 'Writing failed'
@pytest.mark.skipif(os.name != 'posix', reason="Needs a POSIX OS.")
@pytest.mark.skipif(not os.path.exists('/dev/full'),
reason="Needs /dev/full.")
def test_write_error_real(self):
"""Test a real write error with /dev/full on supported systems."""
qf = QFile('/dev/full')
qf.open(QIODevice.WriteOnly | QIODevice.Unbuffered)
dev = qtutils.PyQIODevice(qf)
with pytest.raises(OSError) as excinfo:
dev.write(b'foo')
qf.close()
assert str(excinfo.value) == 'No space left on device'
@pytest.mark.parametrize('size, chunks', [
(-1, [b'1234567890']),
(0, [b'']),
(3, [b'123', b'456', b'789', b'0']),
(20, [b'1234567890'])
])
def test_read(self, size, chunks, pyqiodev):
"""Test reading with different sizes.
Args:
size: The size to pass to read()
chunks: A list of expected data chunks.
"""
with pyqiodev.open(QIODevice.WriteOnly) as f:
f.write(b'1234567890')
pyqiodev.open(QIODevice.ReadOnly)
for i, chunk in enumerate(chunks):
print("Expecting chunk {}: {!r}".format(i, chunk))
assert pyqiodev.read(size) == chunk
@pytest.mark.parametrize('method, args', [
('read', []),
('read', [5]),
('readline', []),
('readline', [5]),
])
def test_failing_reads(self, method, args, pyqiodev_failing):
"""Test reading with a FailingQIODevice.
Args:
method: The name of the method to call.
args: A list of arguments to pass.
"""
func = getattr(pyqiodev_failing, method)
with pytest.raises(OSError) as excinfo:
func(*args)
assert str(excinfo.value) == 'Reading failed'
class TestEventLoop:
"""Tests for EventLoop.
Attributes:
loop: The EventLoop we're testing.
"""
# pylint: disable=protected-access
def _assert_executing(self):
"""Slot which gets called from timers to be sure the loop runs."""
assert self.loop._executing
def _double_exec(self):
"""Slot which gets called from timers to assert double-exec fails."""
with pytest.raises(AssertionError):
self.loop.exec_()
def test_normal_exec(self):
"""Test exec_ without double-executing."""
self.loop = qtutils.EventLoop()
QTimer.singleShot(100, self._assert_executing)
QTimer.singleShot(200, self.loop.quit)
self.loop.exec_()
assert not self.loop._executing
def test_double_exec(self):
"""Test double-executing."""
self.loop = qtutils.EventLoop()
QTimer.singleShot(100, self._assert_executing)
QTimer.singleShot(200, self._double_exec)
QTimer.singleShot(300, self._assert_executing)
QTimer.singleShot(400, self.loop.quit)
self.loop.exec_()
assert not self.loop._executing

View File

@ -17,6 +17,8 @@
# 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/>.
# pylint: disable=protected-access
"""Tests for qutebrowser.utils.standarddir.""" """Tests for qutebrowser.utils.standarddir."""
import os import os
@ -24,7 +26,10 @@ import os.path
import sys import sys
import types import types
import collections import collections
import logging
import textwrap
from PyQt5.QtCore import QStandardPaths
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
import pytest import pytest
@ -44,8 +49,61 @@ def change_qapp_name():
QApplication.instance().setApplicationName(old_name) QApplication.instance().setApplicationName(old_name)
@pytest.fixture
def no_cachedir_tag(monkeypatch):
"""Fixture to prevent writing a CACHEDIR.TAG."""
monkeypatch.setattr('qutebrowser.utils.standarddir._init_cachedir_tag',
lambda: None)
@pytest.fixture(autouse=True)
@pytest.mark.usefixtures('no_cachedir_tag')
def reset_standarddir():
standarddir.init(None)
@pytest.mark.parametrize('data_subdir, config_subdir, expected', [
('foo', 'foo', 'foo/data'),
('foo', 'bar', 'foo'),
])
def test_get_fake_windows_equal_dir(data_subdir, config_subdir, expected,
monkeypatch, tmpdir):
"""Test _get with a fake Windows OS with equal data/config dirs."""
locations = {
QStandardPaths.DataLocation: str(tmpdir / data_subdir),
QStandardPaths.ConfigLocation: str(tmpdir / config_subdir),
}
monkeypatch.setattr('qutebrowser.utils.standarddir.os.name', 'nt')
monkeypatch.setattr(
'qutebrowser.utils.standarddir.QStandardPaths.writableLocation',
locations.get)
expected = str(tmpdir / expected)
assert standarddir.data() == expected
class TestWritableLocation:
"""Tests for _writable_location."""
def test_empty(self, monkeypatch):
"""Test QStandardPaths returning an empty value."""
monkeypatch.setattr(
'qutebrowser.utils.standarddir.QStandardPaths.writableLocation',
lambda typ: '')
with pytest.raises(ValueError):
standarddir._writable_location(QStandardPaths.DataLocation)
def test_sep(self, monkeypatch):
"""Make sure the right kind of separator is used."""
monkeypatch.setattr('qutebrowser.utils.standarddir.os.sep', '\\')
loc = standarddir._writable_location(QStandardPaths.DataLocation)
assert '/' not in loc
assert '\\' in loc
@pytest.mark.skipif(not sys.platform.startswith("linux"), @pytest.mark.skipif(not sys.platform.startswith("linux"),
reason="requires Linux") reason="requires Linux")
@pytest.mark.usefixtures('no_cachedir_tag')
class TestGetStandardDirLinux: class TestGetStandardDirLinux:
"""Tests for standarddir under Linux.""" """Tests for standarddir under Linux."""
@ -53,26 +111,22 @@ class TestGetStandardDirLinux:
def test_data_explicit(self, monkeypatch, tmpdir): def test_data_explicit(self, monkeypatch, tmpdir):
"""Test data dir with XDG_DATA_HOME explicitly set.""" """Test data dir with XDG_DATA_HOME explicitly set."""
monkeypatch.setenv('XDG_DATA_HOME', str(tmpdir)) monkeypatch.setenv('XDG_DATA_HOME', str(tmpdir))
standarddir.init(None)
assert standarddir.data() == str(tmpdir / 'qutebrowser_test') assert standarddir.data() == str(tmpdir / 'qutebrowser_test')
def test_config_explicit(self, monkeypatch, tmpdir): def test_config_explicit(self, monkeypatch, tmpdir):
"""Test config dir with XDG_CONFIG_HOME explicitly set.""" """Test config dir with XDG_CONFIG_HOME explicitly set."""
monkeypatch.setenv('XDG_CONFIG_HOME', str(tmpdir)) monkeypatch.setenv('XDG_CONFIG_HOME', str(tmpdir))
standarddir.init(None)
assert standarddir.config() == str(tmpdir / 'qutebrowser_test') assert standarddir.config() == str(tmpdir / 'qutebrowser_test')
def test_cache_explicit(self, monkeypatch, tmpdir): def test_cache_explicit(self, monkeypatch, tmpdir):
"""Test cache dir with XDG_CACHE_HOME explicitly set.""" """Test cache dir with XDG_CACHE_HOME explicitly set."""
monkeypatch.setenv('XDG_CACHE_HOME', str(tmpdir)) monkeypatch.setenv('XDG_CACHE_HOME', str(tmpdir))
standarddir.init(None)
assert standarddir.cache() == str(tmpdir / 'qutebrowser_test') assert standarddir.cache() == str(tmpdir / 'qutebrowser_test')
def test_data(self, monkeypatch, tmpdir): def test_data(self, monkeypatch, tmpdir):
"""Test data dir with XDG_DATA_HOME not set.""" """Test data dir with XDG_DATA_HOME not set."""
monkeypatch.setenv('HOME', str(tmpdir)) monkeypatch.setenv('HOME', str(tmpdir))
monkeypatch.delenv('XDG_DATA_HOME', raising=False) monkeypatch.delenv('XDG_DATA_HOME', raising=False)
standarddir.init(None)
expected = tmpdir / '.local' / 'share' / 'qutebrowser_test' expected = tmpdir / '.local' / 'share' / 'qutebrowser_test'
assert standarddir.data() == str(expected) assert standarddir.data() == str(expected)
@ -80,7 +134,6 @@ class TestGetStandardDirLinux:
"""Test config dir with XDG_CONFIG_HOME not set.""" """Test config dir with XDG_CONFIG_HOME not set."""
monkeypatch.setenv('HOME', str(tmpdir)) monkeypatch.setenv('HOME', str(tmpdir))
monkeypatch.delenv('XDG_CONFIG_HOME', raising=False) monkeypatch.delenv('XDG_CONFIG_HOME', raising=False)
standarddir.init(None)
expected = tmpdir / '.config' / 'qutebrowser_test' expected = tmpdir / '.config' / 'qutebrowser_test'
assert standarddir.config() == str(expected) assert standarddir.config() == str(expected)
@ -88,21 +141,17 @@ class TestGetStandardDirLinux:
"""Test cache dir with XDG_CACHE_HOME not set.""" """Test cache dir with XDG_CACHE_HOME not set."""
monkeypatch.setenv('HOME', str(tmpdir)) monkeypatch.setenv('HOME', str(tmpdir))
monkeypatch.delenv('XDG_CACHE_HOME', raising=False) monkeypatch.delenv('XDG_CACHE_HOME', raising=False)
standarddir.init(None)
expected = tmpdir / '.cache' / 'qutebrowser_test' expected = tmpdir / '.cache' / 'qutebrowser_test'
assert standarddir.cache() == expected assert standarddir.cache() == expected
@pytest.mark.skipif(not sys.platform.startswith("win"), @pytest.mark.skipif(not sys.platform.startswith("win"),
reason="requires Windows") reason="requires Windows")
@pytest.mark.usefixtures('no_cachedir_tag')
class TestGetStandardDirWindows: class TestGetStandardDirWindows:
"""Tests for standarddir under Windows.""" """Tests for standarddir under Windows."""
@pytest.fixture(autouse=True)
def reset_standarddir(self):
standarddir.init(None)
def test_data(self): def test_data(self):
"""Test data dir.""" """Test data dir."""
expected = ['qutebrowser_test', 'data'] expected = ['qutebrowser_test', 'data']
@ -121,6 +170,7 @@ class TestGetStandardDirWindows:
DirArgTest = collections.namedtuple('DirArgTest', 'arg, expected') DirArgTest = collections.namedtuple('DirArgTest', 'arg, expected')
@pytest.mark.usefixtures('no_cachedir_tag')
class TestArguments: class TestArguments:
"""Tests with confdir/cachedir/datadir arguments.""" """Tests with confdir/cachedir/datadir arguments."""
@ -131,6 +181,7 @@ class TestArguments:
if request.param.expected is None: if request.param.expected is None:
return request.param return request.param
else: else:
# prepend tmpdir to both
arg = str(tmpdir / request.param.arg) arg = str(tmpdir / request.param.arg)
return DirArgTest(arg, arg) return DirArgTest(arg, arg)
@ -155,6 +206,21 @@ class TestArguments:
standarddir.init(args) standarddir.init(args)
assert standarddir.data() == testcase.expected assert standarddir.data() == testcase.expected
def test_confdir_none(self):
"""Test --confdir with None given."""
args = types.SimpleNamespace(confdir=None, cachedir=None, datadir=None)
standarddir.init(args)
assert standarddir.config().split(os.sep)[-1] == 'qutebrowser_test'
def test_runtimedir(self, tmpdir, monkeypatch):
"""Test runtime dir (which has no args)."""
monkeypatch.setattr(
'qutebrowser.utils.standarddir.QStandardPaths.writableLocation',
lambda _typ: str(tmpdir))
args = types.SimpleNamespace(confdir=None, cachedir=None, datadir=None)
standarddir.init(args)
assert standarddir.runtime() == str(tmpdir)
@pytest.mark.parametrize('typ', ['config', 'data', 'cache', 'download', @pytest.mark.parametrize('typ', ['config', 'data', 'cache', 'download',
'runtime']) 'runtime'])
def test_basedir(self, tmpdir, typ): def test_basedir(self, tmpdir, typ):
@ -164,3 +230,51 @@ class TestArguments:
standarddir.init(args) standarddir.init(args)
func = getattr(standarddir, typ) func = getattr(standarddir, typ)
assert func() == expected assert func() == expected
class TestInitCacheDirTag:
"""Tests for _init_cachedir_tag."""
def test_no_cache_dir(self, mocker, monkeypatch):
"""Smoke test with cache() returning None."""
monkeypatch.setattr('qutebrowser.utils.standarddir.cache',
lambda: None)
mocker.patch('builtins.open', side_effect=AssertionError)
standarddir._init_cachedir_tag()
def test_existant_cache_dir_tag(self, tmpdir, mocker, monkeypatch):
"""Test with an existant CACHEDIR.TAG."""
monkeypatch.setattr('qutebrowser.utils.standarddir.cache',
lambda: str(tmpdir))
mocker.patch('builtins.open', side_effect=AssertionError)
m = mocker.patch('qutebrowser.utils.standarddir.os.path.exists',
return_value=True)
standarddir._init_cachedir_tag()
assert not tmpdir.listdir()
m.assert_called_with(str(tmpdir / 'CACHEDIR.TAG'))
def test_new_cache_dir_tag(self, tmpdir, mocker, monkeypatch):
"""Test creating a new CACHEDIR.TAG."""
monkeypatch.setattr('qutebrowser.utils.standarddir.cache',
lambda: str(tmpdir))
standarddir._init_cachedir_tag()
assert tmpdir.listdir() == [(tmpdir / 'CACHEDIR.TAG')]
data = (tmpdir / 'CACHEDIR.TAG').read_text('utf-8')
assert data == textwrap.dedent("""
Signature: 8a477f597d28d172789f06886806bc55
# This file is a cache directory tag created by qutebrowser.
# For information about cache directory tags, see:
# http://www.brynosaurus.com/cachedir/
""").lstrip()
def test_open_oserror(self, caplog, tmpdir, mocker, monkeypatch):
"""Test creating a new CACHEDIR.TAG."""
monkeypatch.setattr('qutebrowser.utils.standarddir.cache',
lambda: str(tmpdir))
mocker.patch('builtins.open', side_effect=OSError)
with caplog.atLevel(logging.ERROR, 'misc'):
standarddir._init_cachedir_tag()
assert len(caplog.records()) == 1
assert caplog.records()[0].message == 'Failed to create CACHEDIR.TAG'
assert not tmpdir.listdir()

View File

@ -23,14 +23,22 @@ import sys
import enum import enum
import datetime import datetime
import os.path import os.path
import io
import logging
import functools
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor from PyQt5.QtGui import QColor
import pytest import pytest
import qutebrowser
import qutebrowser.utils # for test_qualname
from qutebrowser.utils import utils, qtutils from qutebrowser.utils import utils, qtutils
ELLIPSIS = '\u2026'
class Color(QColor): class Color(QColor):
"""A QColor with a nicer repr().""" """A QColor with a nicer repr()."""
@ -41,39 +49,196 @@ class Color(QColor):
alpha=self.alpha()) alpha=self.alpha())
class TestCompactText:
"""Test compact_text."""
@pytest.mark.parametrize('text, expected', [
('foo\nbar', 'foobar'),
(' foo \n bar ', 'foobar'),
('\nfoo\n', 'foo'),
])
def test_compact_text(self, text, expected):
"""Test folding of newlines."""
assert utils.compact_text(text) == expected
@pytest.mark.parametrize('elidelength, text, expected', [
(None, 'x' * 100, 'x' * 100),
(6, 'foobar', 'foobar'),
(5, 'foobar', 'foob' + ELLIPSIS),
(5, 'foo\nbar', 'foob' + ELLIPSIS),
(7, 'foo\nbar', 'foobar'),
])
def test_eliding(self, elidelength, text, expected):
"""Test eliding."""
assert utils.compact_text(text, elidelength) == expected
class TestEliding: class TestEliding:
"""Test elide.""" """Test elide."""
ELLIPSIS = '\u2026'
def test_too_small(self): def test_too_small(self):
"""Test eliding to 0 chars which should fail.""" """Test eliding to 0 chars which should fail."""
with pytest.raises(ValueError): with pytest.raises(ValueError):
utils.elide('foo', 0) utils.elide('foo', 0)
def test_length_one(self): @pytest.mark.parametrize('text, length, expected', [
"""Test eliding to 1 char which should yield ...""" ('foo', 1, ELLIPSIS),
assert utils.elide('foo', 1) == self.ELLIPSIS ('foo', 3, 'foo'),
('foobar', 3, 'fo' + ELLIPSIS),
def test_fits(self): ])
"""Test eliding with a string which fits exactly.""" def test_elided(self, text, length, expected):
assert utils.elide('foo', 3) == 'foo' assert utils.elide(text, length) == expected
def test_elided(self):
"""Test eliding with a string which should get elided."""
assert utils.elide('foobar', 3) == 'fo' + self.ELLIPSIS
class TestReadFile: class TestReadFile:
"""Test read_file.""" """Test read_file."""
@pytest.fixture(autouse=True, params=[True, False])
def freezer(self, request, monkeypatch):
if request.param and not getattr(sys, 'frozen', False):
monkeypatch.setattr(sys, 'frozen', True, raising=False)
monkeypatch.setattr('sys.executable', qutebrowser.__file__)
elif not request.param and getattr(sys, 'frozen', False):
# Want to test unfrozen tests, but we are frozen
pytest.skip("Can't run with sys.frozen = True!")
def test_readfile(self): def test_readfile(self):
"""Read a test file.""" """Read a test file."""
content = utils.read_file(os.path.join('utils', 'testfile')) content = utils.read_file(os.path.join('utils', 'testfile'))
assert content.splitlines()[0] == "Hello World!" assert content.splitlines()[0] == "Hello World!"
def test_readfile_binary(self):
"""Read a test file in binary mode."""
content = utils.read_file(os.path.join('utils', 'testfile'),
binary=True)
assert content.splitlines()[0] == b"Hello World!"
class Patcher:
"""Helper for TestActuteWarning.
Attributes:
monkeypatch: The pytest monkeypatch fixture.
"""
def __init__(self, monkeypatch):
self.monkeypatch = monkeypatch
def patch_platform(self, platform='linux'):
"""Patch sys.platform."""
self.monkeypatch.setattr('sys.platform', platform)
def patch_exists(self, exists=True):
"""Patch os.path.exists."""
self.monkeypatch.setattr('qutebrowser.utils.utils.os.path.exists',
lambda path: exists)
def patch_version(self, version='5.2.0'):
"""Patch Qt version."""
self.monkeypatch.setattr(
'qutebrowser.utils.utils.qtutils.qVersion', lambda: version)
def patch_file(self, data):
"""Patch open() to return the given data."""
fake_file = io.StringIO(data)
self.monkeypatch.setattr(utils, 'open',
lambda filename, mode, encoding: fake_file,
raising=False)
def patch_all(self, data):
"""Patch everything so the issue would exist."""
self.patch_platform()
self.patch_exists()
self.patch_version()
self.patch_file(data)
class TestActuteWarning:
"""Test actute_warning."""
@pytest.fixture
def patcher(self, monkeypatch):
"""Fixture providing a Patcher helper."""
return Patcher(monkeypatch)
def test_non_linux(self, patcher, capsys):
"""Test with a non-Linux OS."""
patcher.patch_platform('toaster')
utils.actute_warning()
out, err = capsys.readouterr()
assert not out
assert not err
def test_no_compose(self, patcher, capsys):
"""Test with no compose file."""
patcher.patch_platform()
patcher.patch_exists(False)
utils.actute_warning()
out, err = capsys.readouterr()
assert not out
assert not err
def test_newer_qt(self, patcher, capsys):
"""Test with compose file but newer Qt version."""
patcher.patch_platform()
patcher.patch_exists()
patcher.patch_version('5.4')
utils.actute_warning()
out, err = capsys.readouterr()
assert not out
assert not err
def test_no_match(self, patcher, capsys):
"""Test with compose file and affected Qt but no match."""
patcher.patch_all('foobar')
utils.actute_warning()
out, err = capsys.readouterr()
assert not out
assert not err
def test_empty(self, patcher, capsys):
"""Test with empty compose file."""
patcher.patch_all(None)
utils.actute_warning()
out, err = capsys.readouterr()
assert not out
assert not err
def test_match(self, patcher, capsys):
"""Test with compose file and affected Qt and a match."""
patcher.patch_all('foobar\n<dead_actute>\nbaz')
utils.actute_warning()
out, err = capsys.readouterr()
assert out.startswith('Note: If you got a')
assert not err
def test_match_stdout_none(self, monkeypatch, patcher, capsys):
"""Test with a match and stdout being None."""
patcher.patch_all('foobar\n<dead_actute>\nbaz')
monkeypatch.setattr('sys.stdout', None)
utils.actute_warning()
def test_unreadable(self, mocker, patcher, capsys, caplog):
"""Test with an unreadable compose file."""
patcher.patch_platform()
patcher.patch_exists()
patcher.patch_version()
mocker.patch('qutebrowser.utils.utils.open', side_effect=OSError,
create=True)
with caplog.atLevel(logging.ERROR, 'init'):
utils.actute_warning()
assert len(caplog.records()) == 1
assert caplog.records()[0].message == 'Failed to read Compose file'
out, _err = capsys.readouterr()
assert not out
class TestInterpolateColor: class TestInterpolateColor:
@ -164,62 +329,40 @@ class TestInterpolateColor:
assert Color(color) == expected assert Color(color) == expected
class TestFormatSeconds: @pytest.mark.parametrize('seconds, out', [
(-1, '-0:01'),
"""Tests for format_seconds. (0, '0:00'),
(59, '0:59'),
Class attributes: (60, '1:00'),
TESTS: A list of (input, output) tuples. (60.4, '1:00'),
""" (61, '1:01'),
(-61, '-1:01'),
TESTS = [ (3599, '59:59'),
(-1, '-0:01'), (3600, '1:00:00'),
(0, '0:00'), (3601, '1:00:01'),
(59, '0:59'), (36000, '10:00:00'),
(60, '1:00'), ])
(60.4, '1:00'), def test_format_seconds(seconds, out):
(61, '1:01'), assert utils.format_seconds(seconds) == out
(-61, '-1:01'),
(3599, '59:59'),
(3600, '1:00:00'),
(3601, '1:00:01'),
(36000, '10:00:00'),
]
@pytest.mark.parametrize('seconds, out', TESTS)
def test_format_seconds(self, seconds, out):
"""Test format_seconds with several tests."""
assert utils.format_seconds(seconds) == out
class TestFormatTimedelta: @pytest.mark.parametrize('td, out', [
(datetime.timedelta(seconds=-1), '-1s'),
"""Tests for format_timedelta. (datetime.timedelta(seconds=0), '0s'),
(datetime.timedelta(seconds=59), '59s'),
Class attributes: (datetime.timedelta(seconds=120), '2m'),
TESTS: A list of (input, output) tuples. (datetime.timedelta(seconds=60.4), '1m'),
""" (datetime.timedelta(seconds=63), '1m 3s'),
(datetime.timedelta(seconds=-64), '-1m 4s'),
TESTS = [ (datetime.timedelta(seconds=3599), '59m 59s'),
(datetime.timedelta(seconds=-1), '-1s'), (datetime.timedelta(seconds=3600), '1h'),
(datetime.timedelta(seconds=0), '0s'), (datetime.timedelta(seconds=3605), '1h 5s'),
(datetime.timedelta(seconds=59), '59s'), (datetime.timedelta(seconds=3723), '1h 2m 3s'),
(datetime.timedelta(seconds=120), '2m'), (datetime.timedelta(seconds=3780), '1h 3m'),
(datetime.timedelta(seconds=60.4), '1m'), (datetime.timedelta(seconds=36000), '10h'),
(datetime.timedelta(seconds=63), '1m 3s'), ])
(datetime.timedelta(seconds=-64), '-1m 4s'), def test_format_timedelta(td, out):
(datetime.timedelta(seconds=3599), '59m 59s'), assert utils.format_timedelta(td) == out
(datetime.timedelta(seconds=3600), '1h'),
(datetime.timedelta(seconds=3605), '1h 5s'),
(datetime.timedelta(seconds=3723), '1h 2m 3s'),
(datetime.timedelta(seconds=3780), '1h 3m'),
(datetime.timedelta(seconds=36000), '10h'),
]
@pytest.mark.parametrize('td, out', TESTS)
def test_format_seconds(self, td, out):
"""Test format_seconds with several tests."""
assert utils.format_timedelta(td) == out
class TestFormatSize: class TestFormatSize:
@ -264,29 +407,34 @@ class TestKeyToString:
"""Test key_to_string.""" """Test key_to_string."""
def test_unicode_garbage_keys(self): @pytest.mark.parametrize('key, expected', [
(Qt.Key_Blue, 'Blue'),
(Qt.Key_Backtab, 'Tab'),
(Qt.Key_Escape, 'Escape'),
(Qt.Key_A, 'A'),
(Qt.Key_degree, '°'),
(Qt.Key_Meta, 'Meta'),
])
def test_normal(self, key, expected):
"""Test a special key where QKeyEvent::toString works incorrectly.""" """Test a special key where QKeyEvent::toString works incorrectly."""
assert utils.key_to_string(Qt.Key_Blue) == 'Blue' assert utils.key_to_string(key) == expected
def test_backtab(self): def test_missing(self, monkeypatch):
"""Test if backtab is normalized to tab correctly.""" """Test with a missing key."""
assert utils.key_to_string(Qt.Key_Backtab) == 'Tab' monkeypatch.delattr('qutebrowser.utils.utils.Qt.Key_Blue')
# We don't want to test the key which is actually missing - we only
def test_escape(self): # want to know if the mapping still behaves properly.
"""Test if escape is normalized to escape correctly."""
assert utils.key_to_string(Qt.Key_Escape) == 'Escape'
def test_letter(self):
"""Test a simple letter key."""
assert utils.key_to_string(Qt.Key_A) == 'A' assert utils.key_to_string(Qt.Key_A) == 'A'
def test_unicode(self): def test_all(self):
"""Test a printable unicode key.""" """Make sure there's some sensible output for all keys."""
assert utils.key_to_string(Qt.Key_degree) == '°' for name, value in sorted(vars(Qt).items()):
if not isinstance(value, Qt.Key):
def test_special(self): continue
"""Test a non-printable key handled by QKeyEvent::toString.""" print(name)
assert utils.key_to_string(Qt.Key_F1) == 'F1' string = utils.key_to_string(value)
assert string
string.encode('utf-8') # make sure it's encodable
class TestKeyEventToString: class TestKeyEventToString:
@ -323,26 +471,275 @@ class TestKeyEventToString:
Qt.MetaModifier | Qt.ShiftModifier)) Qt.MetaModifier | Qt.ShiftModifier))
assert utils.keyevent_to_string(evt) == 'Ctrl+Alt+Meta+Shift+A' assert utils.keyevent_to_string(evt) == 'Ctrl+Alt+Meta+Shift+A'
def test_mac(self, monkeypatch, fake_keyevent_factory):
"""Test with a simulated mac."""
monkeypatch.setattr('sys.platform', 'darwin')
evt = fake_keyevent_factory(key=Qt.Key_A, modifiers=Qt.ControlModifier)
assert utils.keyevent_to_string(evt) == 'Meta+A'
class TestNormalize:
"""Test normalize_keystr.""" @pytest.mark.parametrize('orig, repl', [
('Control+x', 'ctrl+x'),
('Windows+x', 'meta+x'),
('Mod1+x', 'alt+x'),
('Mod4+x', 'meta+x'),
('Control--', 'ctrl+-'),
('Windows++', 'meta++'),
('ctrl-x', 'ctrl+x'),
('control+x', 'ctrl+x')
])
def test_normalize_keystr(orig, repl):
assert utils.normalize_keystr(orig) == repl
STRINGS = (
('Control+x', 'ctrl+x'),
('Windows+x', 'meta+x'),
('Mod1+x', 'alt+x'),
('Mod4+x', 'meta+x'),
('Control--', 'ctrl+-'),
('Windows++', 'meta++'),
('ctrl-x', 'ctrl+x'),
('control+x', 'ctrl+x')
)
@pytest.mark.parametrize('orig, repl', STRINGS) class TestFakeIOStream:
def test_normalize(self, orig, repl):
"""Test normalize with some strings.""" """Test FakeIOStream."""
assert utils.normalize_keystr(orig) == repl
def _write_func(self, text):
return text
def test_flush(self):
"""Smoke-test to see if flushing works."""
s = utils.FakeIOStream(self._write_func)
s.flush()
def test_isatty(self):
"""Make sure isatty() is always false."""
s = utils.FakeIOStream(self._write_func)
assert not s.isatty()
def test_write(self):
"""Make sure writing works."""
s = utils.FakeIOStream(self._write_func)
assert s.write('echo') == 'echo'
class TestFakeIO:
"""Test FakeIO."""
@pytest.yield_fixture(autouse=True)
def restore_streams(self):
"""Restore sys.stderr/sys.stdout after tests."""
old_stdout = sys.stdout
old_stderr = sys.stderr
yield
sys.stdout = old_stdout
sys.stderr = old_stderr
def test_normal(self, capsys):
"""Test without changing sys.stderr/sys.stdout."""
data = io.StringIO()
with utils.fake_io(data.write):
sys.stdout.write('hello\n')
sys.stderr.write('world\n')
out, err = capsys.readouterr()
assert not out
assert not err
assert data.getvalue() == 'hello\nworld\n'
sys.stdout.write('back to\n')
sys.stderr.write('normal\n')
out, err = capsys.readouterr()
assert out == 'back to\n'
assert err == 'normal\n'
def test_stdout_replaced(self, capsys):
"""Test with replaced stdout."""
data = io.StringIO()
new_stdout = io.StringIO()
with utils.fake_io(data.write):
sys.stdout.write('hello\n')
sys.stderr.write('world\n')
sys.stdout = new_stdout
out, err = capsys.readouterr()
assert not out
assert not err
assert data.getvalue() == 'hello\nworld\n'
sys.stdout.write('still new\n')
sys.stderr.write('normal\n')
out, err = capsys.readouterr()
assert not out
assert err == 'normal\n'
assert new_stdout.getvalue() == 'still new\n'
def test_stderr_replaced(self, capsys):
"""Test with replaced stderr."""
data = io.StringIO()
new_stderr = io.StringIO()
with utils.fake_io(data.write):
sys.stdout.write('hello\n')
sys.stderr.write('world\n')
sys.stderr = new_stderr
out, err = capsys.readouterr()
assert not out
assert not err
assert data.getvalue() == 'hello\nworld\n'
sys.stdout.write('normal\n')
sys.stderr.write('still new\n')
out, err = capsys.readouterr()
assert out == 'normal\n'
assert not err
assert new_stderr.getvalue() == 'still new\n'
class GotException(Exception):
"""Exception used for TestDisabledExcepthook."""
pass
def excepthook(_exc, _val, _tb):
return
def excepthook_2(_exc, _val, _tb):
return
class TestDisabledExcepthook:
"""Test disabled_excepthook.
This doesn't test much as some things are untestable without triggering
the excepthook (which is hard to test).
"""
@pytest.yield_fixture(autouse=True)
def restore_excepthook(self):
"""Restore sys.excepthook and sys.__excepthook__ after tests."""
old_excepthook = sys.excepthook
old_dunder_excepthook = sys.__excepthook__
yield
sys.excepthook = old_excepthook
sys.__excepthook__ = old_dunder_excepthook
def test_normal(self):
"""Test without changing sys.excepthook."""
sys.excepthook = excepthook
assert sys.excepthook is excepthook
with utils.disabled_excepthook():
assert sys.excepthook is not excepthook
assert sys.excepthook is excepthook
def test_changed(self):
"""Test with changed sys.excepthook."""
sys.excepthook = excepthook
with utils.disabled_excepthook():
assert sys.excepthook is not excepthook
sys.excepthook = excepthook_2
assert sys.excepthook is excepthook_2
class TestPreventExceptions:
"""Test prevent_exceptions."""
@utils.prevent_exceptions(42)
def func_raising(self):
raise Exception
def test_raising(self, caplog):
"""Test with a raising function."""
with caplog.atLevel(logging.ERROR, 'misc'):
ret = self.func_raising()
assert ret == 42
assert len(caplog.records()) == 1
expected = 'Error in test_utils.TestPreventExceptions.func_raising'
actual = caplog.records()[0].message
assert actual == expected
@utils.prevent_exceptions(42)
def func_not_raising(self):
return 23
def test_not_raising(self, caplog):
"""Test with a non-raising function."""
with caplog.atLevel(logging.ERROR, 'misc'):
ret = self.func_not_raising()
assert ret == 23
assert not caplog.records()
@utils.prevent_exceptions(42, True)
def func_predicate_true(self):
raise Exception
def test_predicate_true(self, caplog):
"""Test with a True predicate."""
with caplog.atLevel(logging.ERROR, 'misc'):
ret = self.func_predicate_true()
assert ret == 42
assert len(caplog.records()) == 1
@utils.prevent_exceptions(42, False)
def func_predicate_false(self):
raise Exception
def test_predicate_false(self, caplog):
"""Test with a False predicate."""
with caplog.atLevel(logging.ERROR, 'misc'):
with pytest.raises(Exception):
self.func_predicate_false()
assert not caplog.records()
class Obj:
"""Test object for test_get_repr()."""
pass
@pytest.mark.parametrize('constructor, attrs, expected', [
(False, {}, '<test_utils.Obj>'),
(False, {'foo': None}, '<test_utils.Obj foo=None>'),
(False, {'foo': "b'ar", 'baz': 2}, '<test_utils.Obj baz=2 foo="b\'ar">'),
(True, {}, 'test_utils.Obj()'),
(True, {'foo': None}, 'test_utils.Obj(foo=None)'),
(True, {'foo': "te'st", 'bar': 2}, 'test_utils.Obj(bar=2, foo="te\'st")'),
])
def test_get_repr(constructor, attrs, expected):
"""Test get_repr()."""
assert utils.get_repr(Obj(), constructor, **attrs) == expected
class QualnameObj():
"""Test object for test_qualname."""
def func(self):
"""Test method for test_qualname."""
pass
def qualname_func(_blah):
"""Test function for test_qualname."""
pass
QUALNAME_OBJ = QualnameObj()
@pytest.mark.parametrize('obj, expected', [
(QUALNAME_OBJ, repr(QUALNAME_OBJ)), # instance - unknown
(QualnameObj, 'test_utils.QualnameObj'), # class
(QualnameObj.func, 'test_utils.QualnameObj.func'), # unbound method
(QualnameObj().func, 'test_utils.QualnameObj.func'), # bound method
(qualname_func, 'test_utils.qualname_func'), # function
(functools.partial(qualname_func, True), 'test_utils.qualname_func'),
(qutebrowser, 'qutebrowser'), # module
(qutebrowser.utils, 'qutebrowser.utils'), # submodule
(utils, 'qutebrowser.utils.utils'), # submodule (from-import)
])
def test_qualname(obj, expected):
assert utils.qualname(obj) == expected
class TestIsEnum: class TestIsEnum:
@ -412,20 +809,13 @@ class TestRaises:
utils.raises(ValueError, self.do_raise) utils.raises(ValueError, self.do_raise)
class TestForceEncoding: @pytest.mark.parametrize('inp, enc, expected', [
('hello world', 'ascii', 'hello world'),
"""Test force_encoding.""" ('hellö wörld', 'utf-8', 'hellö wörld'),
('hellö wörld', 'ascii', 'hell? w?rld'),
TESTS = [ ])
('hello world', 'ascii', 'hello world'), def test_force_encoding(inp, enc, expected):
('hellö wörld', 'utf-8', 'hellö wörld'), assert utils.force_encoding(inp, enc) == expected
('hellö wörld', 'ascii', 'hell? w?rld'),
]
@pytest.mark.parametrize('inp, enc, expected', TESTS)
def test_fitting_ascii(self, inp, enc, expected):
"""Test force_encoding will yield expected text."""
assert utils.force_encoding(inp, enc) == expected
class TestNewestSlice: class TestNewestSlice:
@ -437,44 +827,23 @@ class TestNewestSlice:
with pytest.raises(ValueError): with pytest.raises(ValueError):
utils.newest_slice([], -2) utils.newest_slice([], -2)
def test_count_minus_one(self): @pytest.mark.parametrize('items, count, expected', [
"""Test with a count of -1 (all elements).""" # Count of -1 (all elements).
items = range(20) (range(20), -1, range(20)),
sliced = utils.newest_slice(items, -1) # Count of 0 (no elements).
assert list(sliced) == list(items) (range(20), 0, []),
# Count which is much smaller than the iterable.
def test_count_zero(self): (range(20), 5, [15, 16, 17, 18, 19]),
"""Test with a count of 0 (no elements).""" # Count which is exactly one smaller."""
items = range(20) (range(5), 4, [1, 2, 3, 4]),
sliced = utils.newest_slice(items, 0) # Count which is just as large as the iterable."""
assert list(sliced) == [] (range(5), 5, range(5)),
# Count which is one bigger than the iterable.
def test_count_much_smaller(self): (range(5), 6, range(5)),
"""Test with a count which is much smaller than the iterable.""" # Count which is much bigger than the iterable.
items = range(20) (range(5), 50, range(5)),
sliced = utils.newest_slice(items, 5) ])
assert list(sliced) == [15, 16, 17, 18, 19] def test_good(self, items, count, expected):
"""Test slices which shouldn't raise an exception."""
def test_count_smaller(self): sliced = utils.newest_slice(items, count)
"""Test with a count which is exactly one smaller.""" assert list(sliced) == list(expected)
items = range(5)
sliced = utils.newest_slice(items, 4)
assert list(sliced) == [1, 2, 3, 4]
def test_count_equal(self):
"""Test with a count which is just as large as the iterable."""
items = range(5)
sliced = utils.newest_slice(items, 5)
assert list(sliced) == list(items)
def test_count_bigger(self):
"""Test with a count which is one bigger than the iterable."""
items = range(5)
sliced = utils.newest_slice(items, 6)
assert list(sliced) == list(items)
def test_count_much_bigger(self):
"""Test with a count which is much bigger than the iterable."""
items = range(5)
sliced = utils.newest_slice(items, 50)
assert list(sliced) == list(items)

628
tests/utils/test_version.py Normal file
View File

@ -0,0 +1,628 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015 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=protected-access
"""Tests for qutebrowser.utils.version."""
import io
import sys
import os.path
import subprocess
import contextlib
import builtins
import types
import importlib
import logging
import textwrap
import pytest
import qutebrowser
from qutebrowser.utils import version
class GitStrSubprocessFake:
"""Object returned by the git_str_subprocess_fake fixture.
This provides a function which is used to patch _git_str_subprocess.
Attributes:
retval: The value to return when called. Needs to be set before func is
called.
"""
UNSET = object()
def __init__(self):
self.retval = self.UNSET
def func(self, gitpath):
"""Function called instead of _git_str_subprocess.
Checks whether the path passed is what we expected, and returns
self.retval.
"""
if self.retval is self.UNSET:
raise ValueError("func got called without retval being set!")
retval = self.retval
self.retval = self.UNSET
gitpath = os.path.normpath(gitpath)
expected = os.path.abspath(os.path.join(
os.path.dirname(qutebrowser.__file__), os.pardir))
assert gitpath == expected
return retval
class TestGitStr:
"""Tests for _git_str()."""
@pytest.yield_fixture
def commit_file_mock(self, mocker):
"""Fixture providing a mock for utils.read_file for git-commit-id.
On fixture teardown, it makes sure it got called with git-commit-id as
argument.
"""
mocker.patch('qutebrowser.utils.version.subprocess',
side_effect=AssertionError)
m = mocker.patch('qutebrowser.utils.version.utils.read_file')
yield m
m.assert_called_with('git-commit-id')
@pytest.fixture
def git_str_subprocess_fake(self, mocker, monkeypatch):
"""Fixture patching _git_str_subprocess with a GitStrSubprocessFake."""
mocker.patch('qutebrowser.utils.version.subprocess',
side_effect=AssertionError)
fake = GitStrSubprocessFake()
monkeypatch.setattr('qutebrowser.utils.version._git_str_subprocess',
fake.func)
return fake
def test_frozen_ok(self, commit_file_mock, monkeypatch):
"""Test with sys.frozen=True and a successful git-commit-id read."""
monkeypatch.setattr(qutebrowser.utils.version.sys, 'frozen', True,
raising=False)
commit_file_mock.return_value = 'deadbeef'
assert version._git_str() == 'deadbeef'
def test_frozen_oserror(self, commit_file_mock, monkeypatch):
"""Test with sys.frozen=True and OSError when reading git-commit-id."""
monkeypatch.setattr(qutebrowser.utils.version.sys, 'frozen', True,
raising=False)
commit_file_mock.side_effect = OSError
assert version._git_str() is None
@pytest.mark.skipif(getattr(sys, 'frozen', False),
reason="Can't be executed when frozen!")
def test_normal_successful(self, git_str_subprocess_fake):
"""Test with git returning a successful result."""
git_str_subprocess_fake.retval = 'c0ffeebabe'
assert version._git_str() == 'c0ffeebabe'
@pytest.mark.skipif(not getattr(sys, 'frozen', False),
reason="Can only executed when frozen!")
def test_normal_successful_frozen(self, git_str_subprocess_fake):
"""Test with git returning a successful result."""
# The value is defined in scripts/freeze_tests.py.
assert version._git_str() == 'fake-frozen-git-commit'
def test_normal_error(self, commit_file_mock, git_str_subprocess_fake):
"""Test without repo (but git-commit-id)."""
git_str_subprocess_fake.retval = None
commit_file_mock.return_value = '1b4d1dea'
assert version._git_str() == '1b4d1dea'
def test_normal_path_oserror(self, mocker, git_str_subprocess_fake):
"""Test with things raising OSError."""
mocker.patch('qutebrowser.utils.version.os.path.join',
side_effect=OSError)
mocker.patch('qutebrowser.utils.version.utils.read_file',
side_effect=OSError)
assert version._git_str() is None
@pytest.mark.skipif(getattr(sys, 'frozen', False),
reason="Can't be executed when frozen!")
def test_normal_path_nofile(self, monkeypatch, caplog,
git_str_subprocess_fake, commit_file_mock):
"""Test with undefined __file__ but available git-commit-id."""
monkeypatch.delattr('qutebrowser.utils.version.__file__')
commit_file_mock.return_value = '0deadcode'
with caplog.atLevel(logging.ERROR, 'misc'):
assert version._git_str() == '0deadcode'
assert len(caplog.records()) == 1
assert caplog.records()[0].message == "Error while getting git path"
def _has_git():
"""Check if git is installed."""
try:
subprocess.check_call(['git', '--version'], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
except (OSError, subprocess.CalledProcessError):
return False
else:
return True
# Decorator for tests needing git, so they get skipped when it's unavailable.
needs_git = pytest.mark.skipif(not _has_git(), reason='Needs git installed.')
class TestGitStrSubprocess:
"""Tests for _git_str_subprocess."""
@pytest.fixture
def git_repo(self, tmpdir):
"""A fixture to create a temporary git repo.
Some things are tested against a real repo so we notice if something in
git would change, or we call git incorrectly.
"""
def _git(*args):
"""Helper closure to call git."""
env = {
'GIT_AUTHOR_NAME': 'qutebrowser testsuite',
'GIT_AUTHOR_EMAIL': 'mail@qutebrowser.org',
'GIT_AUTHOR_DATE': 'Thu 1 Jan 01:00:00 CET 1970',
'GIT_COMMITTER_NAME': 'qutebrowser testsuite',
'GIT_COMMITTER_EMAIL': 'mail@qutebrowser.org',
'GIT_COMMITTER_DATE': 'Thu 1 Jan 01:00:00 CET 1970',
}
subprocess.check_call(['git', '-C', str(tmpdir)] + list(args),
env=env)
(tmpdir / 'file').write_text("Hello World!", encoding='utf-8')
_git('init')
_git('add', 'file')
_git('commit', '-am', 'foo', '--no-verify', '--no-edit',
'--no-post-rewrite', '--quiet', '--no-gpg-sign')
_git('tag', 'foobar')
return tmpdir
@needs_git
def test_real_git(self, git_repo):
"""Test with a real git repository."""
ret = version._git_str_subprocess(str(git_repo))
assert ret == 'foobar (1970-01-01 01:00:00 +0100)'
def test_missing_dir(self, tmpdir):
"""Test with a directory which doesn't exist."""
ret = version._git_str_subprocess(str(tmpdir / 'does-not-exist'))
assert ret is None
@pytest.mark.parametrize('exc', [
OSError,
subprocess.CalledProcessError(1, 'foobar')
])
def test_exception(self, exc, mocker, tmpdir):
"""Test with subprocess.check_output raising an exception.
Args:
exc: The exception to raise.
"""
mocker.patch('qutebrowser.utils.version.subprocess.os.path.isdir',
return_value=True)
mocker.patch('qutebrowser.utils.version.subprocess.check_output',
side_effect=exc)
ret = version._git_str_subprocess(str(tmpdir))
assert ret is None
class ReleaseInfoFake:
"""An object providing fakes for glob.glob/open for test_release_info.
Attributes:
_files: The files which should be returned, or None if an exception
should be raised. A {filename: [lines]} dict.
"""
def __init__(self, files):
self._files = files
def glob_fake(self, pattern):
"""Fake for glob.glob.
Verifies the arguments and returns the files listed in self._files, or
a single fake file if an exception is expected.
"""
assert pattern == '/etc/*-release'
if self._files is None:
return ['fake-file']
else:
return sorted(list(self._files))
@contextlib.contextmanager
def open_fake(self, filename, mode, encoding):
"""Fake for open().
Verifies the arguments and returns a StringIO with the content listed
in self._files.
"""
assert mode == 'r'
assert encoding == 'utf-8'
if self._files is None:
raise OSError
yield io.StringIO(''.join(self._files[filename]))
@pytest.mark.parametrize('files, expected', [
({}, []),
({'file': ['']}, [('file', '')]),
({'file': []}, [('file', '')]),
(
{'file1': ['foo\n', 'bar\n'], 'file2': ['baz\n']},
[('file1', 'foo\nbar\n'), ('file2', 'baz\n')]
),
(None, []),
])
def test_release_info(files, expected, caplog, monkeypatch):
"""Test _release_info().
Args:
files: The file dict passed to ReleaseInfoFake.
expected: The expected _release_info output.
"""
fake = ReleaseInfoFake(files)
monkeypatch.setattr('qutebrowser.utils.version.glob.glob', fake.glob_fake)
monkeypatch.setattr(version, 'open', fake.open_fake, raising=False)
with caplog.atLevel(logging.ERROR, 'misc'):
assert version._release_info() == expected
if files is None:
assert len(caplog.records()) == 1
assert caplog.records()[0].message == "Error while reading fake-file."
class ImportFake:
"""A fake for __import__ which is used by the import_fake fixture.
Attributes:
exists: A dict mapping module names to bools. If True, the import will
success. Otherwise, it'll fail with ImportError.
version_attribute: The name to use in the fake modules for the version
attribute.
version: The version to use for the modules.
_real_import: Saving the real __import__ builtin so the imports can be
done normally for modules not in self.exists.
"""
def __init__(self):
self.exists = {
'sip': True,
'colorlog': True,
'colorama': True,
'pypeg2': True,
'jinja2': True,
'pygments': True,
'yaml': True,
}
self.version_attribute = '__version__'
self.version = '1.2.3'
self._real_import = builtins.__import__
def _do_import(self, name):
"""Helper for fake_import and fake_importlib_import to do the work.
Return:
The imported fake module, or None if normal importing should be
used.
"""
if name not in self.exists:
# Not one of the modules to test -> use real import
return None
elif self.exists[name]:
ns = types.SimpleNamespace()
if self.version_attribute is not None:
setattr(ns, self.version_attribute, self.version)
return ns
else:
raise ImportError("Fake ImportError for {}.".format(name))
def fake_import(self, name, *args, **kwargs):
"""Fake for the builtin __import__."""
module = self._do_import(name)
if module is not None:
return module
else:
return self._real_import(name, *args, **kwargs)
def fake_importlib_import(self, name):
"""Fake for importlib.import_module."""
module = self._do_import(name)
if module is not None:
return module
else:
return importlib.import_module(name)
@pytest.fixture
def import_fake(monkeypatch):
"""Fixture to patch imports using ImportFake."""
fake = ImportFake()
monkeypatch.setattr('builtins.__import__', fake.fake_import)
monkeypatch.setattr('qutebrowser.utils.version.importlib.import_module',
fake.fake_importlib_import)
return fake
class TestModuleVersions:
"""Tests for _module_versions()."""
@pytest.mark.usefixtures('import_fake')
def test_all_present(self):
"""Test with all modules present in version 1.2.3."""
expected = ['sip: yes', 'colorlog: yes', 'colorama: 1.2.3',
'pypeg2: 1.2.3', 'jinja2: 1.2.3', 'pygments: 1.2.3',
'yaml: 1.2.3']
assert version._module_versions() == expected
@pytest.mark.parametrize('module, idx, expected', [
('colorlog', 1, 'colorlog: no'),
('colorama', 2, 'colorama: no'),
])
def test_missing_module(self, module, idx, expected, import_fake):
"""Test with a module missing.
Args:
module: The name of the missing module.
idx: The index where the given text is expected.
expected: The expected text.
"""
import_fake.exists[module] = False
assert version._module_versions()[idx] == expected
@pytest.mark.parametrize('value, expected', [
('VERSION', ['sip: yes', 'colorlog: yes', 'colorama: 1.2.3',
'pypeg2: yes', 'jinja2: yes', 'pygments: yes',
'yaml: yes']),
('SIP_VERSION_STR', ['sip: 1.2.3', 'colorlog: yes', 'colorama: yes',
'pypeg2: yes', 'jinja2: yes', 'pygments: yes',
'yaml: yes']),
(None, ['sip: yes', 'colorlog: yes', 'colorama: yes', 'pypeg2: yes',
'jinja2: yes', 'pygments: yes', 'yaml: yes']),
])
def test_version_attribute(self, value, expected, import_fake):
"""Test with a different version attribute.
VERSION is tested for old colorama versions, and None to make sure
things still work if some package suddenly doesn't have __version__.
Args:
value: The name of the version attribute.
expected: The expected return value.
"""
import_fake.version_attribute = value
assert version._module_versions() == expected
@pytest.mark.parametrize('name, has_version', [
('colorlog', False),
('sip', False),
('colorama', True),
('pypeg2', True),
('jinja2', True),
('pygments', True),
('yaml', True),
])
def test_existing_attributes(self, name, has_version):
"""Check if all dependencies have an expected __version__ attribute.
The aim of this test is to fail if modules suddenly don't have a
__version__ attribute anymore in a newer version, or colorlog has one.
Args:
name: The name of the module to check.
has_version: Whether a __version__ attribute is expected.
"""
module = importlib.import_module(name)
assert hasattr(module, '__version__') == has_version
def test_existing_sip_attribute(self):
"""Test if sip has a SIP_VERSION_STR attribute.
The aim of this test is to fail if that gets missing in some future
version of sip.
"""
import sip
assert isinstance(sip.SIP_VERSION_STR, str)
class TestOsInfo:
"""Tests for _os_info."""
@pytest.mark.parametrize('dist, dist_str', [
(('x', '', 'y'), 'x, y'),
(('a', 'b', 'c'), 'a, b, c'),
(('', '', ''), ''),
])
def test_linux_fake(self, monkeypatch, dist, dist_str):
"""Test with a fake Linux.
Args:
dist: The value to set platform.dist() to.
dist_str: The expected distribution string in version._os_info().
"""
monkeypatch.setattr('qutebrowser.utils.version.sys.platform', 'linux')
monkeypatch.setattr('qutebrowser.utils.version._release_info',
lambda: [('releaseinfo', 'Hello World')])
monkeypatch.setattr('qutebrowser.utils.version.platform.dist',
lambda: dist)
ret = version._os_info()
expected = ['OS Version: {}'.format(dist_str), '',
'--- releaseinfo ---', 'Hello World']
assert ret == expected
def test_windows_fake(self, monkeypatch):
"""Test with a fake Windows."""
monkeypatch.setattr('qutebrowser.utils.version.sys.platform', 'win32')
monkeypatch.setattr('qutebrowser.utils.version.platform.win32_ver',
lambda: ('eggs', 'bacon', 'ham', 'spam'))
ret = version._os_info()
expected = ['OS Version: eggs, bacon, ham, spam']
assert ret == expected
@pytest.mark.parametrize('mac_ver, mac_ver_str', [
(('x', ('', '', ''), 'y'), 'x, y'),
(('', ('', '', ''), ''), ''),
(('x', ('1', '2', '3'), 'y'), 'x, 1.2.3, y'),
])
def test_os_x_fake(self, monkeypatch, mac_ver, mac_ver_str):
"""Test with a fake OS X.
Args:
mac_ver: The tuple to set platform.mac_ver() to.
mac_ver_str: The expected Mac version string in version._os_info().
"""
monkeypatch.setattr('qutebrowser.utils.version.sys.platform', 'darwin')
monkeypatch.setattr('qutebrowser.utils.version.platform.mac_ver',
lambda: mac_ver)
ret = version._os_info()
expected = ['OS Version: {}'.format(mac_ver_str)]
assert ret == expected
def test_unknown_fake(self, monkeypatch):
"""Test with a fake unknown sys.platform."""
monkeypatch.setattr('qutebrowser.utils.version.sys.platform',
'toaster')
ret = version._os_info()
expected = ['OS Version: ?']
assert ret == expected
@pytest.mark.skipif(sys.platform != 'linux', reason="requires Linux")
def test_linux_real(self):
"""Make sure there are no exceptions with a real Linux."""
version._os_info()
@pytest.mark.skipif(sys.platform != 'win32', reason="requires Windows")
def test_windows_real(self):
"""Make sure there are no exceptions with a real Windows."""
version._os_info()
@pytest.mark.skipif(sys.platform != 'darwin', reason="requires OS X")
def test_os_x_real(self):
"""Make sure there are no exceptions with a real OS X."""
version._os_info()
class FakeQSslSocket:
"""Fake for the QSslSocket Qt class.
Attributes:
_version: What QSslSocket::sslLibraryVersionString() should return.
"""
def __init__(self, version=None):
self._version = version
def supportsSsl(self):
"""Fake for QSslSocket::supportsSsl()."""
return True
def sslLibraryVersionString(self):
"""Fake for QSslSocket::sslLibraryVersionString()."""
if self._version is None:
raise AssertionError("Got valled with version None!")
return self._version
@pytest.mark.parametrize('git_commit, harfbuzz, frozen, short', [
(True, True, False, False), # normal
(False, True, False, False), # no git commit
(True, False, False, False), # HARFBUZZ unset
(True, True, True, False), # frozen
(True, True, False, True), # short
(False, True, False, True), # short and no git commit
])
def test_version_output(git_commit, harfbuzz, frozen, short, stubs,
monkeypatch):
"""Test version.version()."""
patches = {
'qutebrowser.__version__': 'VERSION',
'_git_str': lambda: ('GIT COMMIT' if git_commit else None),
'platform.python_implementation': lambda: 'PYTHON IMPLEMENTATION',
'platform.python_version': lambda: 'PYTHON VERSION',
'QT_VERSION_STR': 'QT VERSION',
'qVersion': lambda: 'QT RUNTIME VERSION',
'PYQT_VERSION_STR': 'PYQT VERSION',
'_module_versions': lambda: ['MODULE VERSION 1', 'MODULE VERSION 2'],
'qWebKitVersion': lambda: 'WEBKIT VERSION',
'QSslSocket': FakeQSslSocket('SSL VERSION'),
'platform.platform': lambda: 'PLATFORM',
'platform.architecture': lambda: ('ARCHITECTURE', ''),
'_os_info': lambda: ['OS INFO 1', 'OS INFO 2'],
'QApplication': stubs.FakeQApplication(style='STYLE'),
}
for attr, val in patches.items():
monkeypatch.setattr('qutebrowser.utils.version.' + attr, val)
monkeypatch.setenv('DESKTOP_SESSION', 'DESKTOP')
if harfbuzz:
monkeypatch.setenv('QT_HARFBUZZ', 'HARFBUZZ')
else:
monkeypatch.delenv('QT_HARFBUZZ', raising=False)
if frozen:
monkeypatch.setattr(sys, 'frozen', True, raising=False)
else:
monkeypatch.delattr(sys, 'frozen', raising=False)
template = textwrap.dedent("""
qutebrowser vVERSION
{git_commit}
PYTHON IMPLEMENTATION: PYTHON VERSION
Qt: QT VERSION, runtime: QT RUNTIME VERSION
PyQt: PYQT VERSION
""".lstrip('\n'))
if git_commit:
substitutions = {'git_commit': 'Git commit: GIT COMMIT\n'}
else:
substitutions = {'git_commit': ''}
if not short:
template += textwrap.dedent("""
Style: STYLE
Desktop: DESKTOP
MODULE VERSION 1
MODULE VERSION 2
Webkit: WEBKIT VERSION
Harfbuzz: {harfbuzz}
SSL: SSL VERSION
Frozen: {frozen}
Platform: PLATFORM, ARCHITECTURE
OS INFO 1
OS INFO 2
""".lstrip('\n'))
substitutions['harfbuzz'] = 'HARFBUZZ' if harfbuzz else 'system'
substitutions['frozen'] = str(frozen)
expected = template.rstrip('\n').format(**substitutions)
assert version.version(short=short) == expected

View File

@ -54,3 +54,9 @@ def test_start():
e = usertypes.enum('Enum', ['three', 'four'], start=3) e = usertypes.enum('Enum', ['three', 'four'], start=3)
assert e.three.value == 3 assert e.three.value == 3
assert e.four.value == 4 assert e.four.value == 4
def test_exit():
"""Make sure the exit status enum is correct."""
assert usertypes.Exit.ok == 0
assert usertypes.Exit.reserved == 1

112
tox.ini
View File

@ -4,9 +4,10 @@
# and then run "tox" from this directory. # and then run "tox" from this directory.
[tox] [tox]
envlist = unittests,misc,pep257,pyflakes,pep8,mccabe,pylint,pyroma,check-manifest envlist = smoke,unittests,misc,pep257,pyflakes,pep8,mccabe,pylint,pyroma,check-manifest
[testenv] [testenv]
passenv = PYTHON
basepython = python3 basepython = python3
[testenv:mkvenv] [testenv:mkvenv]
@ -17,37 +18,47 @@ usedevelop = true
[testenv:unittests] [testenv:unittests]
# https://bitbucket.org/hpk42/tox/issue/246/ - only needed for Windows though # https://bitbucket.org/hpk42/tox/issue/246/ - only needed for Windows though
setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/platforms setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/platforms
passenv = DISPLAY XAUTHORITY HOME passenv = PYTHON DISPLAY XAUTHORITY HOME
deps = deps =
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
py==1.4.27 py==1.4.30
pytest==2.7.1 pytest==2.7.2
pytest-capturelog==0.7 pytest-capturelog==0.7
pytest-qt==1.3.0 pytest-qt==1.4.0
pytest-mock==0.5 pytest-mock==0.6.0
pytest-html==1.3.1 pytest-html==1.3.1
# We don't use {[testenv:mkvenv]commands} here because that seems to be broken
# on Ubuntu Trusty.
commands = commands =
{envpython} scripts/link_pyqt.py --tox {envdir} {envpython} scripts/link_pyqt.py --tox {envdir}
{envpython} -m py.test --strict -rfEsw {posargs} {envpython} -m py.test --strict -rfEsw {posargs}
[testenv:unittests-frozen]
setenv = {[testenv:unittests]setenv}
passenv = {[testenv:unittests]passenv}
skip_install = true
deps =
{[testenv:unittests]deps}
cx_Freeze==4.3.4
commands =
{envpython} scripts/link_pyqt.py --tox {envdir}
{envpython} scripts/freeze_tests.py build_exe -b {envdir}/build
{envdir}/build/run-frozen-tests --strict -rfEsw {posargs}
[testenv:coverage] [testenv:coverage]
passenv = DISPLAY XAUTHORITY HOME passenv = PYTHON DISPLAY XAUTHORITY HOME
deps = deps =
{[testenv:unittests]deps} {[testenv:unittests]deps}
coverage==3.7.1 coverage==3.7.1
pytest-cov==1.8.1 pytest-cov==1.8.1
cov-core==1.15.0 cov-core==1.15.0
commands = commands =
{[testenv:mkvenv]commands} {envpython} scripts/link_pyqt.py --tox {envdir}
{envpython} -m py.test --strict -rfEswx -v --cov qutebrowser --cov-report term --cov-report html {posargs} {envpython} -m py.test --strict -rfEswx -v --cov qutebrowser --cov-report term --cov-report html {posargs}
[testenv:misc] [testenv:misc]
commands = commands =
{envpython} scripts/misc_checks.py git {envpython} scripts/misc_checks.py git
{envpython} scripts/misc_checks.py vcs qutebrowser scripts tests {envpython} scripts/misc_checks.py vcs
{envpython} scripts/misc_checks.py spelling qutebrowser scripts tests {envpython} scripts/misc_checks.py spelling
[testenv:pylint] [testenv:pylint]
skip_install = true skip_install = true
@ -60,14 +71,14 @@ deps =
logilab-common==0.63.2 logilab-common==0.63.2
six==1.9.0 six==1.9.0
commands = commands =
{[testenv:mkvenv]commands} {envpython} scripts/link_pyqt.py --tox {envdir}
{envdir}/bin/pylint scripts qutebrowser --rcfile=.pylintrc --output-format=colorized --reports=no {envpython} -m pylint scripts qutebrowser --rcfile=.pylintrc --output-format=colorized --reports=no --expected-line-ending-format=LF
{envpython} scripts/run_pylint_on_tests.py --rcfile=.pylintrc --output-format=colorized --reports=no {envpython} scripts/run_pylint_on_tests.py --rcfile=.pylintrc --output-format=colorized --reports=no --expected-line-ending-format=LF
[testenv:pep257] [testenv:pep257]
skip_install = true skip_install = true
deps = pep257==0.5.0 deps = pep257==0.5.0
passenv = LANG passenv = PYTHON LANG
# Disabled checks: # Disabled checks:
# D102: Docstring missing, will be handled by others # D102: Docstring missing, will be handled by others
# D209: Blank line before closing """ (removed from PEP257) # D209: Blank line before closing """ (removed from PEP257)
@ -79,43 +90,43 @@ commands = {envpython} -m pep257 scripts tests qutebrowser --ignore=D102,D103,D2
setenv = LANG=en_US.UTF-8 setenv = LANG=en_US.UTF-8
deps = deps =
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
py==1.4.27 py==1.4.30
pytest==2.7.1 pytest==2.7.2
pyflakes==0.9.0 pyflakes==0.9.2
pytest-flakes==0.2 pytest-flakes==1.0.0
commands = commands =
{[testenv:mkvenv]commands} {envpython} scripts/link_pyqt.py --tox {envdir}
{envpython} -m py.test -q --flakes -m flakes {envpython} -m py.test -q --flakes --ignore=tests
[testenv:pep8] [testenv:pep8]
deps = deps =
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
py==1.4.27 py==1.4.30
pytest==2.7.1 pytest==2.7.2
pep8==1.6.2 pep8==1.6.2
pytest-pep8==1.0.6 pytest-pep8==1.0.6
commands = commands =
{[testenv:mkvenv]commands} {envpython} scripts/link_pyqt.py --tox {envdir}
{envpython} -m py.test -q --pep8 -m pep8 {envpython} -m py.test -q --pep8 --ignore=tests
[testenv:mccabe] [testenv:mccabe]
deps = deps =
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
py==1.4.27 py==1.4.30
pytest==2.7.1 pytest==2.7.2
mccabe==0.3 mccabe==0.3.1
pytest-mccabe==0.1 pytest-mccabe==0.1
commands = commands =
{[testenv:mkvenv]commands} {envpython} scripts/link_pyqt.py --tox {envdir}
{envpython} -m py.test -q --mccabe -m mccabe {envpython} -m py.test -q --mccabe --ignore=tests
[testenv:pyroma] [testenv:pyroma]
skip_install = true skip_install = true
deps = deps =
pyroma==1.8.1 pyroma==1.8.2
docutils==0.12 docutils==0.12
commands = commands =
{[testenv:mkvenv]commands} {envpython} scripts/link_pyqt.py --tox {envdir}
{envdir}/bin/pyroma . {envdir}/bin/pyroma .
[testenv:check-manifest] [testenv:check-manifest]
@ -123,7 +134,7 @@ skip_install = true
deps = deps =
check-manifest==0.25 check-manifest==0.25
commands = commands =
{[testenv:mkvenv]commands} {envpython} scripts/link_pyqt.py --tox {envdir}
{envdir}/bin/check-manifest --ignore 'qutebrowser/git-commit-id,qutebrowser/html/doc,qutebrowser/html/doc/*,*/__pycache__' {envdir}/bin/check-manifest --ignore 'qutebrowser/git-commit-id,qutebrowser/html/doc,qutebrowser/html/doc/*,*/__pycache__'
[testenv:docs] [testenv:docs]
@ -132,7 +143,7 @@ whitelist_externals = git
deps = deps =
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
commands = commands =
{[testenv:mkvenv]commands} {envpython} scripts/link_pyqt.py --tox {envdir}
{envpython} scripts/src2asciidoc.py {envpython} scripts/src2asciidoc.py
git --no-pager diff --exit-code --stat git --no-pager diff --exit-code --stat
{envpython} scripts/asciidoc2html.py {posargs} {envpython} scripts/asciidoc2html.py {posargs}
@ -140,15 +151,35 @@ commands =
[testenv:smoke] [testenv:smoke]
# https://bitbucket.org/hpk42/tox/issue/246/ - only needed for Windows though # https://bitbucket.org/hpk42/tox/issue/246/ - only needed for Windows though
setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/platforms setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/platforms
passenv = DISPLAY XAUTHORITY HOME USERNAME USER passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER
deps = deps =
-r{toxinidir}/requirements.txt -r{toxinidir}/requirements.txt
# We don't use {[testenv:mkvenv]commands} here because that seems to be broken
# on Ubuntu Trusty.
commands = commands =
{envpython} scripts/link_pyqt.py --tox {envdir} {envpython} scripts/link_pyqt.py --tox {envdir}
{envpython} -m qutebrowser --no-err-windows --nowindow --temp-basedir about:blank ":later 500 quit" {envpython} -m qutebrowser --no-err-windows --nowindow --temp-basedir about:blank ":later 500 quit"
[testenv:smoke-frozen]
setenv = {[testenv:smoke]setenv}
passenv = {[testenv:smoke]passenv}
skip_install = true
deps =
{[testenv:smoke]deps}
cx_Freeze==4.3.4
commands =
{envpython} scripts/link_pyqt.py --tox {envdir}
{envpython} scripts/freeze.py build_exe --qute-skip-html -b {envdir}/build
{envdir}/build/qutebrowser --no-err-windows --nowindow --temp-basedir about:blank ":later 500 quit"
[testenv:cxfreeze-windows]
# PYTHON is actually required when using this env, but the entire tox.ini would
# fail if we didn't have a fallback defined.
basepython = {env:PYTHON:}/python.exe
skip_install = true
deps = {[testenv:smoke-frozen]deps}
commands =
{envpython} scripts/link_pyqt.py --tox {envdir}
{envpython} scripts/freeze.py {posargs}
[pytest] [pytest]
norecursedirs = .tox .venv norecursedirs = .tox .venv
markers = markers =
@ -165,3 +196,8 @@ pep8ignore =
W503 # line break before binary operator W503 # line break before binary operator
resources.py ALL resources.py ALL
mccabe-complexity = 12 mccabe-complexity = 12
qt_log_level_fail = WARNING
qt_log_ignore =
^SpellCheck: .*
^SetProcessDpiAwareness failed: .*
^QWindowsWindow::setGeometryDp: Unable to set geometry .*