Merge branch 'master' into save-fifo

This commit is contained in:
Martin Tournoij 2015-06-27 22:23:47 +02:00
commit 26664ba644
66 changed files with 3742 additions and 701 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

@ -27,7 +27,8 @@ disable=no-self-use,
broad-except,
bare-except,
eval-used,
exec-used
exec-used,
file-ignored
[BASIC]
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,30 +21,33 @@ Added
~~~~~
- 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 `: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 `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 -> 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 `--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 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 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 `colors -> webpage.bg` to set the background color to use for websites which don't set one.
- New (hidden) command `:clear-keychain` to clear a partially entered keychain (bound to `<Escape>` by default, in addition to clearing search).
- 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
~~~~~~~
- `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.
- *Breaking change for userscripts:* `QUTE_HTML` and `QUTE_TEXT` for userscripts now don't store the contents directly, and instead contain a filename.
- 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.
- `<Ctrl-M>` and numpad-enter are now bound by default for bindings where `<Return>` was bound.
@ -52,6 +55,10 @@ Changed
- `: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
~~~~~~~~~~
@ -69,23 +76,21 @@ Fixed
- 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.
v0.2.2 (unreleased)
-------------------
Fixed
~~~~~
- Fixed wrong cursor position when completing the first item in the completion.
- 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 searching for terms starting with a hyphen (e.g. `/-foo`)
- Proxy authentication credentials are now remembered between different tabs.
- Fixed updating of the tab title on pages without title.
- Fixed AssertionError when closing many windows quickly.
- 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 crash when downloading an URL without filename (e.g. magnet links) via "Save as...".
- 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]
-----------------------------------------------------------------------

View File

@ -153,7 +153,7 @@ Useful websites
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]
* http://httpbin.org/[httpbin, a test service for HTTP requests/responses]
* 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])
* http://www.w3.org/TR/CSS2/[Cascading Style Sheets Level 2 Revision 1 (CSS
2.1) Specification]
* http://qt-project.org/doc/qt-4.8/stylesheet-reference.html[Qt Style Sheets
Reference]
* http://doc.qt.io/qt-5/stylesheet-reference.html[Qt Style Sheets Reference]
* http://mimesniff.spec.whatwg.org/[MIME Sniffing Standard]
* http://spec.whatwg.org/[WHATWG specifications]
* 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
slots.
* `QProcess` is used instead of Python's `subprocess` if certain actions (e.g.
cleanup) when the process finished are desired, as it provides signals for
that.
* `QProcess` is used instead of Python's `subprocess`
* `QUrl` is used instead of storing URLs as string, see the
<<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
useful in places where using Qt's
http://qt-project.org/doc/qt-5/signalsandslots.html[signals and slots]
mechanism would be difficult.
http://doc.qt.io/qt-5/signalsandslots.html[signals and slots] mechanism would
be difficult.
Logging
~~~~~~~
@ -541,7 +538,7 @@ New Qt release
* Run all tests and check nothing is broken.
* 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.
* Update own PKGBUILDs based on upstream Archlinux updates and rebuild.
* Update recommended Qt version in `README`

View File

@ -4,8 +4,8 @@ The Compiler <mail@qutebrowser.org>
[qanda]
What is qutebrowser based on?::
qutebrowser uses http://www.python.org/[Python], http://qt-project.org/[Qt]
and http://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
qutebrowser uses http://www.python.org/[Python], http://qt.io/[Qt] and
http://www.riverbankcomputing.com/software/pyqt/intro[PyQt].
+
The concept of it is largely inspired by http://portix.bitbucket.org/dwb/[dwb]
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
happy with, so I started to write my own. Also, I needed a project to get
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
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
http://www.uzbl.org/[uzbl]).
+
qutebrowser uses http://qt-project.org/[Qt] and
http://qt-project.org/wiki/QtWebKit[QtWebKit] instead, which suffers from far
less such crashes. It might switch to
http://qt-project.org/wiki/QtWebEngine[QtWebEngine] in the future, which is
based on Google's https://en.wikipedia.org/wiki/Blink_(layout_engine)[Blink]
rendering engine.
qutebrowser uses http://qt.io/[Qt] and http://wiki.qt.io/QtWebKit[QtWebKit]
instead, which suffers from far less such crashes. It might switch to
http://wiki.qt.io/QtWebEngine[QtWebEngine] in the future, which is based on
Google's https://en.wikipedia.org/wiki/Blink_(layout_engine)[Blink] 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]?::
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?::
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
http://qt-project.org/wiki/QtWebKit[QtWebKit] so I didn't have
http://qt-project.org/wiki/Category:LanguageBindings[many other choices]. I
don't like C++ and can't write it very well, so that wasn't an alternative.
choices. I wanted to use http://qt.io/[Qt] because of
http://wiki.qt.io/QtWebKit[QtWebKit] so I didn't have
http://wiki.qt.io/Category:LanguageBindings[many other choices]. I don't
like C++ and can't write it very well, so that wasn't an alternative.
But isn't Python too slow for a browser?::
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
recursive-include qutebrowser *.py
recursive-include qutebrowser/html *.html
recursive-include qutebrowser/test *.py
recursive-include qutebrowser/javascript *.js
graft icons
graft scripts/pylint_checkers
graft doc/img
graft misc
graft scripts
include qutebrowser/utils/testfile
include qutebrowser/git-commit-id
include COPYING doc/* README.asciidoc CONTRIBUTING.asciidoc FAQ.asciidoc INSTALL.asciidoc CHANGELOG.asciidoc
@ -15,13 +16,15 @@ include requirements.txt
include tox.ini
include qutebrowser.py
exclude scripts/cleanup.py
exclude scripts/minimal_webkit_testbrowser.py
exclude scripts/run_profile.py
exclude scripts/src2asciidoc.sh
exclude scripts/gen_resources.sh
exclude scripts/quit_segfault_test.sh
exclude scripts/segfault_test.sh
include scripts/__init__.py
include scripts/hostblock_blame.py
include scripts/importer.py
include scripts/keytester.py
include scripts/link_pyqt.py
include scripts/minimal_webkit_testbrowser.py
include scripts/setupcommon.py
include scripts/utils.py
exclude doc/notes
recursive-exclude doc *.asciidoc
include doc/qutebrowser.1.asciidoc
@ -31,3 +34,6 @@ exclude .coveragerc
exclude .pylintrc
exclude .eslintrc
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
link:doc/CONTRIBUTING.asciidoc[the contribution guidelines] for details and
link:CONTRIBUTING.asciidoc[the contribution guidelines] for details and
useful hints.
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:
* http://www.python.org/[Python] 3.4
* http://qt-project.org/[Qt] 5.2.0 or newer (5.4.2 recommended)
* http://qt.io/[Qt] 5.2.0 or newer (5.4.2 recommended)
* QtWebKit
* 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]
* http://fdik.org/pyPEG/[pyPEG2]
* http://jinja.pocoo.org/[jinja2]
@ -137,9 +137,10 @@ Contributors, sorted by the number of commits in descending order:
* Bruno Oliveira
* Raphael Pierzina
* Joel Torstensson
* Martin Tournoij
* Claude
* Lamar Pavel
* Martin Tournoij
* Austin Anderson
* Artur Shaik
* Antoni Boucher
* ZDarian
@ -160,6 +161,7 @@ Contributors, sorted by the number of commits in descending order:
* Mathias Fussenegger
* Larry Hynes
* Fritz V155 Reichwald
* Franz Fellner
* error800
* Thorsten Wißmann
* Thiago Barroso Perrotta
@ -167,7 +169,6 @@ Contributors, sorted by the number of commits in descending order:
* Helen Sherwood-Taylor
* HalosGhost
* Gregor Pohl
* Franz Fellner
* Eivind Uggedal
* Andreas Fischer
// QUTE_AUTHORS_END
@ -221,7 +222,7 @@ Also, thanks to:
* Everyone who had the patience to test qutebrowser before v0.1.
* 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]
and in IRC.
* All the projects which were a great help while developing qutebrowser.

View File

@ -20,6 +20,7 @@
|<<hint,hint>>|Start hinting.
|<<home,home>>|Open main startpage in current tab.
|<<inspector,inspector>>|Toggle the web inspector.
|<<jseval,jseval>>|Evaluate a JavaScript string.
|<<later,later>>|Execute a command after some time.
|<<navigate,navigate>>|Open typical prev/next links or navigate using the URL path.
|<<open,open>>|Open a URL in the current/[count]th tab.
@ -241,6 +242,22 @@ Open main startpage in current tab.
=== 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
Syntax: +:later 'ms' 'command'+
@ -512,18 +529,23 @@ Preset the statusbar to some text.
[[spawn]]
=== spawn
Syntax: +:spawn [*--userscript*] [*--quiet*] 'args' ['args' ...]+
Syntax: +:spawn [*--userscript*] [*--verbose*] [*--detach*] 'cmdline'+
Spawn a command in a shell.
Note the {url} variable which gets replaced by the current URL might be useful here.
==== positional arguments
* +'args'+: The commandline to execute.
* +'cmdline'+: The commandline to execute.
==== optional arguments
* +*-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

View File

@ -65,6 +65,7 @@
[options="header",width="75%",cols="25%,75%"]
|==============
|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-timestamp-format,timestamp-format>>|How to format timestamps (e.g. for history)
|<<completion-show,show>>|Whether to show the autocompletion window.
@ -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.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-statusbar.bg,statusbar.bg>>|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.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.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.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.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.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.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.
@ -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.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.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.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.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.bar,tabs.bg.bar>>|Background color of the tab bar.
|<<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.system,tabs.indicator.system>>|Color gradient interpolation system for the tab indicator.
|<<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-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.start,downloads.bg.start>>|Color gradient start for downloads.
|<<colors-downloads.bg.stop,downloads.bg.stop>>|Color gradient end for downloads.
|<<colors-downloads.bg.system,downloads.bg.system>>|Color gradient interpolation system for downloads.
|<<colors-downloads.fg.start,downloads.fg.start>>|Color gradient start for download text.
|<<colors-downloads.bg.start,downloads.bg.start>>|Color gradient start for download backgrounds.
|<<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-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.
* +window+: Open in a new window.
Default: +pass:[window]+
Default: +pass:[tab]+
[[general-log-javascript-console]]
=== log-javascript-console
@ -683,6 +695,17 @@ Default: +pass:[true]+
== completion
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]]
=== download-path-suggestion
What to display in the download filename input.
@ -1463,7 +1486,9 @@ A value can be in one of the following format:
* transparent (no color)
* `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)
* 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].
@ -1539,17 +1564,23 @@ Foreground color of the matched text in the completion.
Default: +pass:[#ff4444]+
[[colors-statusbar.fg]]
=== statusbar.fg
Foreground color of the statusbar.
Default: +pass:[white]+
[[colors-statusbar.bg]]
=== statusbar.bg
Foreground color of the statusbar.
Default: +pass:[black]+
[[colors-statusbar.fg]]
=== statusbar.fg
Foreground color of the statusbar.
[[colors-statusbar.fg.error]]
=== statusbar.fg.error
Foreground color of the statusbar if there was an error.
Default: +pass:[white]+
Default: +pass:[${statusbar.fg}]+
[[colors-statusbar.bg.error]]
=== statusbar.bg.error
@ -1557,30 +1588,72 @@ Background color of the statusbar if there was an error.
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]]
=== statusbar.bg.warning
Background color of the statusbar if there is a warning.
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]]
=== statusbar.bg.prompt
Background color of the statusbar if there is a prompt.
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]]
=== statusbar.bg.insert
Background color of the statusbar in insert mode.
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]]
=== statusbar.bg.caret
Background color of the statusbar in caret mode.
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]]
=== statusbar.bg.caret-selection
Background color of the statusbar in caret mode with a selection
@ -1629,30 +1702,30 @@ Foreground color of unselected odd tabs.
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]]
=== tabs.bg.odd
Background color of unselected odd tabs.
Default: +pass:[grey]+
[[colors-tabs.fg.even]]
=== tabs.fg.even
Foreground color of unselected even tabs.
Default: +pass:[white]+
[[colors-tabs.bg.even]]
=== tabs.bg.even
Background color of unselected even tabs.
Default: +pass:[darkgrey]+
[[colors-tabs.fg.selected]]
=== tabs.fg.selected
Foreground color of selected tabs.
Default: +pass:[white]+
[[colors-tabs.bg.selected]]
=== tabs.bg.selected
Background color of selected tabs.
@ -1701,23 +1774,17 @@ Font color for hints.
Default: +pass:[black]+
[[colors-hints.fg.match]]
=== hints.fg.match
Font color for the matched part of hints.
Default: +pass:[green]+
[[colors-hints.bg]]
=== hints.bg
Background color for hints.
Default: +pass:[-webkit-gradient(linear, left top, left bottom, color-stop(0%,#FFF785), color-stop(100%,#FFC542))]+
[[colors-downloads.fg]]
=== downloads.fg
Foreground color for downloads.
[[colors-hints.fg.match]]
=== hints.fg.match
Font color for the matched part of hints.
Default: +pass:[#ffffff]+
Default: +pass:[green]+
[[colors-downloads.bg.bar]]
=== downloads.bg.bar
@ -1725,21 +1792,33 @@ Background color for the download bar.
Default: +pass:[black]+
[[colors-downloads.fg.start]]
=== downloads.fg.start
Color gradient start for download text.
Default: +pass:[white]+
[[colors-downloads.bg.start]]
=== downloads.bg.start
Color gradient start for downloads.
Color gradient start for download backgrounds.
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]]
=== downloads.bg.stop
Color gradient end for downloads.
Color gradient stop for download backgrounds.
Default: +pass:[#00aa00]+
[[colors-downloads.bg.system]]
=== downloads.bg.system
Color gradient interpolation system for downloads.
[[colors-downloads.fg.system]]
=== downloads.fg.system
Color gradient interpolation system for download text.
Valid values:
@ -1749,6 +1828,24 @@ Valid values:
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]]
=== downloads.bg.error
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 utilcmds # pylint: disable=unused-import
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.
@ -61,7 +61,7 @@ def run(args):
"""Initialize everthing and run the application."""
# pylint: disable=too-many-statements
if args.version:
print(version.version())
print(version.version(short=True))
print()
print()
print(qutebrowser.__copyright__)
@ -148,7 +148,9 @@ def init(args, crash_handler):
error.handle_fatal_exc(e, args, "Error while initializing!",
pre_text="Error while initializing")
sys.exit(usertypes.Exit.err_init)
QTimer.singleShot(0, functools.partial(_process_args, args))
QTimer.singleShot(10, functools.partial(_init_late_modules, args))
log.init.debug("Initializing eventfilter...")
event_filter = EventFilter(qApp)
@ -428,6 +430,23 @@ def _init_modules(args, crash_handler):
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:
"""Utility class to quit/restart the QApplication.

View File

@ -22,7 +22,6 @@
import re
import os
import shlex
import subprocess
import posixpath
import functools
import xml.etree.ElementTree
@ -37,14 +36,14 @@ import pygments
import pygments.lexers
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.browser import webelem, inspector
from qutebrowser.keyinput import modeman
from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
objreg, utils)
from qutebrowser.utils.usertypes import KeyMode
from qutebrowser.misc import editor
from qutebrowser.misc import editor, guiprocess
class CommandDispatcher:
@ -738,7 +737,11 @@ class CommandDispatcher:
count: How many steps to zoom in.
"""
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',
count='count')
@ -749,7 +752,11 @@ class CommandDispatcher:
count: How many steps to zoom out.
"""
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',
count='count')
@ -769,7 +776,12 @@ class CommandDispatcher:
except ValueError as e:
raise cmdexc.CommandError(e)
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')
def tab_only(self, left=False, right=False):
@ -922,38 +934,39 @@ class CommandDispatcher:
self._tabbed_browser.setUpdatesEnabled(True)
@cmdutils.register(instance='command-dispatcher', scope='window',
win_id='win_id')
def spawn(self, win_id, userscript=False, quiet=False, *args):
maxsplit=0)
def spawn(self, cmdline, userscript=False, verbose=False, detach=False):
"""Spawn a command in a shell.
Note the {url} variable which gets replaced by the current URL might be
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:
userscript: Run the command as an userscript.
quiet: Don't print the commandline being executed.
*args: The commandline to execute.
verbose: Show notifications when the command started/exited.
detach: Whether the command should be detached from qutebrowser.
cmdline: The commandline to execute.
"""
log.procs.debug("Executing: {}, userscript={}".format(
args, userscript))
if not quiet:
fake_cmdline = ' '.join(shlex.quote(arg) for arg in args)
message.info(win_id, 'Executing: ' + fake_cmdline)
try:
cmd, *args = shlex.split(cmdline)
except ValueError as e:
raise cmdexc.CommandError("Error while splitting command: "
"{}".format(e))
args = runners.replace_variables(self._win_id, args)
log.procs.debug("Executing {} with args {}, userscript={}".format(
cmd, args, userscript))
if userscript:
cmd = args[0]
args = [] if not args else args[1:]
self.run_userscript(cmd, *args)
self.run_userscript(cmd, *args, verbose=verbose)
else:
try:
subprocess.Popen(args)
except OSError as e:
raise cmdexc.CommandError("Error while spawning command: "
"{}".format(e))
proc = guiprocess.GUIProcess(self._win_id, what='command',
verbose=verbose,
parent=self._tabbed_browser)
if detach:
proc.start_detached(cmd, args)
else:
proc.start(cmd, args)
@cmdutils.register(instance='command-dispatcher', scope='window')
def home(self):
@ -962,12 +975,13 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window',
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.
Args:
cmd: The userscript to run.
args: Arguments to pass to the userscript.
verbose: Show notifications when the command started/exited.
"""
cmd = os.path.expanduser(cmd)
env = {
@ -995,7 +1009,8 @@ class CommandDispatcher:
env['QUTE_URL'] = url.toString(QUrl.FullyEncoded)
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')
def quickmark_save(self):
@ -1166,12 +1181,6 @@ class CommandDispatcher:
The editor which should be launched can be configured via the
`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()
try:
@ -1193,7 +1202,7 @@ class CommandDispatcher:
def on_editing_finished(self, elem, text):
"""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:
elem: The WebElementWrapper which was modified.
@ -1576,3 +1585,33 @@ class CommandDispatcher:
view = self._current_widget()
for _ in range(count):
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):
"""Purge expired cookies from the cookie jar."""
# 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()
cookies = [c for c in self.allCookies()
if c.isSessionCookie() or c.expirationDate() >= now]

View File

@ -355,12 +355,19 @@ class DownloadItem(QObject):
if reply.error() != QNetworkReply.NoError:
QTimer.singleShot(0, lambda: self.error.emit(reply.errorString()))
def bg_color(self):
"""Background color to be shown."""
start = config.get('colors', 'downloads.bg.start')
stop = config.get('colors', 'downloads.bg.stop')
system = config.get('colors', 'downloads.bg.system')
error = config.get('colors', 'downloads.bg.error')
def get_status_color(self, position):
"""Choose an appropriate color for presenting the download's status.
Args:
position: The color type requested, can be 'fg' or 'bg'.
"""
# 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:
assert not self.successful
return error
@ -685,7 +692,7 @@ class DownloadManager(QAbstractListModel):
if fileobj is not None and filename is not None:
raise TypeError("Only one of fileobj/filename may be given!")
# 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,
QNetworkRequest.AlwaysNetwork)
suggested_fn = urlutils.filename_from_url(request.url())
@ -1029,9 +1036,9 @@ class DownloadManager(QAbstractListModel):
if role == Qt.DisplayRole:
data = str(item)
elif role == Qt.ForegroundRole:
data = config.get('colors', 'downloads.fg')
data = item.get_status_color('fg')
elif role == Qt.BackgroundRole:
data = item.bg_color()
data = item.get_status_color('bg')
elif role == ModelRole.item:
data = item
elif role == Qt.ToolTipRole:

View File

@ -21,7 +21,6 @@
import math
import functools
import subprocess
import collections
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.commands import userscripts, cmdexc, cmdutils, runners
from qutebrowser.utils import usertypes, log, qtutils, message, objreg
from qutebrowser.misc import guiprocess
ElemTuple = collections.namedtuple('ElemTuple', ['elem', 'label'])
@ -548,11 +548,9 @@ class HintManager(QObject):
"""
urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword)
args = context.get_args(urlstr)
try:
subprocess.Popen(args)
except OSError as e:
msg = "Error while spawning command: {}".format(e)
message.error(self._win_id, msg, immediately=True)
cmd, *args = args
proc = guiprocess.GUIProcess(self._win_id, what='command', parent=self)
proc.start(cmd, args)
def _resolve_url(self, elem, baseurl):
"""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.
_new_history: A list of HistoryEntry items of the current session.
_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:
item_about_to_be_added: Emitted before a new HistoryEntry is added.
arg: The new HistoryEntry.
add_completion_item: Emitted before a new HistoryEntry is added.
arg: The new HistoryEntry.
item_added: Emitted after a new HistoryEntry is added.
arg: The new HistoryEntry.
"""
item_about_to_be_added = pyqtSignal(HistoryEntry)
add_completion_item = pyqtSignal(HistoryEntry)
item_added = pyqtSignal(HistoryEntry)
async_read_done = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self._initial_read_started = False
self._initial_read_done = False
self._lineparser = lineparser.AppendLineParser(
standarddir.data(), 'history', parent=self)
self._history_dict = collections.OrderedDict()
self._read_history()
self._temp_history = collections.OrderedDict()
self._new_history = []
self._saved_count = 0
objreg.get('save-manager').add_saveable(
@ -101,12 +108,21 @@ class WebHistory(QWebHistoryInterface):
def __len__(self):
return len(self._history_dict)
def _read_history(self):
def async_read(self):
"""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
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():
for line in self._lineparser:
yield
data = line.rstrip().split(maxsplit=1)
if not data:
# empty line
@ -128,8 +144,23 @@ class WebHistory(QWebHistoryInterface):
# information about previous hits change the items in
# old_urls to be lists or change HistoryEntry to have a
# list of atimes.
self._history_dict[url] = HistoryEntry(atime, url)
self._history_dict.move_to_end(url)
entry = HistoryEntry(atime, 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):
"""Get the most recent history entries."""
@ -151,13 +182,16 @@ class WebHistory(QWebHistoryInterface):
"""
if not url_string:
return
if not config.get('general', 'private-browsing'):
entry = HistoryEntry(time.time(), url_string)
self.item_about_to_be_added.emit(entry)
if config.get('general', 'private-browsing'):
return
entry = HistoryEntry(time.time(), url_string)
if self._initial_read_done:
self.add_completion_item.emit(entry)
self._new_history.append(entry)
self._history_dict[url_string] = entry
self._history_dict.move_to_end(url_string)
self._add_entry(entry)
self.item_added.emit(entry)
else:
self._add_entry(entry, target=self._temp_history)
def historyContains(self, url_string):
"""Called by WebKit to determine if an URL is contained in the history.

View File

@ -159,7 +159,7 @@ def qute_help(win_id, request):
url=request.url().toDisplayString(),
error="This most likely means the documentation was not generated "
"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 "
"use :report to report it.",
icon='')

View File

@ -241,7 +241,7 @@ class BrowserPage(QWebPage):
if cur_data is not None:
frame = self.mainFrame()
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
frame.scrollPosition() == QPoint(0, 0)):
QTimer.singleShot(0, functools.partial(
@ -418,7 +418,7 @@ class BrowserPage(QWebPage):
if data is None:
return
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):
frame.setScrollPosition(data['scroll-pos'])

View File

@ -33,7 +33,6 @@ from qutebrowser.config import config
from qutebrowser.keyinput import modeman
from qutebrowser.utils import message, log, usertypes, utils, qtutils, objreg
from qutebrowser.browser import webpage, hints, webelem
from qutebrowser.commands import cmdexc
LoadStatus = usertypes.enum('LoadStatus', ['none', 'success', 'error', 'warn',
@ -369,9 +368,8 @@ class WebView(QWebView):
if fuzzyval:
self._zoom.fuzzyval = int(perc)
if perc < 0:
raise cmdexc.CommandError("Can't zoom {}%!".format(perc))
raise ValueError("Can't zoom {}%!".format(perc))
self.setZoomFactor(float(perc) / 100)
message.info(self.win_id, "Zoom level: {}%".format(perc))
self._default_zoom_changed = True
def zoom(self, offset):
@ -379,9 +377,13 @@ class WebView(QWebView):
Args:
offset: The offset in the zoom level list.
Return:
The new zoom percentage.
"""
level = self._zoom.getitem(offset)
self.zoom_perc(level, fuzzyval=False)
return level
@pyqtSlot('QUrl')
def on_url_changed(self, url):
@ -460,15 +462,22 @@ class WebView(QWebView):
elif mode == usertypes.KeyMode.caret:
settings = self.settings()
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True)
self.selection_enabled = False
self.selection_enabled = bool(self.page().selectedText())
if self.isVisible():
# Sometimes the caret isn't immediately visible, but unfocusing
# and refocusing it fixes that.
self.clearFocus()
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)
def on_mode_left(self, mode):

View File

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

View File

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

View File

@ -19,7 +19,7 @@
"""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.commands import cmdutils, runners
@ -40,14 +40,22 @@ class Completer(QObject):
_last_cursor_pos: The old cursor position 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):
super().__init__(parent)
self._win_id = win_id
self._cmd = cmd
self._cmd.update_completion.connect(self.schedule_completion_update)
self._cmd.textEdited.connect(self.on_text_edited)
self._signals_connected = False
self._ignore_change = False
self._empty_item_idx = None
self._timer = QTimer()
@ -58,9 +66,63 @@ class Completer(QObject):
self._last_cursor_pos = 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):
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):
"""Convienience method to get the current completion model."""
completion = objreg.get('completion', scope='window',
@ -328,7 +390,7 @@ class Completer(QObject):
cursor_pos))
skip = 0
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:
skip += 1
continue
@ -350,7 +412,11 @@ class Completer(QObject):
"Removing len({!r}) -> {} from cursor_pos -> {}".format(
part, len(part), cursor_pos))
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:
self._empty_item_idx = i - skip
else:
@ -401,3 +467,17 @@ class Completer(QObject):
# We also want to update the cursor part and emit update_completion
# here, but that's already done for us by cursorPositionChanged
# 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.
"""
# 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())
self._painter.save()
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.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel
from qutebrowser.commands import cmdutils
from qutebrowser.config import config, style
from qutebrowser.completion import completiondelegate, completer
from qutebrowser.utils import usertypes, qtutils, objreg, utils
from qutebrowser.utils import qtutils, objreg, utils
class CompletionView(QTreeView):
@ -96,12 +95,13 @@ class CompletionView(QTreeView):
objreg.register('completion', self, 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.next_prev_item.connect(self.on_next_prev_item)
objreg.register('completer', completer_obj, scope='window',
window=win_id)
self.enabled = config.get('completion', 'show')
objreg.get('config').changed.connect(self.set_enabled)
# 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.setItemDelegate(self._delegate)
@ -168,12 +168,15 @@ class CompletionView(QTreeView):
# Item is a real item, not a category header -> success
return idx
def _next_prev_item(self, prev):
@pyqtSlot(bool)
def on_next_prev_item(self, prev):
"""Handle a tab press for the CompletionView.
Select the previous/next item and write the new text to the
statusbar.
Called from the Completer's next_prev_item signal.
Args:
prev: True for prev item, False for next one.
"""
@ -233,18 +236,6 @@ class CompletionView(QTreeView):
selmod.clearSelection()
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):
"""Extend selectionChanged to call completers selection_changed."""
super().selectionChanged(selected, deselected)

View File

@ -165,6 +165,11 @@ def init():
quickmark_manager.changed.connect(
functools.partial(update, [usertypes.Completion.quickmark_by_url,
usertypes.Completion.quickmark_by_name]))
session_manager = objreg.get('session-manager')
session_manager.update_completion.connect(
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)
for entry in history:
self._add_history_entry(entry)
self._history.item_about_to_be_added.connect(
self._history.add_completion_item.connect(
self.on_history_item_added)
objreg.get('config').changed.connect(self.reformat_timestamps)

View File

@ -321,6 +321,7 @@ class ConfigManager(QObject):
('colors', 'tab.indicator.system'): 'tabs.indicator.system',
('tabs', 'auto-hide'): 'hide-auto',
('completion', 'history-length'): 'cmd-history-max-items',
('colors', 'downloads.fg'): 'downloads.fg.start',
}
DELETED_OPTIONS = [
('colors', 'tab.separator'),

View File

@ -100,9 +100,13 @@ SECTION_DESC = {
" * `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or "
"percentages)\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 "
"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 "
"colors, not Qt-CSS colors. There, for a gradient, you need to use "
"`-webkit-gradient`, see https://www.webkit.org/blog/175/introducing-"
@ -204,7 +208,7 @@ def data(readonly=False):
"be used."),
('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 "
"launched."),
@ -348,6 +352,10 @@ def data(readonly=False):
)),
('completion', sect.KeyValue(
('auto-open',
SettingValue(typ.Bool(), 'true'),
"Automatically open completion when typing."),
('download-path-suggestion',
SettingValue(typ.DownloadPathSuggestion(), 'path'),
"What to display in the download filename input."),
@ -818,34 +826,67 @@ def data(readonly=False):
SettingValue(typ.QssColor(), '#ff4444'),
"Foreground color of the matched text in the completion."),
('statusbar.fg',
SettingValue(typ.QssColor(), 'white'),
"Foreground color of the statusbar."),
('statusbar.bg',
SettingValue(typ.QssColor(), 'black'),
"Foreground color of the statusbar."),
('statusbar.fg',
SettingValue(typ.QssColor(), 'white'),
"Foreground color of the statusbar."),
('statusbar.fg.error',
SettingValue(typ.QssColor(), '${statusbar.fg}'),
"Foreground color of the statusbar if there was an error."),
('statusbar.bg.error',
SettingValue(typ.QssColor(), 'red'),
"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',
SettingValue(typ.QssColor(), 'darkorange'),
"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',
SettingValue(typ.QssColor(), 'darkblue'),
"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',
SettingValue(typ.QssColor(), 'darkgreen'),
"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',
SettingValue(typ.QssColor(), 'purple'),
"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',
SettingValue(typ.QssColor(), '#a12dff'),
"Background color of the statusbar in caret mode with a "
@ -882,22 +923,22 @@ def data(readonly=False):
SettingValue(typ.QtColor(), 'white'),
"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',
SettingValue(typ.QtColor(), 'grey'),
"Background color of unselected odd tabs."),
('tabs.fg.even',
SettingValue(typ.QtColor(), 'white'),
"Foreground color of unselected even tabs."),
('tabs.bg.even',
SettingValue(typ.QtColor(), 'darkgrey'),
"Background color of unselected even tabs."),
('tabs.fg.selected',
SettingValue(typ.QtColor(), 'white'),
"Foreground color of selected tabs."),
('tabs.bg.selected',
SettingValue(typ.QtColor(), 'black'),
"Background color of selected tabs."),
@ -926,10 +967,6 @@ def data(readonly=False):
SettingValue(typ.CssColor(), 'black'),
"Font color for hints."),
('hints.fg.match',
SettingValue(typ.CssColor(), 'green'),
"Font color for the matched part of hints."),
('hints.bg',
SettingValue(
typ.CssColor(), '-webkit-gradient(linear, left top, '
@ -937,25 +974,41 @@ def data(readonly=False):
'color-stop(100%,#FFC542))'),
"Background color for hints."),
('downloads.fg',
SettingValue(typ.QtColor(), '#ffffff'),
"Foreground color for downloads."),
('hints.fg.match',
SettingValue(typ.CssColor(), 'green'),
"Font color for the matched part of hints."),
('downloads.bg.bar',
SettingValue(typ.QssColor(), 'black'),
"Background color for the download bar."),
('downloads.fg.start',
SettingValue(typ.QtColor(), 'white'),
"Color gradient start for download text."),
('downloads.bg.start',
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',
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',
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',
SettingValue(typ.QtColor(), 'red'),
@ -1299,7 +1352,7 @@ KEY_DATA = collections.OrderedDict([
('rl-unix-line-discard', ['<Ctrl-U>']),
('rl-kill-line', ['<Ctrl-K>']),
('rl-kill-word', ['<Alt-D>']),
('rl-unix-word-rubout', ['<Ctrl-W>']),
('rl-unix-word-rubout', ['<Ctrl-W>', '<Alt-Backspace>']),
('rl-yank', ['<Ctrl-Y>']),
('rl-delete-char', ['<Ctrl-?>']),
('rl-backward-delete-char', ['<Ctrl-H>']),

View File

@ -1110,8 +1110,15 @@ class SearchEngineUrl(BaseType):
return
else:
raise configexc.ValidationError(value, "may not be empty!")
if '{}' not in value:
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'))
if not url.isValid():
raise configexc.ValidationError(value, "invalid url, {}".format(

View File

@ -120,14 +120,6 @@ class MainWindow(QWidget):
window=self.win_id)
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.setContentsMargins(0, 0, 0, 0)
self._vbox.setSpacing(0)
@ -165,6 +157,15 @@ class MainWindow(QWidget):
log.init.debug("Initializing modes...")
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()
# 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
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).
For some reason we need to have this as class attribute
@ -97,41 +102,68 @@ class StatusBar(QWidget):
_severity = None
_prompt_active = False
_insert_active = False
_command_active = False
_caret_mode = CaretMode.off
STYLESHEET = """
QWidget#StatusBar {
QWidget#StatusBar,
QWidget#StatusBar QLabel,
QWidget#StatusBar QLineEdit {
{{ font['statusbar'] }}
{{ color['statusbar.bg'] }}
{{ color['statusbar.fg'] }}
}
QWidget#StatusBar[insert_active="true"] {
{{ color['statusbar.bg.insert'] }}
}
QWidget#StatusBar[caret_mode="on"] {
QWidget#StatusBar[caret_mode="on"],
QWidget#StatusBar[caret_mode="on"] QLabel,
QWidget#StatusBar[caret_mode="on"] QLineEdit {
{{ color['statusbar.fg.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'] }}
}
QWidget#StatusBar[prompt_active="true"] {
{{ color['statusbar.bg.prompt'] }}
}
QWidget#StatusBar[severity="error"] {
QWidget#StatusBar[severity="error"],
QWidget#StatusBar[severity="error"] QLabel,
QWidget#StatusBar[severity="error"] QLineEdit {
{{ color['statusbar.fg.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'] }}
}
QLabel, QLineEdit {
{{ color['statusbar.fg'] }}
{{ font['statusbar'] }}
QWidget#StatusBar[prompt_active="true"],
QWidget#StatusBar[prompt_active="true"] QLabel,
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):
@ -263,6 +295,11 @@ class StatusBar(QWidget):
self._prompt_active = val
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)
def insert_active(self):
"""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
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
updated by Qt properly.
@ -282,6 +319,9 @@ class StatusBar(QWidget):
if mode == usertypes.KeyMode.insert:
log.statusbar.debug("Setting insert_active to {}".format(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:
webview = objreg.get('tabbed-browser', scope='window',
window=self._win_id).currentWidget()
@ -473,7 +513,9 @@ class StatusBar(QWidget):
window=self._win_id)
if keyparsers[mode].passthrough:
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)
@pyqtSlot(usertypes.KeyMode, usertypes.KeyMode)
@ -486,7 +528,9 @@ class StatusBar(QWidget):
self._set_mode_text(new_mode.name)
else:
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)
@config.change_filter('ui', 'message-timeout')

View File

@ -70,7 +70,7 @@ class TextBase(QLabel):
More info:
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/
Args:

View File

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

View File

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

View File

@ -300,6 +300,7 @@ class SignalHandler(QObject):
signal.SIGTERM, self.interrupt)
if os.name == 'posix' and hasattr(signal, 'set_wakeup_fd'):
# pylint: disable=import-error,no-member
import fcntl
read_fd, write_fd = os.pipe()
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
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:
https://bugreports.qt-project.org/browse/QTBUG-39278
https://bugreports.qt.io/browse/QTBUG-39278
So the new engine will be more stable.
- On Qt 5.3.1 this bug is fixed and the old engine will be the more stable

View File

@ -22,10 +22,11 @@
import os
import tempfile
from PyQt5.QtCore import pyqtSignal, QProcess, QObject
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QProcess
from qutebrowser.config import config
from qutebrowser.utils import message, log
from qutebrowser.misc import guiprocess
class ExternalEditor(QObject):
@ -36,7 +37,7 @@ class ExternalEditor(QObject):
_text: The current text before the editor is opened.
_oshandle: The OS level 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.
"""
@ -69,15 +70,10 @@ class ExternalEditor(QObject):
log.procs.debug("Editor closed")
if exitstatus != QProcess.NormalExit:
# No error/cleanup here, since we already handle this in
# on_proc_error
# on_proc_error.
return
try:
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
encoding = config.get('general', 'editor-encoding')
try:
@ -94,22 +90,8 @@ class ExternalEditor(QObject):
finally:
self._cleanup()
def on_proc_error(self, error):
"""Display an error message and clean up when editor crashed."""
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]))
@pyqtSlot(QProcess.ProcessError)
def on_proc_error(self, _err):
self._cleanup()
def edit(self, text):
@ -132,7 +114,8 @@ class ExternalEditor(QObject):
message.error(self._win_id, "Failed to create initial file: "
"{}".format(e))
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.error.connect(self.on_proc_error)
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():
encoded = item.originalUrl().toEncoded()
item_data['original-url'] = bytes(encoded).decode('ascii')
user_data = item.userData()
if history.currentItemIndex() == idx:
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:
data['zoom'] = user_data['zoom']
item_data['zoom'] = user_data['zoom']
if 'scroll-pos' in user_data:
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
def _save_all(self):
@ -235,11 +237,25 @@ class SessionManager(QObject):
entries = []
for histentry in data['history']:
user_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']
elif 'zoom' in histentry:
user_data['zoom'] = histentry['zoom']
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']
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)
url = QUrl.fromEncoded(histentry['url'].encode('ascii'))
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.
"""
started = datetime.datetime.now()
yield
finished = datetime.datetime.now()
delta = (finished - started).total_seconds()
logger.debug("{} took {} seconds.".format(action.capitalize(), delta))
try:
yield
finally:
finished = datetime.datetime.now()
delta = (finished - started).total_seconds()
logger.debug("{} took {} seconds.".format(action.capitalize(), delta))
def _get_widgets():

View File

@ -159,8 +159,10 @@ def init_log(args):
def disable_qt_msghandler():
"""Contextmanager which temporarily disables the Qt message handler."""
old_handler = QtCore.qInstallMessageHandler(None)
yield
QtCore.qInstallMessageHandler(old_handler)
try:
yield
finally:
QtCore.qInstallMessageHandler(old_handler)
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 = (
# 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 "
"been edited",
# Hopefully harmless warning
"OpenType support missing for script ",
# Error if a QNetworkReply gets two different errors set. Harmless Qt
# 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 "
"only be called once.",
# Sometimes indicates missing text, but most of the time harmless
"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 "
"application/x-www-form-urlencoded. Use QNetworkRequest::setHeader() "
"to fix this problem.",
# https://bugreports.qt-project.org/browse/QTBUG-43118
# https://bugreports.qt.io/browse/QTBUG-43118
"Using blocking call!",
# Hopefully harmless
'"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: "_ECORE_',
'QXcbWindow: Unhandled client message: "_GTK_',
# Happens on AppVeyor CI
'SetProcessDpiAwareness failed:',
)
if any(msg.strip().startswith(pattern) for pattern in suppressed_msgs):
level = logging.DEBUG
@ -317,8 +321,10 @@ def hide_qt_warning(pattern, logger='qt'):
log_filter = QtWarningFilter(pattern)
logger_obj = logging.getLogger(logger)
logger_obj.addFilter(log_filter)
yield
logger_obj.removeFilter(log_filter)
try:
yield
finally:
logger_obj.removeFilter(log_filter)
class QtWarningFilter(logging.Filter):

View File

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

View File

@ -118,7 +118,7 @@ def _get(typ):
Args:
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)
if not overridden:
@ -127,7 +127,7 @@ def _get(typ):
if (typ == QStandardPaths.ConfigLocation and
path.split(os.sep)[-1] != appname):
# WORKAROUND - see
# https://bugreports.qt-project.org/browse/QTBUG-38872
# https://bugreports.qt.io/browse/QTBUG-38872
path = os.path.join(path, appname)
if typ == QStandardPaths.DataLocation and os.name == 'nt':
# 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 "
"qutebrowser.\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:
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
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
to solve this?
https://github.com/The-Compiler/qutebrowser/issues/109

View File

@ -50,8 +50,6 @@ def elide(text, length):
def compact_text(text, elidelength=None):
"""Remove leading whitespace and newlines from a text and maybe elide it.
FIXME: Add tests.
Args:
text: The text to compact.
elidelength: To how many chars to elide.
@ -105,12 +103,12 @@ def actute_warning():
try:
if qtutils.version_check('5.3.0'):
return
except ValueError:
except ValueError: # pragma: no cover
pass
try:
with open('/usr/share/X11/locale/en_US.UTF-8/Compose', 'r',
encoding='utf-8') as f:
for line in f:
for line in f: # pragma: no branch
if '<dead_actute>' in line:
if sys.stdout is not None:
sys.stdout.flush()
@ -118,7 +116,7 @@ def actute_warning():
"that is not a bug in qutebrowser! See "
"https://bugs.freedesktop.org/show_bug.cgi?id=69476 "
"for details.")
break
break # pragma: no branch
except OSError:
log.init.exception("Failed to read Compose file")
@ -242,7 +240,7 @@ def key_to_string(key):
"""
special_names_str = {
# 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 ;)
# For dead/combining keys, we return the corresponding non-combining
# key, as that's easier to add to the config.
@ -290,6 +288,18 @@ def key_to_string(key):
'Key_TouchpadOn': 'Touchpad On',
'Key_TouchpadToggle': 'Touchpad toggle',
'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.
# 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."""
old_excepthook = sys.excepthook
sys.excepthook = sys.__excepthook__
yield
# If the code we did run did change sys.excepthook, we leave it
# unchanged. Otherwise, we reset it.
if sys.excepthook is sys.__excepthook__:
sys.excepthook = old_excepthook
try:
yield
finally:
# If the code we did run did change sys.excepthook, we leave it
# unchanged. Otherwise, we reset it.
if sys.excepthook is sys.__excepthook__:
sys.excepthook = old_excepthook
class prevent_exceptions: # pylint: disable=invalid-name
@ -541,7 +553,7 @@ def qualname(obj):
elif hasattr(obj, '__name__'):
name = obj.__name__
else:
name = '<unknown>'
name = repr(obj)
if inspect.isclass(obj) or inspect.isfunction(obj):
module = obj.__module__

View File

@ -30,6 +30,7 @@ import collections
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, qVersion
from PyQt5.QtWebKit import qWebKitVersion
from PyQt5.QtNetwork import QSslSocket
from PyQt5.QtWidgets import QApplication
import qutebrowser
from qutebrowser.utils import log, utils
@ -111,7 +112,7 @@ def _release_info():
for fn in glob.glob("/etc/*-release"):
try:
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:
log.misc.exception("Error while reading {}.".format(fn))
return data
@ -183,8 +184,12 @@ def _os_info():
return lines
def version():
"""Return a string with various version informations."""
def version(short=False):
"""Return a string with various version informations.
Args:
short: Return a shortened output.
"""
lines = ["qutebrowser v{}".format(qutebrowser.__version__)]
gitver = _git_str()
if gitver is not None:
@ -197,16 +202,23 @@ def version():
'PyQt: {}'.format(PYQT_VERSION_STR),
]
lines += _module_versions()
if not short:
style = QApplication.instance().style()
lines += [
'Style: {}'.format(style.metaObject().className()),
'Desktop: {}'.format(os.environ.get('DESKTOP_SESSION')),
]
lines += [
'Webkit: {}'.format(qWebKitVersion()),
'Harfbuzz: {}'.format(os.environ.get('QT_HARFBUZZ', 'system')),
'SSL: {}'.format(QSslSocket.sslLibraryVersionString()),
'',
'Frozen: {}'.format(hasattr(sys, 'frozen')),
'Platform: {}, {}'.format(platform.platform(),
platform.architecture()[0]),
]
lines += _os_info()
lines += _module_versions()
lines += [
'Webkit: {}'.format(qWebKitVersion()),
'Harfbuzz: {}'.format(os.environ.get('QT_HARFBUZZ', 'system')),
'SSL: {}'.format(QSslSocket.sslLibraryVersionString()),
'',
'Frozen: {}'.format(hasattr(sys, 'frozen')),
'Platform: {}, {}'.format(platform.platform(),
platform.architecture()[0]),
]
lines += _os_info()
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))
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):
"""Common buildsteps used for all OS'."""
utils.print_title("Running asciidoc2html.py")
@ -64,22 +78,33 @@ def _maybe_remove(path):
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():
"""Build windows executables/setups."""
parts = str(sys.version_info.major), str(sys.version_info.minor)
ver = ''.join(parts)
dotver = '.'.join(parts)
python_x86 = r'C:\Python{}_x32\python.exe'.format(ver)
python_x64 = r'C:\Python{}\python.exe'.format(ver)
python_x86 = r'C:\Python{}_x32'.format(ver)
python_x64 = r'C:\Python{}'.format(ver)
utils.print_title("Running 32bit freeze.py build_exe")
call_script('freeze.py', 'build_exe', python=python_x86)
utils.print_title("Running 64bit freeze.py build_exe")
call_script('freeze.py', 'build_exe', python=python_x64)
call_freeze('build_exe', python=python_x86)
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")
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')
_maybe_remove(destdir)
@ -126,6 +151,14 @@ def main():
args = parser.parse_args()
utils.change_cwd()
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_windows()
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(),
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()
if egl_path is not None:
build_exe_options['include_files'].append((egl_path, 'libEGL.dll'))
def get_build_exe_options(skip_html=False):
"""Get the options passed as build_exe_options to cx_Freeze.
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 = {
# random GUID generated by uuid.uuid4()
@ -92,19 +113,21 @@ executable = cx.Executable('qutebrowser/__main__.py', base=base,
icon=os.path.join(BASEDIR, 'icons',
'qutebrowser.ico'))
try:
setupcommon.write_git_file()
cx.setup(
executables=[executable],
options={
'build_exe': build_exe_options,
'bdist_msi': bdist_msi_options,
'bdist_mac': bdist_mac_options,
'bdist_dmg': bdist_dmg_options,
},
**setupcommon.setupdata
)
finally:
path = os.path.join(BASEDIR, 'qutebrowser', 'git-commit-id')
if os.path.exists(path):
os.remove(path)
if __name__ == '__main__':
try:
setupcommon.write_git_file()
cx.setup(
executables=[executable],
options={
'build_exe': get_build_exe_options(),
'bdist_msi': bdist_msi_options,
'bdist_mac': bdist_mac_options,
'bdist_dmg': bdist_dmg_options,
},
**setupcommon.setupdata
)
finally:
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):
"""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)
@ -112,6 +113,7 @@ def copy_or_link(source, dest):
"""Copy or symlink source to dest."""
if os.name == 'nt':
if os.path.isdir(source):
print('{} -> {}'.format(source, dest))
shutil.copytree(source, dest, ignore=get_ignored_files,
copy_function=verbose_copy)
else:
@ -138,7 +140,10 @@ def get_python_lib(executable, venv=False):
treatments for Windows/Ubuntu shouldn't take place.
"""
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
# Windows when running via tox, and sys.prefix is empty too...
return os.path.join(os.path.dirname(executable), '..', 'Lib',

View File

@ -0,0 +1,33 @@
#!/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/>.
# pylint: disable=import-error,no-member
"""cx_Freeze script to run qutebrowser tests on the frozen executable."""
import sys
import pytest
import pytestqt.plugin
import pytest_mock
import pytest_capturelog
sys.exit(pytest.main(plugins=[pytestqt.plugin, pytest_mock,
pytest_capturelog]))

View File

@ -70,20 +70,20 @@ def main():
if len(sys.argv) < 2:
# pages which previously caused problems
pages = [
# ANGLE, https://bugreports.qt-project.org/browse/QTBUG-39723
# ANGLE, https://bugreports.qt.io/browse/QTBUG-39723
('http://www.binpress.com/', False),
('http://david.li/flow/', False),
('https://imzdl.com/', False),
# not reproducible
# https://bugreports.qt-project.org/browse/QTBUG-39847
# https://bugreports.qt.io/browse/QTBUG-39847
('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://phoronix.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),
# 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),
]
else:

View File

@ -37,7 +37,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
import qutebrowser.app
from scripts import asciidoc2html, utils
from qutebrowser import qutebrowser
from qutebrowser.commands import cmdutils
from qutebrowser.commands import cmdutils, command
from qutebrowser.config import configdata
from qutebrowser.utils import docutils
@ -54,6 +54,14 @@ class UsageFormatter(argparse.HelpFormatter):
"""Override _format_usage to not add the 'usage:' prefix."""
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):
"""Override _metavar_formatter to add asciidoc markup to metavars.

View File

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

View File

@ -1881,6 +1881,11 @@ class TestSearchEngineUrl:
with pytest.raises(configexc.ValidationError):
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):
"""Test transform with an empty value."""
assert self.t.transform('') is None

View File

@ -28,7 +28,7 @@ import jinja2
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtWebKitWidgets import QWebView, QWebPage
import qutebrowser
from qutebrowser.utils import utils
class TestWebPage(QWebPage):
@ -107,11 +107,7 @@ class JSTester:
Return:
The javascript return value.
"""
base_path = os.path.join(os.path.dirname(qutebrowser.__file__),
'javascript')
full_path = os.path.join(base_path, filename)
with open(full_path, 'r', encoding='utf-8') as f:
source = f.read()
source = utils.read_file(os.path.join('javascript', filename))
return self.run(source)
def run(self, source):

View File

@ -39,7 +39,6 @@ def progress_widget(qtbot, monkeypatch, config_stub):
'qutebrowser.mainwindow.statusbar.progress.style.config', config_stub)
widget = Progress()
qtbot.add_widget(widget)
widget.setGeometry(200, 200, 200, 200)
assert not widget.isVisible()
assert not widget.isTextVisible()
return widget

View File

@ -42,8 +42,8 @@ class TestArg:
@pytest.yield_fixture(autouse=True)
def setup(self, monkeypatch, stubs):
monkeypatch.setattr('qutebrowser.misc.editor.QProcess',
stubs.FakeQProcess())
monkeypatch.setattr('qutebrowser.misc.editor.guiprocess.QProcess',
stubs.fake_qprocess())
self.editor = editor.ExternalEditor(0)
yield
self.editor._cleanup() # pylint: disable=protected-access
@ -60,7 +60,7 @@ class TestArg:
stubbed_config.data = {
'general': {'editor': ['bin'], 'editor-encoding': 'utf-8'}}
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):
"""Test starting editor with static arguments."""
@ -68,7 +68,7 @@ class TestArg:
'general': {'editor': ['bin', 'foo', 'bar'],
'editor-encoding': 'utf-8'}}
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):
"""Test starting editor with placeholder argument."""
@ -77,7 +77,7 @@ class TestArg:
'editor-encoding': 'utf-8'}}
self.editor.edit("")
filename = self.editor._filename
self.editor._proc.start.assert_called_with(
self.editor._proc._proc.start.assert_called_with(
"bin", ["foo", filename, "bar"])
def test_in_arg_placeholder(self, stubbed_config):
@ -86,7 +86,7 @@ class TestArg:
'general': {'editor': ['bin', 'foo{}bar'],
'editor-encoding': 'utf-8'}}
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:
@ -101,8 +101,8 @@ class TestFileHandling:
def setup(self, monkeypatch, stubs, config_stub):
monkeypatch.setattr('qutebrowser.misc.editor.message',
stubs.MessageModule())
monkeypatch.setattr('qutebrowser.misc.editor.QProcess',
stubs.FakeQProcess())
monkeypatch.setattr('qutebrowser.misc.editor.guiprocess.QProcess',
stubs.fake_qprocess())
config_stub.data = {'general': {'editor': [''],
'editor-encoding': 'utf-8'}}
monkeypatch.setattr('qutebrowser.misc.editor.config', config_stub)
@ -113,7 +113,7 @@ class TestFileHandling:
self.editor.edit("")
filename = self.editor._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)
def test_file_handling_closed_error(self, caplog):
@ -122,7 +122,7 @@ class TestFileHandling:
filename = self.editor._filename
assert os.path.exists(filename)
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 not os.path.exists(filename)
@ -132,9 +132,9 @@ class TestFileHandling:
filename = self.editor._filename
assert os.path.exists(filename)
with caplog.atLevel(logging.ERROR):
self.editor.on_proc_error(QProcess.Crashed)
self.editor._proc.error.emit(QProcess.Crashed)
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)
@ -148,8 +148,8 @@ class TestModifyTests:
@pytest.fixture(autouse=True)
def setup(self, monkeypatch, stubs, config_stub):
monkeypatch.setattr('qutebrowser.misc.editor.QProcess',
stubs.FakeQProcess())
monkeypatch.setattr('qutebrowser.misc.editor.guiprocess.QProcess',
stubs.fake_qprocess())
config_stub.data = {'general': {'editor': [''],
'editor-encoding': 'utf-8'}}
monkeypatch.setattr('qutebrowser.misc.editor.config', config_stub)
@ -182,7 +182,7 @@ class TestModifyTests:
self.editor.edit("")
assert self._read() == ""
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")
def test_simple_input(self):
@ -190,7 +190,7 @@ class TestModifyTests:
self.editor.edit("Hello")
assert self._read() == "Hello"
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")
def test_umlaut(self):
@ -198,7 +198,7 @@ class TestModifyTests:
self.editor.edit("Hällö Wörld")
assert self._read() == "Hällö Wörld"
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")
def test_unicode(self):
@ -220,8 +220,8 @@ class TestErrorMessage:
@pytest.yield_fixture(autouse=True)
def setup(self, monkeypatch, stubs, config_stub):
monkeypatch.setattr('qutebrowser.misc.editor.QProcess',
stubs.FakeQProcess())
monkeypatch.setattr('qutebrowser.misc.editor.guiprocess.QProcess',
stubs.fake_qprocess())
monkeypatch.setattr('qutebrowser.misc.editor.message',
stubs.MessageModule())
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.QtNetwork import QNetworkRequest
from PyQt5.QtWidgets import QCommonStyle
class FakeKeyEvent:
@ -72,8 +73,10 @@ class FakeQApplication:
"""Stub to insert as QApplication module."""
def __init__(self):
def __init__(self, style=None):
self.instance = mock.Mock(return_value=self)
self.style = mock.Mock(spec=QCommonStyle)
self.style().metaObject().className.return_value = style
class FakeUrl:
@ -147,22 +150,13 @@ class FakeNetworkReply:
self.headers[key] = value
class FakeQProcess(mock.Mock):
"""QProcess stub.
Gets some enum values from the real QProcess.
"""
NormalExit = QProcess.NormalExit
CrashExit = QProcess.CrashExit
FailedToStart = QProcess.FailedToStart
Crashed = QProcess.Crashed
Timedout = QProcess.Timedout
WriteError = QProcess.WriteError
ReadError = QProcess.ReadError
UnknownError = QProcess.UnknownError
def fake_qprocess():
"""Factory for a QProcess mock which has the QProcess enum values."""
m = mock.Mock(spec=QProcess)
for attr in ['NormalExit', 'CrashExit', 'FailedToStart', 'Crashed',
'Timedout', 'WriteError', 'ReadError', 'UnknownError']:
setattr(m, attr, getattr(QProcess, attr))
return m
class FakeSignal:
@ -267,14 +261,16 @@ class MessageModule:
"""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."""
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."""
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."""
logging.getLogger('message').info(message)

View File

@ -19,15 +19,51 @@
"""Tests for qutebrowser.utils.qtutils."""
import io
import os
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 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.utils import qtutils
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:
"""Test check_overflow."""
@ -68,20 +104,18 @@ class TestGetQtArgs:
mocker.patch.object(parser, 'exit', side_effect=Exception)
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."""
args = parser.parse_args(['--debug'])
assert qtutils.get_args(args) == [sys.argv[0]]
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']
parsed = parser.parse_args(args)
assert qtutils.get_args(parsed) == expected
def test_qt_both(self, parser):
"""Test commandline with a Qt argument and flag."""
@ -91,3 +125,838 @@ class TestGetQtArgs:
assert '-reverse' in qt_args
assert '-stylesheet' 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
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# pylint: disable=protected-access
"""Tests for qutebrowser.utils.standarddir."""
import os
@ -24,7 +26,10 @@ import os.path
import sys
import types
import collections
import logging
import textwrap
from PyQt5.QtCore import QStandardPaths
from PyQt5.QtWidgets import QApplication
import pytest
@ -44,8 +49,61 @@ def change_qapp_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"),
reason="requires Linux")
@pytest.mark.usefixtures('no_cachedir_tag')
class TestGetStandardDirLinux:
"""Tests for standarddir under Linux."""
@ -53,26 +111,22 @@ class TestGetStandardDirLinux:
def test_data_explicit(self, monkeypatch, tmpdir):
"""Test data dir with XDG_DATA_HOME explicitly set."""
monkeypatch.setenv('XDG_DATA_HOME', str(tmpdir))
standarddir.init(None)
assert standarddir.data() == str(tmpdir / 'qutebrowser_test')
def test_config_explicit(self, monkeypatch, tmpdir):
"""Test config dir with XDG_CONFIG_HOME explicitly set."""
monkeypatch.setenv('XDG_CONFIG_HOME', str(tmpdir))
standarddir.init(None)
assert standarddir.config() == str(tmpdir / 'qutebrowser_test')
def test_cache_explicit(self, monkeypatch, tmpdir):
"""Test cache dir with XDG_CACHE_HOME explicitly set."""
monkeypatch.setenv('XDG_CACHE_HOME', str(tmpdir))
standarddir.init(None)
assert standarddir.cache() == str(tmpdir / 'qutebrowser_test')
def test_data(self, monkeypatch, tmpdir):
"""Test data dir with XDG_DATA_HOME not set."""
monkeypatch.setenv('HOME', str(tmpdir))
monkeypatch.delenv('XDG_DATA_HOME', raising=False)
standarddir.init(None)
expected = tmpdir / '.local' / 'share' / 'qutebrowser_test'
assert standarddir.data() == str(expected)
@ -80,7 +134,6 @@ class TestGetStandardDirLinux:
"""Test config dir with XDG_CONFIG_HOME not set."""
monkeypatch.setenv('HOME', str(tmpdir))
monkeypatch.delenv('XDG_CONFIG_HOME', raising=False)
standarddir.init(None)
expected = tmpdir / '.config' / 'qutebrowser_test'
assert standarddir.config() == str(expected)
@ -88,21 +141,17 @@ class TestGetStandardDirLinux:
"""Test cache dir with XDG_CACHE_HOME not set."""
monkeypatch.setenv('HOME', str(tmpdir))
monkeypatch.delenv('XDG_CACHE_HOME', raising=False)
standarddir.init(None)
expected = tmpdir / '.cache' / 'qutebrowser_test'
assert standarddir.cache() == expected
@pytest.mark.skipif(not sys.platform.startswith("win"),
reason="requires Windows")
@pytest.mark.usefixtures('no_cachedir_tag')
class TestGetStandardDirWindows:
"""Tests for standarddir under Windows."""
@pytest.fixture(autouse=True)
def reset_standarddir(self):
standarddir.init(None)
def test_data(self):
"""Test data dir."""
expected = ['qutebrowser_test', 'data']
@ -121,6 +170,7 @@ class TestGetStandardDirWindows:
DirArgTest = collections.namedtuple('DirArgTest', 'arg, expected')
@pytest.mark.usefixtures('no_cachedir_tag')
class TestArguments:
"""Tests with confdir/cachedir/datadir arguments."""
@ -131,6 +181,7 @@ class TestArguments:
if request.param.expected is None:
return request.param
else:
# prepend tmpdir to both
arg = str(tmpdir / request.param.arg)
return DirArgTest(arg, arg)
@ -155,6 +206,21 @@ class TestArguments:
standarddir.init(args)
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',
'runtime'])
def test_basedir(self, tmpdir, typ):
@ -164,3 +230,51 @@ class TestArguments:
standarddir.init(args)
func = getattr(standarddir, typ)
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 datetime
import os.path
import io
import logging
import functools
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor
import pytest
import qutebrowser
import qutebrowser.utils # for test_qualname
from qutebrowser.utils import utils, qtutils
ELLIPSIS = '\u2026'
class Color(QColor):
"""A QColor with a nicer repr()."""
@ -41,39 +49,196 @@ class Color(QColor):
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:
"""Test elide."""
ELLIPSIS = '\u2026'
def test_too_small(self):
"""Test eliding to 0 chars which should fail."""
with pytest.raises(ValueError):
utils.elide('foo', 0)
def test_length_one(self):
"""Test eliding to 1 char which should yield ..."""
assert utils.elide('foo', 1) == self.ELLIPSIS
def test_fits(self):
"""Test eliding with a string which fits exactly."""
assert utils.elide('foo', 3) == 'foo'
def test_elided(self):
"""Test eliding with a string which should get elided."""
assert utils.elide('foobar', 3) == 'fo' + self.ELLIPSIS
@pytest.mark.parametrize('text, length, expected', [
('foo', 1, ELLIPSIS),
('foo', 3, 'foo'),
('foobar', 3, 'fo' + ELLIPSIS),
])
def test_elided(self, text, length, expected):
assert utils.elide(text, length) == expected
class TestReadFile:
"""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):
"""Read a test file."""
content = utils.read_file(os.path.join('utils', 'testfile'))
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:
@ -164,62 +329,40 @@ class TestInterpolateColor:
assert Color(color) == expected
class TestFormatSeconds:
"""Tests for format_seconds.
Class attributes:
TESTS: A list of (input, output) tuples.
"""
TESTS = [
(-1, '-0:01'),
(0, '0:00'),
(59, '0:59'),
(60, '1:00'),
(60.4, '1:00'),
(61, '1:01'),
(-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
@pytest.mark.parametrize('seconds, out', [
(-1, '-0:01'),
(0, '0:00'),
(59, '0:59'),
(60, '1:00'),
(60.4, '1:00'),
(61, '1:01'),
(-61, '-1:01'),
(3599, '59:59'),
(3600, '1:00:00'),
(3601, '1:00:01'),
(36000, '10:00:00'),
])
def test_format_seconds(seconds, out):
assert utils.format_seconds(seconds) == out
class TestFormatTimedelta:
"""Tests for format_timedelta.
Class attributes:
TESTS: A list of (input, output) tuples.
"""
TESTS = [
(datetime.timedelta(seconds=-1), '-1s'),
(datetime.timedelta(seconds=0), '0s'),
(datetime.timedelta(seconds=59), '59s'),
(datetime.timedelta(seconds=120), '2m'),
(datetime.timedelta(seconds=60.4), '1m'),
(datetime.timedelta(seconds=63), '1m 3s'),
(datetime.timedelta(seconds=-64), '-1m 4s'),
(datetime.timedelta(seconds=3599), '59m 59s'),
(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
@pytest.mark.parametrize('td, out', [
(datetime.timedelta(seconds=-1), '-1s'),
(datetime.timedelta(seconds=0), '0s'),
(datetime.timedelta(seconds=59), '59s'),
(datetime.timedelta(seconds=120), '2m'),
(datetime.timedelta(seconds=60.4), '1m'),
(datetime.timedelta(seconds=63), '1m 3s'),
(datetime.timedelta(seconds=-64), '-1m 4s'),
(datetime.timedelta(seconds=3599), '59m 59s'),
(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'),
])
def test_format_timedelta(td, out):
assert utils.format_timedelta(td) == out
class TestFormatSize:
@ -264,29 +407,34 @@ class TestKeyToString:
"""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."""
assert utils.key_to_string(Qt.Key_Blue) == 'Blue'
assert utils.key_to_string(key) == expected
def test_backtab(self):
"""Test if backtab is normalized to tab correctly."""
assert utils.key_to_string(Qt.Key_Backtab) == 'Tab'
def test_escape(self):
"""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."""
def test_missing(self, monkeypatch):
"""Test with a missing key."""
monkeypatch.delattr('qutebrowser.utils.utils.Qt.Key_Blue')
# We don't want to test the key which is actually missing - we only
# want to know if the mapping still behaves properly.
assert utils.key_to_string(Qt.Key_A) == 'A'
def test_unicode(self):
"""Test a printable unicode key."""
assert utils.key_to_string(Qt.Key_degree) == '°'
def test_special(self):
"""Test a non-printable key handled by QKeyEvent::toString."""
assert utils.key_to_string(Qt.Key_F1) == 'F1'
def test_all(self):
"""Make sure there's some sensible output for all keys."""
for name, value in sorted(vars(Qt).items()):
if not isinstance(value, Qt.Key):
continue
print(name)
string = utils.key_to_string(value)
assert string
string.encode('utf-8') # make sure it's encodable
class TestKeyEventToString:
@ -323,26 +471,275 @@ class TestKeyEventToString:
Qt.MetaModifier | Qt.ShiftModifier))
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)
def test_normalize(self, orig, repl):
"""Test normalize with some strings."""
assert utils.normalize_keystr(orig) == repl
class TestFakeIOStream:
"""Test FakeIOStream."""
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:
@ -412,20 +809,13 @@ class TestRaises:
utils.raises(ValueError, self.do_raise)
class TestForceEncoding:
"""Test force_encoding."""
TESTS = [
('hello world', 'ascii', 'hello world'),
('hellö wörld', 'utf-8', 'hellö wörld'),
('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
@pytest.mark.parametrize('inp, enc, expected', [
('hello world', 'ascii', 'hello world'),
('hellö wörld', 'utf-8', 'hellö wörld'),
('hellö wörld', 'ascii', 'hell? w?rld'),
])
def test_force_encoding(inp, enc, expected):
assert utils.force_encoding(inp, enc) == expected
class TestNewestSlice:
@ -437,44 +827,23 @@ class TestNewestSlice:
with pytest.raises(ValueError):
utils.newest_slice([], -2)
def test_count_minus_one(self):
"""Test with a count of -1 (all elements)."""
items = range(20)
sliced = utils.newest_slice(items, -1)
assert list(sliced) == list(items)
def test_count_zero(self):
"""Test with a count of 0 (no elements)."""
items = range(20)
sliced = utils.newest_slice(items, 0)
assert list(sliced) == []
def test_count_much_smaller(self):
"""Test with a count which is much smaller than the iterable."""
items = range(20)
sliced = utils.newest_slice(items, 5)
assert list(sliced) == [15, 16, 17, 18, 19]
def test_count_smaller(self):
"""Test with a count which is exactly one smaller."""
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)
@pytest.mark.parametrize('items, count, expected', [
# Count of -1 (all elements).
(range(20), -1, range(20)),
# Count of 0 (no elements).
(range(20), 0, []),
# Count which is much smaller than the iterable.
(range(20), 5, [15, 16, 17, 18, 19]),
# Count which is exactly one smaller."""
(range(5), 4, [1, 2, 3, 4]),
# Count which is just as large as the iterable."""
(range(5), 5, range(5)),
# Count which is one bigger than the iterable.
(range(5), 6, range(5)),
# Count which is much bigger than the iterable.
(range(5), 50, range(5)),
])
def test_good(self, items, count, expected):
"""Test slices which shouldn't raise an exception."""
sliced = utils.newest_slice(items, count)
assert list(sliced) == list(expected)

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

100
tox.ini
View File

@ -4,9 +4,10 @@
# and then run "tox" from this directory.
[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]
passenv = PYTHON
basepython = python3
[testenv:mkvenv]
@ -17,30 +18,40 @@ usedevelop = true
[testenv:unittests]
# 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
passenv = DISPLAY XAUTHORITY HOME
passenv = PYTHON DISPLAY XAUTHORITY HOME
deps =
-r{toxinidir}/requirements.txt
py==1.4.28
pytest==2.7.1
py==1.4.30
pytest==2.7.2
pytest-capturelog==0.7
pytest-qt==1.4.0
pytest-mock==0.6.0
pytest-html==1.3.1
# We don't use {[testenv:mkvenv]commands} here because that seems to be broken
# on Ubuntu Trusty.
commands =
{envpython} scripts/link_pyqt.py --tox {envdir}
{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]
passenv = DISPLAY XAUTHORITY HOME
passenv = PYTHON DISPLAY XAUTHORITY HOME
deps =
{[testenv:unittests]deps}
coverage==3.7.1
pytest-cov==1.8.1
cov-core==1.15.0
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}
[testenv:misc]
@ -60,14 +71,14 @@ deps =
logilab-common==0.63.2
six==1.9.0
commands =
{[testenv:mkvenv]commands}
{envdir}/bin/pylint scripts qutebrowser --rcfile=.pylintrc --output-format=colorized --reports=no --expected-line-ending-format=LF
{envpython} scripts/link_pyqt.py --tox {envdir}
{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 --expected-line-ending-format=LF
[testenv:pep257]
skip_install = true
deps = pep257==0.5.0
passenv = LANG
passenv = PYTHON LANG
# Disabled checks:
# D102: Docstring missing, will be handled by others
# 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
deps =
-r{toxinidir}/requirements.txt
py==1.4.28
pytest==2.7.1
pyflakes==0.9.0
py==1.4.30
pytest==2.7.2
pyflakes==0.9.2
pytest-flakes==1.0.0
commands =
{[testenv:mkvenv]commands}
{envpython} -m py.test -q --flakes -m flakes
{envpython} scripts/link_pyqt.py --tox {envdir}
{envpython} -m py.test -q --flakes --ignore=tests
[testenv:pep8]
deps =
-r{toxinidir}/requirements.txt
py==1.4.28
pytest==2.7.1
py==1.4.30
pytest==2.7.2
pep8==1.6.2
pytest-pep8==1.0.6
commands =
{[testenv:mkvenv]commands}
{envpython} -m py.test -q --pep8 -m pep8
{envpython} scripts/link_pyqt.py --tox {envdir}
{envpython} -m py.test -q --pep8 --ignore=tests
[testenv:mccabe]
deps =
-r{toxinidir}/requirements.txt
py==1.4.28
pytest==2.7.1
mccabe==0.3
py==1.4.30
pytest==2.7.2
mccabe==0.3.1
pytest-mccabe==0.1
commands =
{[testenv:mkvenv]commands}
{envpython} -m py.test -q --mccabe -m mccabe
{envpython} scripts/link_pyqt.py --tox {envdir}
{envpython} -m py.test -q --mccabe --ignore=tests
[testenv:pyroma]
skip_install = true
deps =
pyroma==1.8.1
pyroma==1.8.2
docutils==0.12
commands =
{[testenv:mkvenv]commands}
{envpython} scripts/link_pyqt.py --tox {envdir}
{envdir}/bin/pyroma .
[testenv:check-manifest]
@ -123,7 +134,7 @@ skip_install = true
deps =
check-manifest==0.25
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__'
[testenv:docs]
@ -132,7 +143,7 @@ whitelist_externals = git
deps =
-r{toxinidir}/requirements.txt
commands =
{[testenv:mkvenv]commands}
{envpython} scripts/link_pyqt.py --tox {envdir}
{envpython} scripts/src2asciidoc.py
git --no-pager diff --exit-code --stat
{envpython} scripts/asciidoc2html.py {posargs}
@ -140,15 +151,35 @@ commands =
[testenv:smoke]
# 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
passenv = DISPLAY XAUTHORITY HOME USERNAME USER
passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER
deps =
-r{toxinidir}/requirements.txt
# We don't use {[testenv:mkvenv]commands} here because that seems to be broken
# on Ubuntu Trusty.
commands =
{envpython} scripts/link_pyqt.py --tox {envdir}
{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]
norecursedirs = .tox .venv
markers =
@ -166,4 +197,7 @@ pep8ignore =
resources.py ALL
mccabe-complexity = 12
qt_log_level_fail = WARNING
qt_log_ignore = ^SpellCheck: .*
qt_log_ignore =
^SpellCheck: .*
^SetProcessDpiAwareness failed: .*
^QWindowsWindow::setGeometryDp: Unable to set geometry .*