Merge branch 'master' into spell

This commit is contained in:
Michał Siedlaczek 2017-10-04 09:47:42 -04:00 committed by GitHub
commit e20ad95666
111 changed files with 3641 additions and 1956 deletions

View File

@ -27,6 +27,7 @@ exclude scripts/asciidoc2html.py
exclude doc/notes
recursive-exclude doc *.asciidoc
include doc/qutebrowser.1.asciidoc
include doc/changelog.asciidoc
prune tests
prune qutebrowser/3rdparty
prune misc/requirements

View File

@ -11,7 +11,6 @@ image:icons/qutebrowser-64x64.png[qutebrowser logo] *A keyboard-driven, vim-like
image:https://img.shields.io/pypi/l/qutebrowser.svg?style=flat["license badge",link="https://github.com/qutebrowser/qutebrowser/blob/master/LICENSE"]
image:https://img.shields.io/pypi/v/qutebrowser.svg?style=flat["version badge",link="https://pypi.python.org/pypi/qutebrowser/"]
image:https://requires.io/github/qutebrowser/qutebrowser/requirements.svg?branch=master["requirements badge",link="https://requires.io/github/qutebrowser/qutebrowser/requirements/?branch=master"]
image:https://travis-ci.org/qutebrowser/qutebrowser.svg?branch=master["Build Status", link="https://travis-ci.org/qutebrowser/qutebrowser"]
image:https://ci.appveyor.com/api/projects/status/5pyauww2k68bbow2/branch/master?svg=true["AppVeyor build status", link="https://ci.appveyor.com/project/qutebrowser/qutebrowser"]
image:https://codecov.io/github/qutebrowser/qutebrowser/coverage.svg?branch=master["coverage badge",link="https://codecov.io/github/qutebrowser/qutebrowser?branch=master"]

View File

@ -39,7 +39,10 @@ Breaking changes
v0.9.0) is now not supported anymore.
- Upgrading qutebrowser with a version older than v0.4.0 still running now won't
work properly anymore.
- The `--harfbuzz` commandline argument got dropped.
- The `--harfbuzz` and `--relaxed-config` commandline arguments got dropped.
- `:set` now doesn't support toggling/cycling values anymore, that functionality
got moved to `:config-cycle`.
- `:scroll-perc` got renamed to `:scroll-to-perc`.
Major changes
~~~~~~~~~~~~~
@ -57,6 +60,14 @@ Added
- New `backend` setting to select the backend to use (auto/webengine/webkit).
Together with the previous setting, this should make wrapper scripts
unnecessary.
- Proxy authentication is now supported with the QtWebEngine backend.
- New config commands:
- `:config-cycle` to cycle an option between multiple values.
- `:config-unset` to remove a configured option
- `:config-clear` to remove all configured options
- `:config-source` to (re-)read a `config.py` file
- `:config-edit` to open the `config.py` file in an editor
- New `:version` command which opens `qute://version`.
Changed
~~~~~~~
@ -66,12 +77,20 @@ Changed
- When there are multiple messages shown, the timeout is increased.
- `:search` now only clears the search if one was displayed before, so pressing
`<Escape>` doesn't un-focus inputs anymore.
- Pinned tabs now adjust to their text's width, so the `tabs.width.pinned`
setting got removed.
- `:set-cmd-text` now has a `--run-on-count` argument to run the underlying
command directly if a count was given.
Fixes
~~~~~
- Exiting fullscreen via `:fullscreen` or buttons on a page now
restores the correct previous window state (maximized/fullscreen).
- When `input.insert_mode.auto_load` is set, background tabs now don't enter
insert mode anymore.
- The keybinding help widget now works correctly when using keybindings with a
count.
v0.11.1 (unreleased)
--------------------

View File

@ -31,6 +31,11 @@ It is possible to run or bind multiple commands by separating them with `;;`.
|<<bookmark-load,bookmark-load>>|Load a bookmark.
|<<buffer,buffer>>|Select tab by index or url/title best match.
|<<close,close>>|Close the current window.
|<<config-clear,config-clear>>|Set all settings back to their default.
|<<config-cycle,config-cycle>>|Cycle an option between multiple values.
|<<config-edit,config-edit>>|Open the config.py file in the editor.
|<<config-source,config-source>>|Read a config.py file.
|<<config-unset,config-unset>>|Unset an option.
|<<download,download>>|Download a given URL, or current page if no URL given.
|<<download-cancel,download-cancel>>|Cancel the last/[count]th download.
|<<download-clear,download-clear>>|Remove all finished downloads from the list.
@ -86,6 +91,7 @@ It is possible to run or bind multiple commands by separating them with `;;`.
|<<tab-prev,tab-prev>>|Switch to the previous tab, or switch [count] tabs back.
|<<unbind,unbind>>|Unbind a keychain.
|<<undo,undo>>|Re-open a closed tab.
|<<version,version>>|Show version information.
|<<view-source,view-source>>|Show the source of the current page in a new tab.
|<<window-only,window-only>>|Close all windows except for the current one.
|<<yank,yank>>|Yank something to the clipboard or primary selection.
@ -115,7 +121,7 @@ How many pages to go back.
[[bind]]
=== bind
Syntax: +:bind [*--mode* 'mode'] [*--force*] 'key' ['command']+
Syntax: +:bind [*--mode* 'mode'] 'key' ['command']+
Bind a key to a command.
@ -128,7 +134,6 @@ Bind a key to a command.
* +*-m*+, +*--mode*+: A comma-separated list of modes to bind the key in (default: `normal`). See `:help bindings.commands` for the
available modes.
* +*-f*+, +*--force*+: Rebind the key if it is already bound.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
@ -184,20 +189,83 @@ Load a bookmark.
[[buffer]]
=== buffer
Syntax: +:buffer 'index'+
Syntax: +:buffer ['index']+
Select tab by index or url/title best match.
Focuses window if necessary.
Focuses window if necessary when index is given. If both index and count are given, use count.
==== positional arguments
* +'index'+: The [win_id/]index of the tab to focus. Or a substring in which case the closest match will be focused.
==== count
The tab index to focus, starting with 1.
[[close]]
=== close
Close the current window.
[[config-clear]]
=== config-clear
Syntax: +:config-clear [*--save*]+
Set all settings back to their default.
==== optional arguments
* +*-s*+, +*--save*+: If given, all configuration in autoconfig.yml is also removed.
[[config-cycle]]
=== config-cycle
Syntax: +:config-cycle [*--temp*] [*--print*] 'option' ['values' ['values' ...]]+
Cycle an option between multiple values.
==== positional arguments
* +'option'+: The name of the option.
* +'values'+: The values to cycle through.
==== optional arguments
* +*-t*+, +*--temp*+: Set value temporarily until qutebrowser is closed.
* +*-p*+, +*--print*+: Print the value after setting.
[[config-edit]]
=== config-edit
Syntax: +:config-edit [*--no-source*]+
Open the config.py file in the editor.
==== optional arguments
* +*-n*+, +*--no-source*+: Don't re-source the config file after editing.
[[config-source]]
=== config-source
Syntax: +:config-source [*--clear*] ['filename']+
Read a config.py file.
==== positional arguments
* +'filename'+: The file to load. If not given, loads the default config.py.
==== optional arguments
* +*-c*+, +*--clear*+: Clear current settings first.
[[config-unset]]
=== config-unset
Syntax: +:config-unset [*--temp*] 'option'+
Unset an option.
This sets an option back to its default and removes it from autoconfig.yml.
==== positional arguments
* +'option'+: The name of the option.
==== optional arguments
* +*-t*+, +*--temp*+: Don't touch autoconfig.yml.
[[download]]
=== download
Syntax: +:download [*--mhtml*] [*--dest* 'dest'] ['url'] ['dest-old']+
@ -773,15 +841,15 @@ Save a session.
[[set]]
=== set
Syntax: +:set [*--temp*] [*--print*] ['option'] ['values' ['values' ...]]+
Syntax: +:set [*--temp*] [*--print*] ['option'] ['value']+
Set an option.
If the option name ends with '?', the value of the option is shown instead. If the option name ends with '!' and it is a boolean value, toggle it.
If the option name ends with '?', the value of the option is shown instead.
==== positional arguments
* +'option'+: The name of the option.
* +'values'+: The value to set, or the values to cycle through.
* +'value'+: The value to set.
==== optional arguments
* +*-t*+, +*--temp*+: Set value temporarily until qutebrowser is closed.
@ -789,7 +857,7 @@ If the option name ends with '?', the value of the option is shown instead. If t
[[set-cmd-text]]
=== set-cmd-text
Syntax: +:set-cmd-text [*--space*] [*--append*] 'text'+
Syntax: +:set-cmd-text [*--space*] [*--append*] [*--run-on-count*] 'text'+
Preset the statusbar to some text.
@ -799,6 +867,11 @@ Preset the statusbar to some text.
==== optional arguments
* +*-s*+, +*--space*+: If given, a space is added to the end.
* +*-a*+, +*--append*+: If given, the text is appended to the current text.
* +*-r*+, +*--run-on-count*+: If given with a count, the command is run with the given count rather than setting the command text.
==== count
The count if given.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
@ -919,7 +992,7 @@ Close all tabs except for the current one.
=== tab-pin
Pin/Unpin the current/[count]th tab.
Pinning a tab shrinks it to `tabs.width.pinned` size. Attempting to close a pinned tab will cause a confirmation, unless --force is passed.
Pinning a tab shrinks it to the size of its title text. Attempting to close a pinned tab will cause a confirmation, unless --force is passed.
==== count
The tab index to pin or unpin
@ -948,6 +1021,10 @@ Unbind a keychain.
=== undo
Re-open a closed tab.
[[version]]
=== version
Show version information.
[[view-source]]
=== view-source
Show the source of the current page in a new tab.
@ -1068,8 +1145,8 @@ How many steps to zoom out.
|<<run-with-count,run-with-count>>|Run a command with the given count.
|<<scroll,scroll>>|Scroll the current tab in the given direction.
|<<scroll-page,scroll-page>>|Scroll the frame page-wise.
|<<scroll-perc,scroll-perc>>|Scroll to a specific percentage of the page.
|<<scroll-px,scroll-px>>|Scroll the current tab by 'count * dx/dy' pixels.
|<<scroll-to-perc,scroll-to-perc>>|Scroll to a specific percentage of the page.
|<<search-next,search-next>>|Continue the search to the ([count]th) next term.
|<<search-prev,search-prev>>|Continue the search to the ([count]th) previous term.
|<<set-mark,set-mark>>|Set a mark at the current scroll position in the current tab.
@ -1489,9 +1566,22 @@ Scroll the frame page-wise.
==== count
multiplier
[[scroll-perc]]
=== scroll-perc
Syntax: +:scroll-perc [*--horizontal*] ['perc']+
[[scroll-px]]
=== scroll-px
Syntax: +:scroll-px 'dx' 'dy'+
Scroll the current tab by 'count * dx/dy' pixels.
==== positional arguments
* +'dx'+: How much to scroll in x-direction.
* +'dy'+: How much to scroll in y-direction.
==== count
multiplier
[[scroll-to-perc]]
=== scroll-to-perc
Syntax: +:scroll-to-perc [*--horizontal*] ['perc']+
Scroll to a specific percentage of the page.
@ -1506,19 +1596,6 @@ The percentage can be given either as argument or as count. If no percentage is
==== count
Percentage to scroll.
[[scroll-px]]
=== scroll-px
Syntax: +:scroll-px 'dx' 'dy'+
Scroll the current tab by 'count * dx/dy' pixels.
==== positional arguments
* +'dx'+: How much to scroll in x-direction.
* +'dy'+: How much to scroll in y-direction.
==== count
multiplier
[[search-next]]
=== search-next
Continue the search to the ([count]th) next term.

View File

@ -11,8 +11,9 @@ Migrating older configurations
------------------------------
qutebrowser does no automatic migration for the new configuration. However,
there's a special link:qute://configdiff[] page in qutebrowser, which will show
you the changes you did in your old configuration, compared to the old defaults.
there's a special link:qute://configdiff/old[configdiff] page in qutebrowser,
which will show you the changes you did in your old configuration, compared to
the old defaults.
Other changes in default settings:
@ -57,11 +58,9 @@ To get more help about a setting, use e.g. `:help tabs.position`.
To bind and unbind keys, you can use the link:commands.html#bind[`:bind`] and
link:commands.html#unbind[`:unbind`] commands:
- Binding the key chain "`,`, `v`" to the `:spawn mpv {url}` command:
- Binding the key chain `,v` to the `:spawn mpv {url}` command:
`:bind ,v spawn mpv {url}`
- Unbinding the same key chain: `:unbind ,v`
- Changing an existing binding: `bind --force ,v message-info foo`. Without
`--force`, qutebrowser will show an error because `,v` is already bound.
Key chains starting with a comma are ideal for custom bindings, as the comma key
will never be used in a default keybinding.
@ -88,7 +87,9 @@ Two global objects are pre-defined when running `config.py`: `c` and `config`.
Changing settings
~~~~~~~~~~~~~~~~~
`c` is a shorthand object to easily set settings like this:
While you can set settings using the `config.set()` method (which is explained
in the next section), it's easier to use the `c` shorthand object to easily set
settings like this:
.config.py:
[source,python]
@ -110,7 +111,7 @@ accepted values depend on the type of the option. Commonly used are:
- Dictionaries:
* `c.headers.custom = {'X-Hello': 'World', 'X-Awesome': 'yes'}` to override
any other values in the dictionary.
* `c.aliases['foo'] = ':message-info foo'` to add a single value.
* `c.aliases['foo'] = 'message-info foo'` to add a single value.
- Lists:
* `c.url.start_pages = ["https://www.qutebrowser.org/"]` to override any
previous elements.
@ -136,6 +137,8 @@ If you want to set settings based on their name as a string, use the
.config.py:
[source,python]
----
# Equivalent to:
# c.content.javascript.enabled = False
config.set('content.javascript.enabled', False)
----
@ -143,6 +146,8 @@ To read a setting, use the `config.get` method:
[source,python]
----
# Equivalent to:
# color = c.colors.completion.fg
color = config.get('colors.completion.fg')
----
@ -172,13 +177,6 @@ To bind a key in a mode other than `'normal'`, add a `mode` argument:
config.bind('<Ctrl-y>', 'prompt-yes', mode='prompt')
----
If the key is already bound, `force=True` needs to be given to rebind it:
[source,python]
----
config.bind('<Ctrl-v>', 'message-info foo', force=True)
----
To unbind a key (either a key which has been bound before, or a default binding):
[source,python]
@ -198,17 +196,52 @@ config.bind(',v', 'spawn mpv {url}')
To suppress loading of any default keybindings, you can set
`c.bindings.default = {}`.
Prevent loading `autoconfig.yml`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Loading `autoconfig.yml`
~~~~~~~~~~~~~~~~~~~~~~~~
If you want all customization done via `:set`, `:bind` and `:unbind` to be
temporary, you can suppress loading `autoconfig.yml` in your `config.py` by
doing:
By default, all customization done via `:set`, `:bind` and `:unbind` is
temporary as soon as a `config.py` exists. The settings done that way are always
saved in the `autoconfig.yml` file, but you'll need to explicitly load it in
your `config.py` by doing:
.config.py:
[source,python]
----
config.load_autoconfig = False
config.load_autoconfig()
----
If you do so at the top of your file, your `config.py` settings will take
precedence as they overwrite the settings done in `autoconfig.yml`.
Importing other modules
~~~~~~~~~~~~~~~~~~~~~~~
You can import any module from the
https://docs.python.org/3/library/index.html[Python standard library] (e.g.
`import os.path`), as well as any module installed in the environment
qutebrowser is run with.
If you have an `utils.py` file in your qutebrowser config folder, you can import
that via `import utils` as well.
While it's in some cases possible to import code from the qutebrowser
installation, doing so is unsupported and discouraged.
Getting the config directory
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you need to get the qutebrowser config directory, you can do so by reading
`config.configdir`. Similarily, you can get the qutebrowser data directory via
`config.datadir`.
This gives you a https://docs.python.org/3/library/pathlib.html[`pathlib.Path`
object], on which you can use `/` to add more directory parts, or `str(...)` to
get a string:
.config.py:
[source,python]
----
print(str(config.configdir / 'config.py')
----
Handling errors
@ -221,3 +254,106 @@ qutebrowser tries to display errors which are easy to understand even for people
who are not used to writing Python. If you see a config error which you find
confusing or you think qutebrowser could handle better, please
https://github.com/qutebrowser/qutebrowser/issues[open an issue]!
Recipes
~~~~~~~
Reading a YAML file
^^^^^^^^^^^^^^^^^^^
To read a YAML config like this:
.config.yml:
----
tabs.position: left
tabs.show: switching
----
You can use:
.config.py:
[source,python]
----
import yaml
with (config.configdir / 'config.yml').open() as f:
yaml_data = yaml.load(f)
for k, v in yaml_data.items():
config.set(k, v)
----
Reading a nested YAML file
^^^^^^^^^^^^^^^^^^^^^^^^^^
To read a YAML file with nested values like this:
.colors.yml:
----
colors:
statusbar:
normal:
bg: lime
fg: black
url:
fg: red
----
You can use:
.config.py:
[source,python]
----
import yaml
with (config.configdir / 'colors.yml').open() as f:
yaml_data = yaml.load(f)
def dict_attrs(obj, path=''):
if isinstance(obj, dict):
for k, v in obj.items():
yield from dict_attrs(v, '{}.{}'.format(path, k) if path else k)
else:
yield path, obj
for k, v in dict_attrs(yaml_data):
config.set(k, v)
----
Note that this won't work for values which are dictionaries.
Binding chained commands
^^^^^^^^^^^^^^^^^^^^^^^^
If you have a lot of chained commands you want to bind, you can write a helper
to do so:
[source,python]
----
def bind_chained(key, *commands):
config.bind(key, ' ;; '.join(commands))
bind_chained('<Escape>', 'clear-keychain', 'search')
----
Avoiding flake8 errors
^^^^^^^^^^^^^^^^^^^^^^
If you use an editor with flake8 integration which complains about `c` and `config` being undefined, you can use:
[source,python]
----
c = c # noqa: F821
config = config # noqa: F821
----
For type annotation support (note that those imports aren't guaranteed to be
stable across qutebrowser versions):
[source,python]
----
from qutebrowser.config.configfiles import ConfigAPI # noqa: F401
from qutebrowser.config.config import ConfigContainer # noqa: F401
config = config # type: ConfigAPI # noqa: F821
c = c # type: ConfigContainer # noqa: F821
----

View File

@ -7,7 +7,7 @@ Documentation
The following help pages are currently available:
* link:../quickstart.html[Quick start guide]
* link:../doc.html[Frequently asked questions]
* link:../faq.html[Frequently asked questions]
* link:../changelog.html[Change Log]
* link:commands.html[Documentation of commands]
* link:configuring.html[Configuring qutebrowser]

View File

@ -55,6 +55,7 @@
|<<colors.messages.warning.border,colors.messages.warning.border>>|Border color of an error message.
|<<colors.messages.warning.fg,colors.messages.warning.fg>>|Foreground color a warning message.
|<<colors.prompts.bg,colors.prompts.bg>>|Background color for prompts.
|<<colors.prompts.border,colors.prompts.border>>|Border used around UI elements in prompts.
|<<colors.prompts.fg,colors.prompts.fg>>|Foreground color for prompts.
|<<colors.prompts.selected.bg,colors.prompts.selected.bg>>|Background color for the selected item in filename prompts.
|<<colors.statusbar.caret.bg,colors.statusbar.caret.bg>>|Background color of the statusbar in caret mode.
@ -178,6 +179,7 @@
|<<fonts.web.size.default_fixed,fonts.web.size.default_fixed>>|The default font size for fixed-pitch text.
|<<fonts.web.size.minimum,fonts.web.size.minimum>>|The hard minimum font size.
|<<fonts.web.size.minimum_logical,fonts.web.size.minimum_logical>>|The minimum logical font size that is applied when zooming out.
|<<force_software_rendering,force_software_rendering>>|Force software rendering for QtWebEngine.
|<<hints.auto_follow,hints.auto_follow>>|Controls when a hint can be automatically followed without pressing Enter.
|<<hints.auto_follow_timeout,hints.auto_follow_timeout>>|A timeout (in milliseconds) to ignore normal-mode key bindings after a successful auto-follow.
|<<hints.border,hints.border>>|CSS border value for hints.
@ -201,7 +203,7 @@
|<<input.partial_timeout,input.partial_timeout>>|Timeout (in milliseconds) for partially typed key bindings.
|<<input.rocker_gestures,input.rocker_gestures>>|Enable Opera-like mouse rocker gestures.
|<<input.spatial_navigation,input.spatial_navigation>>|Enable Spatial Navigation.
|<<keyhint.blacklist,keyhint.blacklist>>|Keychains that shouldn\'t be shown in the keyhint dialog.
|<<keyhint.blacklist,keyhint.blacklist>>|Keychains that shouldn't be shown in the keyhint dialog.
|<<keyhint.delay,keyhint.delay>>|Time from pressing a key to seeing the keyhint dialog (ms).
|<<messages.timeout,messages.timeout>>|Time (in ms) to show messages in the statusbar for.
|<<messages.unfocused,messages.unfocused>>|Show messages in unfocused windows.
@ -236,7 +238,6 @@
|<<tabs.title.format_pinned,tabs.title.format_pinned>>|The format to use for the tab title for pinned tabs. The same placeholders like for `tabs.title.format` are defined.
|<<tabs.width.bar,tabs.width.bar>>|The width of the tab bar if it's vertical, in px or as percentage of the window.
|<<tabs.width.indicator,tabs.width.indicator>>|Width of the progress indicator (0 to disable).
|<<tabs.width.pinned,tabs.width.pinned>>|The width for pinned tabs with a horizontal tabbar, in px.
|<<tabs.wrap,tabs.wrap>>|Whether to wrap when changing tabs.
|<<url.auto_search,url.auto_search>>|Whether to start a search when something else than a URL is entered.
|<<url.default_page,url.default_page>>|The page to open if :open -t/-b/-w is used without URL.
@ -284,24 +285,24 @@ Valid values:
* +true+
* +false+
Default: empty
Default: +pass:[false]+
[[backend]]
=== backend
The backend to use to display websites.
qutebrowser supports two different web rendering engines / backends, QtWebKit and QtWebEngine.
QtWebKit is based on WebKit (similar to Safari). It was discontinued by the Qt project with Qt 5.6, but picked up as a well maintained fork: https://github.com/annulen/webkit/wiki - qutebrowser only supports the fork.
QtWebEngine is Qt's official successor to QtWebKit and based on the Chromium project. It's slightly more resource hungry that QtWebKit and has a couple of missing features in qutebrowser, but is generally the preferred choice.
QtWebKit was discontinued by the Qt project with Qt 5.6, but picked up as a well maintained fork: https://github.com/annulen/webkit/wiki - qutebrowser only supports the fork.
QtWebEngine is Qt's official successor to QtWebKit. It's slightly more resource hungry that QtWebKit and has a couple of missing features in qutebrowser, but is generally the preferred choice.
This setting requires a restart.
Type: <<types,String>>
Valid values:
* +auto+: Automatically select either QtWebEngine or QtWebKit
* +webkit+: Force QtWebKit
* +webengine+: Force QtWebEngine
* +webengine+: Use QtWebEngine (based on Chromium)
* +webkit+: Use QtWebKit (based on WebKit, similar to Safari)
Default: +pass:[auto]+
Default: +pass:[webengine]+
[[bindings.commands]]
=== bindings.commands
@ -506,7 +507,7 @@ Default:
* +pass:[B]+: +pass:[set-cmd-text -s :quickmark-load -t]+
* +pass:[D]+: +pass:[tab-close -o]+
* +pass:[F]+: +pass:[hint all tab]+
* +pass:[G]+: +pass:[scroll-perc]+
* +pass:[G]+: +pass:[scroll-to-perc]+
* +pass:[H]+: +pass:[back]+
* +pass:[J]+: +pass:[tab-next]+
* +pass:[K]+: +pass:[tab-prev]+
@ -541,7 +542,7 @@ Default:
* +pass:[gb]+: +pass:[set-cmd-text -s :bookmark-load]+
* +pass:[gd]+: +pass:[download]+
* +pass:[gf]+: +pass:[view-source]+
* +pass:[gg]+: +pass:[scroll-perc 0]+
* +pass:[gg]+: +pass:[scroll-to-perc 0]+
* +pass:[gl]+: +pass:[tab-move -]+
* +pass:[gm]+: +pass:[tab-move]+
* +pass:[go]+: +pass:[set-cmd-text :open {url:pretty}]+
@ -627,6 +628,7 @@ Default:
This setting can be used to map keys to other keys.
When the key used as dictionary-key is pressed, the binding for the key used as dictionary-value is invoked instead.
This is useful for global remappings of keys, for example to map Ctrl-[ to Escape.
Note that when a key is bound (via `bindings.default` or `bindings.commands`), the mapping is ignored.
Type: <<types,Dict>>
@ -966,7 +968,15 @@ Background color for prompts.
Type: <<types,QssColor>>
Default: +pass:[darkblue]+
Default: +pass:[#444444]+
[[colors.prompts.border]]
=== colors.prompts.border
Border used around UI elements in prompts.
Type: <<types,String>>
Default: +pass:[1px solid gray]+
[[colors.prompts.fg]]
=== colors.prompts.fg
@ -982,7 +992,7 @@ Background color for the selected item in filename prompts.
Type: <<types,QssColor>>
Default: +pass:[#308cc6]+
Default: +pass:[grey]+
[[colors.statusbar.caret.bg]]
=== colors.statusbar.caret.bg
@ -1342,7 +1352,7 @@ Valid values:
* +true+
* +false+
Default: empty
Default: +pass:[false]+
[[completion.timestamp_format]]
=== completion.timestamp_format
@ -1402,7 +1412,7 @@ For more information about the feature, please refer to: http://webkit.org/blog/
Type: <<types,Int>>
Default: empty
Default: +pass:[0]+
This setting is only available with the QtWebKit backend.
@ -1466,7 +1476,7 @@ Valid values:
* +true+
* +false+
Default: empty
Default: +pass:[false]+
This setting is only available with the QtWebKit backend.
@ -1497,7 +1507,7 @@ Valid values:
* +true+
* +false+
Default: empty
Default: +pass:[false]+
This setting is only available with the QtWebKit backend.
@ -1595,7 +1605,7 @@ The file can be in one of the following formats:
`hosts` (with any extension).
Type: <<types,List>>
Type: <<types,List of Url>>
Default:
@ -1611,7 +1621,7 @@ List of domains that should always be loaded, despite being ad-blocked.
Domains may contain * and ? wildcards and are otherwise required to exactly match the requested domain.
Local domains are always exempt from hostblocking.
Type: <<types,List>>
Type: <<types,List of String>>
Default:
@ -1628,7 +1638,7 @@ Valid values:
* +true+
* +false+
Default: empty
Default: +pass:[false]+
[[content.images]]
=== content.images
@ -1668,7 +1678,7 @@ Valid values:
* +true+
* +false+
Default: empty
Default: +pass:[false]+
[[content.javascript.can_close_tabs]]
=== content.javascript.can_close_tabs
@ -1681,7 +1691,7 @@ Valid values:
* +true+
* +false+
Default: empty
Default: +pass:[false]+
This setting is only available with the QtWebKit backend.
@ -1696,7 +1706,7 @@ Valid values:
* +true+
* +false+
Default: empty
Default: +pass:[false]+
[[content.javascript.enabled]]
=== content.javascript.enabled
@ -1737,7 +1747,7 @@ Valid values:
* +true+
* +false+
Default: empty
Default: +pass:[false]+
[[content.javascript.prompt]]
=== content.javascript.prompt
@ -1776,7 +1786,7 @@ Valid values:
* +true+
* +false+
Default: empty
Default: +pass:[false]+
[[content.local_storage]]
=== content.local_storage
@ -1842,7 +1852,7 @@ Valid values:
* +true+
* +false+
Default: empty
Default: +pass:[false]+
This setting is only available with the QtWebKit backend.
@ -1857,7 +1867,7 @@ Valid values:
* +true+
* +false+
Default: empty
Default: +pass:[false]+
[[content.print_element_backgrounds]]
=== content.print_element_backgrounds
@ -1885,7 +1895,7 @@ Valid values:
* +true+
* +false+
Default: empty
Default: +pass:[false]+
[[content.proxy]]
=== content.proxy
@ -1934,7 +1944,7 @@ Default: +pass:[ask]+
=== content.user_stylesheets
A list of user stylesheet filenames to use.
Type: <<types,List>>
Type: <<types,List of File&#44; or File>>
Default: empty
@ -1954,7 +1964,7 @@ Default: +pass:[true]+
[[content.xss_auditing]]
=== content.xss_auditing
Whether load requests should be monitored for cross-site scripting attempts.
Suspicious scripts will be blocked and reported in the inspector\'s JavaScript console. Enabling this feature might have an impact on performance.
Suspicious scripts will be blocked and reported in the inspector's JavaScript console. Enabling this feature might have an impact on performance.
Type: <<types,Bool>>
@ -1963,7 +1973,7 @@ Valid values:
* +true+
* +false+
Default: empty
Default: +pass:[false]+
[[downloads.location.directory]]
=== downloads.location.directory
@ -2143,7 +2153,7 @@ Default: +pass:[8pt monospace]+
[[fonts.monospace]]
=== fonts.monospace
Default monospace fonts.
Whenever "monospace" is used in a font setting, it\'s replaced with the fonts listed here.
Whenever "monospace" is used in a font setting, it's replaced with the fonts listed here.
Type: <<types,Font>>
@ -2243,7 +2253,7 @@ The hard minimum font size.
Type: <<types,Int>>
Default: empty
Default: +pass:[0]+
[[fonts.web.size.minimum_logical]]
=== fonts.web.size.minimum_logical
@ -2253,6 +2263,22 @@ Type: <<types,Int>>
Default: +pass:[6]+
[[force_software_rendering]]
=== force_software_rendering
Force software rendering for QtWebEngine.
This is needed for QtWebEngine to work with Nouveau drivers. This setting requires a restart.
Type: <<types,Bool>>
Valid values:
* +true+
* +false+
Default: +pass:[false]+
This setting is only available with the QtWebEngine backend.
[[hints.auto_follow]]
=== hints.auto_follow
Controls when a hint can be automatically followed without pressing Enter.
@ -2274,7 +2300,7 @@ A timeout (in milliseconds) to ignore normal-mode key bindings after a successfu
Type: <<types,Int>>
Default: empty
Default: +pass:[0]+
[[hints.border]]
=== hints.border
@ -2354,7 +2380,7 @@ Default: +pass:[letter]+
=== hints.next_regexes
A comma-separated list of regexes to use for 'next' links.
Type: <<types,List>>
Type: <<types,List of Regex>>
Default:
@ -2369,7 +2395,7 @@ Default:
=== hints.prev_regexes
A comma-separated list of regexes to use for 'prev' links.
Type: <<types,List>>
Type: <<types,List of Regex>>
Default:
@ -2404,7 +2430,7 @@ Valid values:
* +true+
* +false+
Default: empty
Default: +pass:[false]+
[[history_gap_interval]]
=== history_gap_interval
@ -2467,7 +2493,7 @@ Valid values:
* +true+
* +false+
Default: empty
Default: +pass:[false]+
[[input.insert_mode.plugins]]
=== input.insert_mode.plugins
@ -2480,7 +2506,7 @@ Valid values:
* +true+
* +false+
Default: empty
Default: +pass:[false]+
[[input.links_included_in_focus_chain]]
=== input.links_included_in_focus_chain
@ -2516,7 +2542,7 @@ Valid values:
* +true+
* +false+
Default: empty
Default: +pass:[false]+
[[input.spatial_navigation]]
=== input.spatial_navigation
@ -2530,14 +2556,14 @@ Valid values:
* +true+
* +false+
Default: empty
Default: +pass:[false]+
[[keyhint.blacklist]]
=== keyhint.blacklist
Keychains that shouldn\'t be shown in the keyhint dialog.
Keychains that shouldn't be shown in the keyhint dialog.
Globs are supported, so `;*` will blacklist all keychains starting with `;`. Use `*` to disable keyhints.
Type: <<types,List>>
Type: <<types,List of String>>
Default: empty
@ -2569,7 +2595,7 @@ Valid values:
* +true+
* +false+
Default: empty
Default: +pass:[false]+
[[new_instance_open_target]]
=== new_instance_open_target
@ -2630,8 +2656,9 @@ Default: +pass:[8]+
=== qt_args
Additional arguments to pass to Qt, without leading `--`.
With QtWebEngine, some Chromium arguments (see https://peter.sh/experiments/chromium-command-line-switches/ for a list) will work.
This setting requires a restart.
Type: <<types,List>>
Type: <<types,List of String>>
Default: empty
@ -2646,7 +2673,7 @@ Valid values:
* +true+
* +false+
Default: empty
Default: +pass:[false]+
[[scrolling.smooth]]
=== scrolling.smooth
@ -2660,7 +2687,7 @@ Valid values:
* +true+
* +false+
Default: empty
Default: +pass:[false]+
[[session_default_name]]
=== session_default_name
@ -2682,7 +2709,7 @@ Valid values:
* +true+
* +false+
Default: empty
Default: +pass:[false]+
[[statusbar.padding]]
=== statusbar.padding
@ -2693,8 +2720,8 @@ Type: <<types,Padding>>
Default:
- +pass:[bottom]+: +pass:[1]+
- +pass:[left]+: empty
- +pass:[right]+: empty
- +pass:[left]+: +pass:[0]+
- +pass:[right]+: +pass:[0]+
- +pass:[top]+: +pass:[1]+
[[statusbar.position]]
@ -2721,7 +2748,7 @@ Valid values:
* +true+
* +false+
Default: empty
Default: +pass:[false]+
[[tabs.close_mouse_button]]
=== tabs.close_mouse_button
@ -2768,7 +2795,7 @@ Type: <<types,Padding>>
Default:
- +pass:[bottom]+: +pass:[2]+
- +pass:[left]+: empty
- +pass:[left]+: +pass:[0]+
- +pass:[right]+: +pass:[4]+
- +pass:[top]+: +pass:[2]+
@ -2839,10 +2866,10 @@ Type: <<types,Padding>>
Default:
- +pass:[bottom]+: empty
- +pass:[bottom]+: +pass:[0]+
- +pass:[left]+: +pass:[5]+
- +pass:[right]+: +pass:[5]+
- +pass:[top]+: empty
- +pass:[top]+: +pass:[0]+
[[tabs.position]]
=== tabs.position
@ -2907,7 +2934,7 @@ Valid values:
* +true+
* +false+
Default: empty
Default: +pass:[false]+
[[tabs.title.alignment]]
=== tabs.title.alignment
@ -2968,14 +2995,6 @@ Type: <<types,Int>>
Default: +pass:[3]+
[[tabs.width.pinned]]
=== tabs.width.pinned
The width for pinned tabs with a horizontal tabbar, in px.
Type: <<types,Int>>
Default: +pass:[43]+
[[tabs.wrap]]
=== tabs.wrap
Whether to wrap when changing tabs.
@ -3046,17 +3065,15 @@ Default:
=== url.start_pages
The page(s) to open at the start.
Type: <<types,List>>
Type: <<types,List of FuzzyUrl&#44; or FuzzyUrl>>
Default:
- +pass:[https://start.duckduckgo.com]+
Default: +pass:[https://start.duckduckgo.com]+
[[url.yank_ignored_parameters]]
=== url.yank_ignored_parameters
The URL parameters to strip with `:yank url`.
Type: <<types,List>>
Type: <<types,List of String>>
Default:
@ -3078,7 +3095,7 @@ Valid values:
* +true+
* +false+
Default: empty
Default: +pass:[false]+
[[window.title_format]]
=== window.title_format
@ -3112,7 +3129,7 @@ Default: +pass:[100%]+
=== zoom.levels
The available zoom levels.
Type: <<types,List>>
Type: <<types,List of Perc>>
Default:
@ -3152,7 +3169,7 @@ Valid values:
* +true+
* +false+
Default: empty
Default: +pass:[false]+
This setting is only available with the QtWebKit backend.
@ -3166,7 +3183,7 @@ This setting is only available with the QtWebKit backend.
When setting from a string, `1`, `yes`, `on` and `true` count as true, while `0`, `no`, `off` and `false` count as false (case-insensitive).
|BoolAsk|Like `Bool`, but `ask` is allowed as additional value.
|ColorSystem|The color system to use for color interpolation.
|Command|Base class for a command value with arguments.
|Command|A qutebrowser command with arguments.
|ConfirmQuit|Whether to display a confirmation when the window is closed.
|Dict|A dictionary of values.
@ -3189,6 +3206,7 @@ Lists with duplicate flags are invalid. Each item is checked against the valid v
|List|A list of values.
When setting from a string, pass a json-like list, e.g. `["one", "two"]`.
|ListOrValue|A list of values, or a single value.
|NewTabPosition|How new tabs are positioned.
|Padding|Setting for paddings around elements.
|Perc|A percentage.

View File

@ -264,9 +264,6 @@ Manual install
* Use the installer from http://www.python.org/downloads[python.org] to get
Python 3 (be sure to install pip).
* Use the installer from
http://www.riverbankcomputing.com/software/pyqt/download5[Riverbank computing]
to get Qt and PyQt5.
* Install https://testrun.org/tox/latest/index.html[tox] via
https://pip.pypa.io/en/latest/[pip]:

View File

@ -31,7 +31,7 @@ image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding c
* Run `:adblock-update` to download adblock lists and activate adblocking.
* If you just cloned the repository, you'll need to run
`scripts/asciidoc2html.py` to generate the documentation.
* Go to the link:qute://settings[settings page] to set up qutebrowser the way you want it. (Currently not available with the QtWebEngine backend and on the macOS build - use the `:set` command instead)
* Go to the link:qute://settings[settings page] to set up qutebrowser the way you want it.
* Subscribe to
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[the mailinglist] or
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce[the announce-only mailinglist].

View File

@ -84,9 +84,6 @@ show it.
*--force-color*::
Force colored logging
*--relaxed-config*::
Silently remove unknown config options.
*--nowindow*::
Don't show the main window.

View File

@ -5,7 +5,6 @@ import os
sys.path.insert(0, os.getcwd())
from scripts import setupcommon
from qutebrowser import utils
block_cipher = None
@ -31,9 +30,9 @@ def get_data_files():
setupcommon.write_git_file()
if utils.is_windows:
if os.name == 'nt':
icon = 'icons/qutebrowser.ico'
elif utils.is_mac:
elif sys.platform == 'darwin':
icon = 'icons/qutebrowser.icns'
else:
icon = None

View File

@ -3,6 +3,6 @@
appdirs==1.4.3
packaging==16.8
pyparsing==2.2.0
setuptools==36.2.7
six==1.10.0
wheel==0.29.0
setuptools==36.5.0
six==1.11.0
wheel==0.30.0

View File

@ -11,13 +11,13 @@ fields==5.0.0
Flask==0.12.2
glob2==0.6
hunter==2.0.1
hypothesis==3.28.3
hypothesis==3.30.3
itsdangerous==0.24
# Jinja2==2.9.6
Mako==1.0.7
# MarkupSafe==1.0
parse==1.8.2
parse-type==0.3.4
parse-type==0.4.1
py==1.4.34
py-cpuinfo==3.3.0
pytest==3.2.2

View File

@ -1,6 +1,6 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
pluggy==0.4.0
pluggy==0.5.2
py==1.4.34
tox==2.8.2
virtualenv==15.1.0

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python2
#!/usr/bin/env python
#
# Executes python-readability on current page and opens the summary as new tab.
#

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python2
#!/usr/bin/env python
#
# Adds DuckDuckGo bang as searchengine.
#
@ -8,14 +8,21 @@
# Example:
# :spawn --userscript ripbang amazon maps
#
import os, re, requests, sys, urllib
from __future__ import print_function
import os, re, requests, sys
try:
from urllib.parse import unquote
except ImportError:
from urllib import unquote
for argument in sys.argv[1:]:
bang = '!' + argument
r = requests.get('https://duckduckgo.com/',
params={'q': bang + ' SEARCHTEXT'})
searchengine = urllib.unquote(re.search("url=[^']+", r.text).group(0))
searchengine = unquote(re.search("url=[^']+", r.text).group(0))
searchengine = searchengine.replace('url=', '')
searchengine = searchengine.replace('/l/?kh=-1&uddg=', '')
searchengine = searchengine.replace('SEARCHTEXT', '{}')
@ -24,4 +31,4 @@ for argument in sys.argv[1:]:
with open(os.environ['QUTE_FIFO'], 'w') as fifo:
fifo.write('set searchengines %s %s' % (bang, searchengine))
else:
print '%s %s' % (bang, searchengine)
print('%s %s' % (bang, searchengine))

View File

@ -1,5 +1,5 @@
[pytest]
addopts = --strict -rfEw --faulthandler-timeout=70 --instafail --pythonwarnings error --benchmark-columns=Min,Max,Median
addopts = --strict -rfEw --faulthandler-timeout=90 --instafail --pythonwarnings error --benchmark-columns=Min,Max,Median
testpaths = tests
markers =
gui: Tests using the GUI (e.g. spawning widgets)

View File

@ -17,7 +17,25 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Initialization of qutebrowser and application-wide things."""
"""Initialization of qutebrowser and application-wide things.
The run() function will get called once early initialization (in
qutebrowser.py/earlyinit.py) is done. See the qutebrowser.py docstring for
details about early initialization.
As we need to access the config before the QApplication is created, we
initialize everything the config needs before the QApplication is created, and
then leave it in a partially initialized state (no saving, no config errors
shown yet).
We then set up the QApplication object and initialize a few more low-level
things.
After that, init() and _init_modules() take over and initialize the rest.
After all initialization is done, the qt_mainloop() function is called, which
blocks and spins the Qt mainloop.
"""
import os
import sys
@ -41,9 +59,10 @@ except ImportError:
import qutebrowser
import qutebrowser.resources
from qutebrowser.completion import completiondelegate
from qutebrowser.completion.models import miscmodels
from qutebrowser.commands import cmdutils, runners, cmdexc
from qutebrowser.config import config, websettings, configexc, configfiles
from qutebrowser.config import config, websettings, configfiles, configinit
from qutebrowser.browser import (urlmarks, adblock, history, browsertab,
downloads)
from qutebrowser.browser.network import proxy
@ -52,7 +71,8 @@ from qutebrowser.browser.webkit.network import networkmanager
from qutebrowser.keyinput import macros
from qutebrowser.mainwindow import mainwindow, prompt
from qutebrowser.misc import (readline, ipc, savemanager, sessions,
crashsignal, earlyinit, sql, cmdhistory)
crashsignal, earlyinit, sql, cmdhistory,
backendproblem)
from qutebrowser.utils import (log, version, message, utils, urlutils, objreg,
usertypes, standarddir, error)
# pylint: disable=unused-import
@ -77,7 +97,7 @@ def run(args):
standarddir.init(args)
log.init.debug("Initializing config...")
config.early_init(args)
configinit.early_init(args)
global qApp
qApp = Application(args)
@ -186,12 +206,6 @@ def _init_icon():
def _process_args(args):
"""Open startpage etc. and process commandline args."""
for opt, val in args.temp_settings:
try:
config.instance.set_str(opt, val)
except configexc.Error as e:
message.error("set: {} - {}".format(e.__class__.__name__, e))
if not args.override_restore:
_load_session(args.session)
session_manager = objreg.get('session-manager')
@ -387,13 +401,16 @@ def _init_modules(args, crash_handler):
crash_handler: The CrashHandler instance.
"""
# pylint: disable=too-many-statements
log.init.debug("Initializing prompts...")
prompt.init()
log.init.debug("Initializing save manager...")
save_manager = savemanager.SaveManager(qApp)
objreg.register('save-manager', save_manager)
config.late_init(save_manager)
configinit.late_init(save_manager)
log.init.debug("Checking backend requirements...")
backendproblem.init()
log.init.debug("Initializing prompts...")
prompt.init()
log.init.debug("Initializing network...")
networkmanager.init()
@ -408,11 +425,14 @@ def _init_modules(args, crash_handler):
log.init.debug("Initializing sql...")
try:
sql.init(os.path.join(standarddir.data(), 'history.sqlite'))
except sql.SqlException as e:
except sql.SqlError as e:
error.handle_fatal_exc(e, args, 'Error initializing SQL',
pre_text='Error initializing SQL')
sys.exit(usertypes.Exit.err_init)
log.init.debug("Initializing completion...")
completiondelegate.init()
log.init.debug("Initializing command history...")
cmdhistory.init()
@ -504,12 +524,13 @@ class Quitter:
with tokenize.open(os.path.join(dirpath, fn)) as f:
compile(f.read(), fn, 'exec')
def _get_restart_args(self, pages=(), session=None):
def _get_restart_args(self, pages=(), session=None, override_args=None):
"""Get the current working directory and args to relaunch qutebrowser.
Args:
pages: The pages to re-open.
session: The session to load, or None.
override_args: Argument overrides as a dict.
Return:
An (args, cwd) tuple.
@ -560,6 +581,9 @@ class Quitter:
argdict['temp_basedir'] = False
argdict['temp_basedir_restarted'] = True
if override_args is not None:
argdict.update(override_args)
# Dump the data
data = json.dumps(argdict)
args += ['--json-args', data]
@ -584,7 +608,7 @@ class Quitter:
if ok:
self.shutdown(restart=True)
def restart(self, pages=(), session=None):
def restart(self, pages=(), session=None, override_args=None):
"""Inner logic to restart qutebrowser.
The "better" way to restart is to pass a session (_restart usually) as
@ -597,6 +621,7 @@ class Quitter:
Args:
pages: A list of URLs to open.
session: The session to load, or None.
override_args: Argument overrides as a dict.
Return:
True if the restart succeeded, False otherwise.
@ -606,13 +631,19 @@ class Quitter:
log.destroy.debug("sys.path: {}".format(sys.path))
log.destroy.debug("sys.argv: {}".format(sys.argv))
log.destroy.debug("frozen: {}".format(hasattr(sys, 'frozen')))
# Save the session if one is given.
if session is not None:
session_manager = objreg.get('session-manager')
session_manager.save(session, with_private=True)
# Make sure we're not accepting a connection from the new process
# before we fully exited.
ipc.server.shutdown()
# Open a new process and immediately shutdown the existing one
try:
args, cwd = self._get_restart_args(pages, session)
args, cwd = self._get_restart_args(pages, session, override_args)
if cwd is None:
subprocess.Popen(args)
else:
@ -700,7 +731,7 @@ class Quitter:
QApplication.closeAllWindows()
# Shut down IPC
try:
objreg.get('ipc-server').shutdown()
ipc.server.shutdown()
except KeyError:
pass
# Save everything
@ -762,7 +793,7 @@ class Application(QApplication):
"""
self._last_focus_object = None
qt_args = config.qt_args(args)
qt_args = configinit.qt_args(args)
log.init.debug("Qt arguments: {}, based on {}".format(qt_args, args))
super().__init__(qt_args)

View File

@ -716,7 +716,7 @@ class AbstractTab(QWidget):
self._set_load_status(usertypes.LoadStatus.loading)
self.load_started.emit()
def _handle_auto_insert_mode(self, ok):
def handle_auto_insert_mode(self, ok):
"""Handle `input.insert_mode.auto_load` after loading finished."""
if not config.val.input.insert_mode.auto_load or not ok:
return
@ -753,7 +753,6 @@ class AbstractTab(QWidget):
self.load_finished.emit(ok)
if not self.title():
self.title_changed.emit(self.url().toDisplayString())
self._handle_auto_insert_mode(ok)
@pyqtSlot()
def _on_history_trigger(self):

View File

@ -256,7 +256,7 @@ class CommandDispatcher:
def tab_pin(self, count=None):
"""Pin/Unpin the current/[count]th tab.
Pinning a tab shrinks it to `tabs.width.pinned` size.
Pinning a tab shrinks it to the size of its title text.
Attempting to close a pinned tab will cause a confirmation,
unless --force is passed.
@ -688,7 +688,7 @@ class CommandDispatcher:
scope='window')
@cmdutils.argument('count', count=True)
@cmdutils.argument('horizontal', flag='x')
def scroll_perc(self, perc: float = None, horizontal=False, count=None):
def scroll_to_perc(self, perc: float = None, horizontal=False, count=None):
"""Scroll to a specific percentage of the page.
The percentage can be given either as argument or as count.
@ -1011,29 +1011,38 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('index', completion=miscmodels.buffer)
def buffer(self, index):
@cmdutils.argument('count', count=True)
def buffer(self, index=None, count=None):
"""Select tab by index or url/title best match.
Focuses window if necessary.
Focuses window if necessary when index is given. If both index and
count are given, use count.
Args:
index: The [win_id/]index of the tab to focus. Or a substring
in which case the closest match will be focused.
count: The tab index to focus, starting with 1.
"""
index_parts = index.split('/', 1)
if count is not None:
index_parts = [count]
elif index is None:
raise cmdexc.CommandError("buffer: Either a count or the argument "
"index must be specified.")
else:
index_parts = index.split('/', 1)
try:
for part in index_parts:
int(part)
except ValueError:
model = miscmodels.buffer()
model.set_pattern(index)
if model.count() > 0:
index = model.data(model.first_item())
index_parts = index.split('/', 1)
else:
raise cmdexc.CommandError(
"No matching tab for: {}".format(index))
try:
for part in index_parts:
int(part)
except ValueError:
model = miscmodels.buffer()
model.set_pattern(index)
if model.count() > 0:
index = model.data(model.first_item())
index_parts = index.split('/', 1)
else:
raise cmdexc.CommandError(
"No matching tab for: {}".format(index))
if len(index_parts) == 2:
win_id = int(index_parts[0])

View File

@ -40,7 +40,10 @@ class CompletionHistory(sql.SqlTable):
def __init__(self, parent=None):
super().__init__("CompletionHistory", ['url', 'title', 'last_atime'],
constraints={'url': 'PRIMARY KEY'}, parent=parent)
constraints={'url': 'PRIMARY KEY',
'title': 'NOT NULL',
'last_atime': 'NOT NULL'},
parent=parent)
self.create_index('CompletionHistoryAtimeIndex', 'last_atime')
@ -50,6 +53,10 @@ class WebHistory(sql.SqlTable):
def __init__(self, parent=None):
super().__init__("History", ['url', 'title', 'atime', 'redirect'],
constraints={'url': 'NOT NULL',
'title': 'NOT NULL',
'atime': 'NOT NULL',
'redirect': 'NOT NULL'},
parent=parent)
self.completion = CompletionHistory(parent=self)
if sql.Query('pragma user_version').run().value() < _USER_VERSION:
@ -252,10 +259,7 @@ class WebHistory(sql.SqlTable):
except ValueError as ex:
message.error('Failed to import history: {}'.format(ex))
else:
bakpath = path + '.bak'
message.info('History import complete. Moving {} to {}'
.format(path, bakpath))
os.rename(path, bakpath)
self._write_backup(path)
# delay to give message time to appear before locking down for import
message.info('Converting {} to sqlite...'.format(path))
@ -287,6 +291,16 @@ class WebHistory(sql.SqlTable):
self.insert_batch(data)
self.completion.insert_batch(completion_data, replace=True)
def _write_backup(self, path):
bak = path + '.bak'
message.info('History import complete. Appending {} to {}'
.format(path, bak))
with open(path, 'r', encoding='utf-8') as infile:
with open(bak, 'a', encoding='utf-8') as outfile:
for line in infile:
outfile.write('\n' + line)
os.remove(path)
def _format_url(self, url):
return url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)

View File

@ -408,11 +408,15 @@ def qute_settings(url):
@add_handler('configdiff')
def qute_configdiff(_url):
def qute_configdiff(url):
"""Handler for qute://configdiff."""
try:
return 'text/html', configdiff.get_diff()
except OSError as e:
error = (b'Failed to read old config: ' +
str(e.strerror).encode('utf-8'))
return 'text/plain', error
if url.path() == '/old':
try:
return 'text/html', configdiff.get_diff()
except OSError as e:
error = (b'Failed to read old config: ' +
str(e.strerror).encode('utf-8'))
return 'text/plain', error
else:
data = config.instance.dump_userconfig().encode('utf-8')
return 'text/plain', data

View File

@ -194,7 +194,7 @@ def _set_http_headers(profile):
def _update_settings(option):
"""Update global settings when qwebsettings changed."""
websettings.update_mappings(MAPPINGS, option)
if option in ['scrollbar.hide', 'content.user_stylesheets']:
if option in ['scrolling.bar', 'content.user_stylesheets']:
_init_stylesheet(default_profile)
_init_stylesheet(private_profile)
elif option in ['content.headers.user_agent',

View File

@ -19,9 +19,9 @@
"""Wrapper over a QWebEngineView."""
import os
import math
import functools
import html as html_utils
import sip
from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QPoint, QUrl, QTimer
@ -37,7 +37,7 @@ from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
webenginesettings)
from qutebrowser.misc import miscwidgets
from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
objreg, jinja, debug, version)
message, objreg, jinja, debug)
_qute_scheme_handler = None
@ -49,16 +49,8 @@ def init():
# won't work...
# https://www.riverbankcomputing.com/pipermail/pyqt/2016-September/038075.html
global _qute_scheme_handler
app = QApplication.instance()
software_rendering = (os.environ.get('LIBGL_ALWAYS_SOFTWARE') == '1' or
'QT_XCB_FORCE_SOFTWARE_OPENGL' in os.environ)
if version.opengl_vendor() == 'nouveau' and not software_rendering:
# FIXME:qtwebengine display something more sophisticated here
raise browsertab.WebTabError(
"QtWebEngine is not supported with Nouveau graphics (unless "
"QT_XCB_FORCE_SOFTWARE_OPENGL is set as environment variable).")
log.init.debug("Initializing qute://* handler...")
_qute_scheme_handler = webenginequtescheme.QuteSchemeHandler(parent=app)
_qute_scheme_handler.install(webenginesettings.default_profile)
@ -678,6 +670,32 @@ class WebEngineTab(browsertab.AbstractTab):
self.add_history_item.emit(url, requested_url, title)
@pyqtSlot(QUrl, 'QAuthenticator*', 'QString')
def _on_proxy_authentication_required(self, url, authenticator,
proxy_host):
"""Called when a proxy needs authentication."""
msg = "<b>{}</b> requires a username and password.".format(
html_utils.escape(proxy_host))
answer = message.ask(
title="Proxy authentication required", text=msg,
mode=usertypes.PromptMode.user_pwd,
abort_on=[self.shutting_down, self.load_started])
if answer is not None:
authenticator.setUser(answer.user)
authenticator.setPassword(answer.password)
else:
try:
# pylint: disable=no-member, useless-suppression
sip.assign(authenticator, QAuthenticator())
except AttributeError:
url_string = url.toDisplayString()
error_page = jinja.render(
'error.html',
title="Error loading page: {}".format(url_string),
url=url_string, error="Proxy authentication required",
icon='')
self.set_html(error_page)
@pyqtSlot(QUrl, 'QAuthenticator*')
def _on_authentication_required(self, url, authenticator):
# FIXME:qtwebengine support .netrc
@ -755,6 +773,8 @@ class WebEngineTab(browsertab.AbstractTab):
page.loadFinished.connect(self._on_load_finished)
page.certificate_error.connect(self._on_ssl_errors)
page.authenticationRequired.connect(self._on_authentication_required)
page.proxyAuthenticationRequired.connect(
self._on_proxy_authentication_required)
page.fullScreenRequested.connect(self._on_fullscreen_requested)
page.contentsSizeChanged.connect(self.contents_size_changed)

View File

@ -515,3 +515,7 @@ class Command:
raise cmdexc.PrerequisitesError(
"{}: This command is only allowed in {} mode, not {}.".format(
self.name, mode_names, mode.name))
def takes_count(self):
"""Return true iff this command can take a count argument."""
return any(arg.count for arg in self._qute_args)

View File

@ -154,6 +154,9 @@ class Completer(QObject):
"partitioned: {} '{}' {}".format(prefix, center, postfix))
return prefix, center, postfix
# We should always return above
assert False, parts
@pyqtSlot(str)
def on_selection_changed(self, text):
"""Change the completed part if a new item was selected.

View File

@ -34,6 +34,9 @@ from qutebrowser.config import config
from qutebrowser.utils import qtutils, jinja
_cached_stylesheet = None
class CompletionItemDelegate(QStyledItemDelegate):
"""Delegate used by CompletionView to draw individual items.
@ -189,14 +192,8 @@ class CompletionItemDelegate(QStyledItemDelegate):
self._doc.setDefaultTextOption(text_option)
self._doc.setDocumentMargin(2)
stylesheet = """
.highlight {
color: {{ conf.colors.completion.match.fg }};
}
"""
with jinja.environment.no_autoescape():
template = jinja.environment.from_string(stylesheet)
self._doc.setDefaultStyleSheet(template.render(conf=config.val))
assert _cached_stylesheet is not None
self._doc.setDefaultStyleSheet(_cached_stylesheet)
if index.parent().isValid():
view = self.parent()
@ -283,3 +280,24 @@ class CompletionItemDelegate(QStyledItemDelegate):
self._draw_focus_rect()
self._painter.restore()
@config.change_filter('colors.completion.match.fg', function=True)
def _update_stylesheet():
"""Update the cached stylesheet."""
stylesheet = """
.highlight {
color: {{ conf.colors.completion.match.fg }};
}
"""
with jinja.environment.no_autoescape():
template = jinja.environment.from_string(stylesheet)
global _cached_stylesheet
_cached_stylesheet = template.render(conf=config.val)
def init():
"""Initialize the cached stylesheet."""
_update_stylesheet()
config.instance.changed.connect(_update_stylesheet)

View File

@ -21,7 +21,7 @@
from qutebrowser.config import configdata, configexc
from qutebrowser.completion.models import completionmodel, listcategory, util
from qutebrowser.commands import runners
from qutebrowser.commands import runners, cmdexc
def option(*, info):
@ -44,7 +44,7 @@ def value(optname, *_values, info):
model = completionmodel.CompletionModel(column_widths=(30, 70, 0))
try:
current = info.config.get_str(optname) or '""'
current = info.config.get_str(optname)
except configexc.NoOptionError:
return None
@ -72,8 +72,12 @@ def bind(key, *, info):
if cmd_text:
parser = runners.CommandParser()
cmd = parser.parse(cmd_text).cmd
data = [(cmd_text, cmd.desc, key)]
try:
cmd = parser.parse(cmd_text).cmd
except cmdexc.NoSuchCommandError:
data = [(cmd_text, 'Invalid command!', key)]
else:
data = [(cmd_text, cmd.desc, key)]
model.add_category(listcategory.ListCategory("Current", data))
cmdlist = util.get_cmd_completions(info, include_hidden=True,

View File

@ -19,20 +19,15 @@
"""Configuration storage and config-related utilities."""
import sys
import copy
import contextlib
import functools
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl
from PyQt5.QtWidgets import QMessageBox
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
from qutebrowser.config import configdata, configexc, configtypes, configfiles
from qutebrowser.utils import (utils, objreg, message, log, usertypes, jinja,
qtutils)
from qutebrowser.misc import objects, msgbox, earlyinit
from qutebrowser.commands import cmdexc, cmdutils, runners
from qutebrowser.completion.models import configmodel
from qutebrowser.config import configdata, configexc
from qutebrowser.utils import utils, log, jinja
from qutebrowser.misc import objects
# An easy way to access the config from other code via config.val.foo
val = None
@ -40,9 +35,7 @@ instance = None
key_instance = None
# Keeping track of all change filters to validate them later.
_change_filters = []
# Errors which happened during init, so we can show a message box.
_init_errors = []
change_filters = []
class change_filter: # pylint: disable=invalid-name
@ -68,7 +61,7 @@ class change_filter: # pylint: disable=invalid-name
"""
self._option = option
self._function = function
_change_filters.append(self)
change_filters.append(self)
def validate(self):
"""Make sure the configured option or prefix exists.
@ -175,26 +168,11 @@ class KeyConfig:
bindings = self.get_bindings_for(mode)
return bindings.get(key, None)
def bind(self, key, command, *, mode, force=False, save_yaml=False):
def bind(self, key, command, *, mode, save_yaml=False):
"""Add a new binding from key to command."""
key = self._prepare(key, mode)
parser = runners.CommandParser()
try:
results = parser.parse_all(command)
except cmdexc.Error as e:
raise configexc.KeybindingError("Invalid command: {}".format(e))
for result in results: # pragma: no branch
try:
result.cmd.validate_mode(usertypes.KeyMode[mode])
except cmdexc.PrerequisitesError as e:
raise configexc.KeybindingError(str(e))
log.keyboard.vdebug("Adding binding {} -> {} in mode {}.".format(
key, command, mode))
if key in self.get_bindings_for(mode) and not force:
raise configexc.DuplicateKeyError(key)
bindings = self._config.get_obj('bindings.commands')
if mode not in bindings:
@ -223,145 +201,6 @@ class KeyConfig:
self._config.update_mutables(save_yaml=save_yaml)
class ConfigCommands:
"""qutebrowser commands related to the configuration."""
def __init__(self, config, keyconfig):
self._config = config
self._keyconfig = keyconfig
@cmdutils.register(instance='config-commands', star_args_optional=True)
@cmdutils.argument('option', completion=configmodel.option)
@cmdutils.argument('values', completion=configmodel.value)
@cmdutils.argument('win_id', win_id=True)
def set(self, win_id, option=None, *values, temp=False, print_=False):
"""Set an option.
If the option name ends with '?', the value of the option is shown
instead.
If the option name ends with '!' and it is a boolean value, toggle it.
Args:
option: The name of the option.
values: The value to set, or the values to cycle through.
temp: Set value temporarily until qutebrowser is closed.
print_: Print the value after setting.
"""
if option is None:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
tabbed_browser.openurl(QUrl('qute://settings'), newtab=False)
return
if option.endswith('?') and option != '?':
self._print_value(option[:-1])
return
with self._handle_config_error():
if option.endswith('!') and option != '!' and not values:
# Handle inversion as special cases of the cycle code path
option = option[:-1]
opt = self._config.get_opt(option)
if isinstance(opt.typ, configtypes.Bool):
values = ['false', 'true']
else:
raise cmdexc.CommandError(
"set: Can't toggle non-bool setting {}".format(option))
elif not values:
raise cmdexc.CommandError("set: The following arguments "
"are required: value")
self._set_next(option, values, temp=temp)
if print_:
self._print_value(option)
def _print_value(self, option):
"""Print the value of the given option."""
with self._handle_config_error():
value = self._config.get_str(option)
message.info("{} = {}".format(option, value))
def _set_next(self, option, values, *, temp):
"""Set the next value out of a list of values."""
if len(values) == 1:
# If we have only one value, just set it directly (avoid
# breaking stuff like aliases or other pseudo-settings)
self._config.set_str(option, values[0], save_yaml=not temp)
return
# Use the next valid value from values, or the first if the current
# value does not appear in the list
old_value = self._config.get_str(option)
try:
idx = values.index(str(old_value))
idx = (idx + 1) % len(values)
value = values[idx]
except ValueError:
value = values[0]
self._config.set_str(option, value, save_yaml=not temp)
@contextlib.contextmanager
def _handle_config_error(self):
"""Catch errors in set_command and raise CommandError."""
try:
yield
except configexc.Error as e:
raise cmdexc.CommandError("set: {}".format(e))
@cmdutils.register(instance='config-commands', maxsplit=1,
no_cmd_split=True, no_replace_variables=True)
@cmdutils.argument('command', completion=configmodel.bind)
def bind(self, key, command=None, *, mode='normal', force=False):
"""Bind a key to a command.
Args:
key: The keychain or special key (inside `<...>`) to bind.
command: The command to execute, with optional args, or None to
print the current binding.
mode: A comma-separated list of modes to bind the key in
(default: `normal`). See `:help bindings.commands` for the
available modes.
force: Rebind the key if it is already bound.
"""
if command is None:
if utils.is_special_key(key):
# self._keyconfig.get_command does this, but we also need it
# normalized for the output below
key = utils.normalize_keystr(key)
cmd = self._keyconfig.get_command(key, mode)
if cmd is None:
message.info("{} is unbound in {} mode".format(key, mode))
else:
message.info("{} is bound to '{}' in {} mode".format(
key, cmd, mode))
return
try:
self._keyconfig.bind(key, command, mode=mode, force=force,
save_yaml=True)
except configexc.DuplicateKeyError as e:
raise cmdexc.CommandError("bind: {} - use --force to override!"
.format(e))
except configexc.KeybindingError as e:
raise cmdexc.CommandError("bind: {}".format(e))
@cmdutils.register(instance='config-commands')
def unbind(self, key, *, mode='normal'):
"""Unbind a keychain.
Args:
key: The keychain or special key (inside <...>) to unbind.
mode: A mode to unbind the key in (default: `normal`).
See `:help bindings.commands` for the available modes.
"""
try:
self._keyconfig.unbind(key, mode=mode, save_yaml=True)
except configexc.KeybindingError as e:
raise cmdexc.CommandError('unbind: {}'.format(e))
class Config(QObject):
"""Main config object.
@ -399,7 +238,7 @@ class Config(QObject):
raise configexc.BackendError(objects.backend)
opt.typ.to_py(value) # for validation
self._values[opt.name] = value
self._values[opt.name] = opt.typ.from_obj(value)
self.changed.emit(opt.name)
log.config.debug("Config option changed: {} = {}".format(
@ -478,6 +317,32 @@ class Config(QObject):
if save_yaml:
self._yaml[name] = converted
def unset(self, name, *, save_yaml=False):
"""Set the given setting back to its default."""
self.get_opt(name)
try:
del self._values[name]
except KeyError:
return
self.changed.emit(name)
if save_yaml:
self._yaml.unset(name)
def clear(self, *, save_yaml=False):
"""Clear all settings in the config.
If save_yaml=True is given, also remove all customization from the YAML
file.
"""
old_values = self._values
self._values = {}
for name in old_values:
self.changed.emit(name)
if save_yaml:
self._yaml.clear()
def update_mutables(self, *, save_yaml=False):
"""Update mutable settings if they changed.
@ -647,114 +512,3 @@ class StyleSheetObserver(QObject):
self._obj.setStyleSheet(qss)
if update:
instance.changed.connect(self._update_stylesheet)
def early_init(args):
"""Initialize the part of the config which works without a QApplication."""
configdata.init()
yaml_config = configfiles.YamlConfig()
global val, instance, key_instance
instance = Config(yaml_config=yaml_config)
val = ConfigContainer(instance)
key_instance = KeyConfig(instance)
for cf in _change_filters:
cf.validate()
configtypes.Font.monospace_fonts = val.fonts.monospace
config_commands = ConfigCommands(instance, key_instance)
objreg.register('config-commands', config_commands)
config_api = None
try:
config_api = configfiles.read_config_py()
# Raised here so we get the config_api back.
if config_api.errors:
raise configexc.ConfigFileErrors('config.py', config_api.errors)
except configexc.ConfigFileErrors as e:
log.config.exception("Error while loading config.py")
_init_errors.append(e)
try:
if getattr(config_api, 'load_autoconfig', True):
try:
instance.read_yaml()
except configexc.ConfigFileErrors as e:
raise # caught in outer block
except configexc.Error as e:
desc = configexc.ConfigErrorDesc("Error", e)
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
except configexc.ConfigFileErrors as e:
log.config.exception("Error while loading config.py")
_init_errors.append(e)
configfiles.init()
objects.backend = get_backend(args)
earlyinit.init_with_backend(objects.backend)
def get_backend(args):
"""Find out what backend to use based on available libraries."""
try:
import PyQt5.QtWebKit # pylint: disable=unused-variable
except ImportError:
webkit_available = False
else:
webkit_available = qtutils.is_new_qtwebkit()
str_to_backend = {
'webkit': usertypes.Backend.QtWebKit,
'webengine': usertypes.Backend.QtWebEngine,
}
if args.backend is not None:
return str_to_backend[args.backend]
elif val.backend != 'auto':
return str_to_backend[val.backend]
elif webkit_available:
return usertypes.Backend.QtWebKit
else:
return usertypes.Backend.QtWebEngine
def late_init(save_manager):
"""Initialize the rest of the config after the QApplication is created."""
global _init_errors
for err in _init_errors:
errbox = msgbox.msgbox(parent=None,
title="Error while reading config",
text=err.to_html(),
icon=QMessageBox.Warning,
plain_text=False)
errbox.exec_()
_init_errors = []
instance.init_save_manager(save_manager)
configfiles.state.init_save_manager(save_manager)
def qt_args(namespace):
"""Get the Qt QApplication arguments based on an argparse namespace.
Args:
namespace: The argparse namespace.
Return:
The argv list to be passed to Qt.
"""
argv = [sys.argv[0]]
if namespace.qt_flag is not None:
argv += ['--' + flag[0] for flag in namespace.qt_flag]
if namespace.qt_arg is not None:
for name, value in namespace.qt_arg:
argv += ['--' + name, value]
argv += ['--' + arg for arg in val.qt_args]
return argv

View File

@ -0,0 +1,248 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Commands related to the configuration."""
import os.path
import contextlib
from PyQt5.QtCore import QUrl
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.completion.models import configmodel
from qutebrowser.utils import objreg, utils, message, standarddir
from qutebrowser.config import configtypes, configexc, configfiles
from qutebrowser.misc import editor
class ConfigCommands:
"""qutebrowser commands related to the configuration."""
def __init__(self, config, keyconfig):
self._config = config
self._keyconfig = keyconfig
@contextlib.contextmanager
def _handle_config_error(self):
"""Catch errors in set_command and raise CommandError."""
try:
yield
except configexc.Error as e:
raise cmdexc.CommandError("set: {}".format(e))
def _print_value(self, option):
"""Print the value of the given option."""
with self._handle_config_error():
value = self._config.get_str(option)
message.info("{} = {}".format(option, value))
@cmdutils.register(instance='config-commands')
@cmdutils.argument('option', completion=configmodel.option)
@cmdutils.argument('value', completion=configmodel.value)
@cmdutils.argument('win_id', win_id=True)
def set(self, win_id, option=None, value=None, temp=False, print_=False):
"""Set an option.
If the option name ends with '?', the value of the option is shown
instead.
Args:
option: The name of the option.
value: The value to set.
temp: Set value temporarily until qutebrowser is closed.
print_: Print the value after setting.
"""
if option is None:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
tabbed_browser.openurl(QUrl('qute://settings'), newtab=False)
return
if option.endswith('?') and option != '?':
self._print_value(option[:-1])
return
with self._handle_config_error():
if value is None:
raise cmdexc.CommandError("set: The following arguments "
"are required: value")
else:
self._config.set_str(option, value, save_yaml=not temp)
if print_:
self._print_value(option)
@cmdutils.register(instance='config-commands', maxsplit=1,
no_cmd_split=True, no_replace_variables=True)
@cmdutils.argument('command', completion=configmodel.bind)
def bind(self, key, command=None, *, mode='normal'):
"""Bind a key to a command.
Args:
key: The keychain or special key (inside `<...>`) to bind.
command: The command to execute, with optional args, or None to
print the current binding.
mode: A comma-separated list of modes to bind the key in
(default: `normal`). See `:help bindings.commands` for the
available modes.
"""
if command is None:
if utils.is_special_key(key):
# self._keyconfig.get_command does this, but we also need it
# normalized for the output below
key = utils.normalize_keystr(key)
cmd = self._keyconfig.get_command(key, mode)
if cmd is None:
message.info("{} is unbound in {} mode".format(key, mode))
else:
message.info("{} is bound to '{}' in {} mode".format(
key, cmd, mode))
return
try:
self._keyconfig.bind(key, command, mode=mode, save_yaml=True)
except configexc.KeybindingError as e:
raise cmdexc.CommandError("bind: {}".format(e))
@cmdutils.register(instance='config-commands')
def unbind(self, key, *, mode='normal'):
"""Unbind a keychain.
Args:
key: The keychain or special key (inside <...>) to unbind.
mode: A mode to unbind the key in (default: `normal`).
See `:help bindings.commands` for the available modes.
"""
try:
self._keyconfig.unbind(key, mode=mode, save_yaml=True)
except configexc.KeybindingError as e:
raise cmdexc.CommandError('unbind: {}'.format(e))
@cmdutils.register(instance='config-commands', star_args_optional=True)
@cmdutils.argument('option', completion=configmodel.option)
@cmdutils.argument('values', completion=configmodel.value)
def config_cycle(self, option, *values, temp=False, print_=False):
"""Cycle an option between multiple values.
Args:
option: The name of the option.
values: The values to cycle through.
temp: Set value temporarily until qutebrowser is closed.
print_: Print the value after setting.
"""
with self._handle_config_error():
opt = self._config.get_opt(option)
old_value = self._config.get_obj(option, mutable=False)
if not values and isinstance(opt.typ, configtypes.Bool):
values = ['true', 'false']
if len(values) < 2:
raise cmdexc.CommandError("Need at least two values for "
"non-boolean settings.")
# Use the next valid value from values, or the first if the current
# value does not appear in the list
with self._handle_config_error():
values = [opt.typ.from_str(val) for val in values]
try:
idx = values.index(old_value)
idx = (idx + 1) % len(values)
value = values[idx]
except ValueError:
value = values[0]
with self._handle_config_error():
self._config.set_obj(option, value, save_yaml=not temp)
if print_:
self._print_value(option)
@cmdutils.register(instance='config-commands')
@cmdutils.argument('option', completion=configmodel.option)
def config_unset(self, option, temp=False):
"""Unset an option.
This sets an option back to its default and removes it from
autoconfig.yml.
Args:
option: The name of the option.
temp: Don't touch autoconfig.yml.
"""
with self._handle_config_error():
self._config.unset(option, save_yaml=not temp)
@cmdutils.register(instance='config-commands')
def config_clear(self, save=False):
"""Set all settings back to their default.
Args:
save: If given, all configuration in autoconfig.yml is also
removed.
"""
self._config.clear(save_yaml=save)
@cmdutils.register(instance='config-commands')
def config_source(self, filename=None, clear=False):
"""Read a config.py file.
Args:
filename: The file to load. If not given, loads the default
config.py.
clear: Clear current settings first.
"""
if filename is None:
filename = os.path.join(standarddir.config(), 'config.py')
else:
filename = os.path.expanduser(filename)
if clear:
self.config_clear()
try:
configfiles.read_config_py(filename)
except configexc.ConfigFileErrors as e:
raise cmdexc.CommandError(e)
@cmdutils.register(instance='config-commands')
def config_edit(self, no_source=False):
"""Open the config.py file in the editor.
Args:
no_source: Don't re-source the config file after editing.
"""
def on_editing_finished():
"""Source the new config when editing finished.
This can't use cmdexc.CommandError as it's run async.
"""
try:
configfiles.read_config_py(filename)
except configexc.ConfigFileErrors as e:
message.error(str(e))
ed = editor.ExternalEditor(self._config)
if not no_source:
ed.editing_finished.connect(on_editing_finished)
filename = os.path.join(standarddir.config(), 'config.py')
ed.edit_file(filename)

View File

@ -93,7 +93,7 @@ def _parse_yaml_type(name, node):
if typ is configtypes.Dict:
kwargs['keytype'] = _parse_yaml_type(name, kwargs['keytype'])
kwargs['valtype'] = _parse_yaml_type(name, kwargs['valtype'])
elif typ is configtypes.List:
elif typ is configtypes.List or typ is configtypes.ListOrValue:
kwargs['valtype'] = _parse_yaml_type(name, kwargs['valtype'])
except KeyError as e:
_raise_invalid_node(name, str(e), node)

View File

@ -101,27 +101,40 @@ qt_args:
https://peter.sh/experiments/chromium-command-line-switches/ for a list)
will work.
This setting requires a restart.
force_software_rendering:
type: Bool
default: false
backend: QtWebEngine
desc: >-
Force software rendering for QtWebEngine.
This is needed for QtWebEngine to work with Nouveau drivers.
This setting requires a restart.
backend:
type:
name: String
valid_values:
- auto: Automatically select either QtWebEngine or QtWebKit
- webkit: Force QtWebKit
- webengine: Force QtWebEngine
default: auto
- webengine: Use QtWebEngine (based on Chromium)
- webkit: Use QtWebKit (based on WebKit, similar to Safari)
default: webengine
desc: >-
The backend to use to display websites.
qutebrowser supports two different web rendering engines / backends,
QtWebKit and QtWebEngine.
QtWebKit is based on WebKit (similar to Safari). It was discontinued by the
Qt project with Qt 5.6, but picked up as a well maintained fork:
https://github.com/annulen/webkit/wiki - qutebrowser only supports the fork.
QtWebKit was discontinued by the Qt project with Qt 5.6, but picked up as a
well maintained fork: https://github.com/annulen/webkit/wiki - qutebrowser
only supports the fork.
QtWebEngine is Qt's official successor to QtWebKit and based on the Chromium
project. It's slightly more resource hungry that QtWebKit and has a couple
of missing features in qutebrowser, but is generally the preferred choice.
QtWebEngine is Qt's official successor to QtWebKit. It's slightly more
resource hungry that QtWebKit and has a couple of missing features in
qutebrowser, but is generally the preferred choice.
This setting requires a restart.
## auto_save
@ -545,7 +558,7 @@ content.ssl_strict:
content.user_stylesheets:
type:
name: List
name: ListOrValue
valtype: File
none_ok: True
default: null
@ -562,10 +575,12 @@ content.xss_auditing:
desc: >-
Whether load requests should be monitored for cross-site scripting attempts.
Suspicious scripts will be blocked and reported in the inspector\'s
Suspicious scripts will be blocked and reported in the inspector's
JavaScript console. Enabling this feature might have an impact on
performance.
# emacs: '
## completion
completion.cmd_history_max_items:
@ -917,11 +932,13 @@ keyhint.blacklist:
name: String
default: []
desc: >-
Keychains that shouldn\'t be shown in the keyhint dialog.
Keychains that shouldn't be shown in the keyhint dialog.
Globs are supported, so `;*` will blacklist all keychains starting with `;`.
Use `*` to disable keyhints.
# emacs: '
keyhint.delay:
type:
name: Int
@ -1243,13 +1260,6 @@ tabs.width.indicator:
minval: 0
desc: Width of the progress indicator (0 to disable).
tabs.width.pinned:
default: 43
type:
name: Int
minval: 10
desc: The width for pinned tabs with a horizontal tabbar, in px.
tabs.wrap:
default: true
type: Bool
@ -1305,9 +1315,9 @@ url.searchengines:
url.start_pages:
type:
name: List
name: ListOrValue
valtype: FuzzyUrl
default: ["https://start.duckduckgo.com"]
default: "https://start.duckduckgo.com"
desc: The page(s) to open at the start.
url.yank_ignored_parameters:
@ -1616,13 +1626,18 @@ colors.prompts.fg:
type: QssColor
desc: Foreground color for prompts.
colors.prompts.border:
default: 1px solid gray
type: String
desc: Border used around UI elements in prompts.
colors.prompts.bg:
default: darkblue
default: '#444444'
type: QssColor
desc: Background color for prompts.
colors.prompts.selected.bg:
default: '#308cc6'
default: grey
type: QssColor
desc: Background color for the selected item in filename prompts.
@ -1803,9 +1818,11 @@ fonts.monospace:
desc: >-
Default monospace fonts.
Whenever "monospace" is used in a font setting, it\'s replaced with the
Whenever "monospace" is used in a font setting, it's replaced with the
fonts listed here.
# emacs: '
fonts.completion.entry:
default: 8pt monospace
type: Font
@ -1972,6 +1989,9 @@ bindings.key_mappings:
This is useful for global remappings of keys, for example to map Ctrl-[ to
Escape.
Note that when a key is bound (via `bindings.default` or
`bindings.commands`), the mapping is ignored.
bindings.default:
default:
normal:
@ -2040,8 +2060,8 @@ bindings.default:
l: scroll right
u: undo
<Ctrl-Shift-T>: undo
gg: scroll-perc 0
G: scroll-perc
gg: scroll-to-perc 0
G: scroll-to-perc
n: search-next
N: search-prev
i: enter-mode insert

View File

@ -59,14 +59,6 @@ class KeybindingError(Error):
"""Raised for issues with keybindings."""
class DuplicateKeyError(KeybindingError):
"""Raised when there was a duplicate key."""
def __init__(self, key):
super().__init__("Duplicate key {}".format(key))
class NoOptionError(Error):
"""Raised when an option was not found."""
@ -94,6 +86,12 @@ class ConfigErrorDesc:
def __str__(self):
return '{}: {}'.format(self.text, self.exception)
def with_text(self, text):
"""Get a new ConfigErrorDesc with the given text appended."""
return self.__class__(text='{} ({})'.format(self.text, text),
exception=self.exception,
traceback=self.traceback)
class ConfigFileErrors(Error):

View File

@ -19,18 +19,20 @@
"""Configuration files residing on disk."""
import pathlib
import types
import os.path
import sys
import textwrap
import traceback
import configparser
import contextlib
import yaml
from PyQt5.QtCore import QSettings
from PyQt5.QtCore import pyqtSignal, QObject, QSettings
import qutebrowser
from qutebrowser.config import configexc, config
from qutebrowser.config import configexc, config, configdata
from qutebrowser.utils import standarddir, utils, qtutils
@ -70,7 +72,7 @@ class StateConfig(configparser.ConfigParser):
self.write(f)
class YamlConfig:
class YamlConfig(QObject):
"""A config stored on disk as YAML file.
@ -79,8 +81,10 @@ class YamlConfig:
"""
VERSION = 1
changed = pyqtSignal()
def __init__(self):
def __init__(self, parent=None):
super().__init__(parent)
self._filename = os.path.join(standarddir.config(auto=True),
'autoconfig.yml')
self._values = {}
@ -92,20 +96,25 @@ class YamlConfig:
We do this outside of __init__ because the config gets created before
the save_manager exists.
"""
save_manager.add_saveable('yaml-config', self._save)
save_manager.add_saveable('yaml-config', self._save, self.changed)
def __getitem__(self, name):
return self._values[name]
def __setitem__(self, name, value):
self._dirty = True
self._values[name] = value
self._mark_changed()
def __contains__(self, name):
return name in self._values
def __iter__(self):
return iter(self._values.items())
return iter(sorted(self._values.items()))
def _mark_changed(self):
"""Mark the YAML config as changed."""
self._dirty = True
self.changed.emit()
def _save(self):
"""Save the settings to the YAML file if they've changed."""
@ -153,9 +162,28 @@ class YamlConfig:
"'global' object is not a dict")
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
# Delete unknown values
# (e.g. options which were removed from configdata.yml)
for name in list(global_obj):
if name not in configdata.DATA:
del global_obj[name]
self._values = global_obj
self._dirty = False
def unset(self, name):
"""Remove the given option name if it's configured."""
try:
del self._values[name]
except KeyError:
return
self._mark_changed()
def clear(self):
"""Clear all values from the YAML file."""
self._values = []
self._mark_changed()
class ConfigAPI:
@ -168,20 +196,26 @@ class ConfigAPI:
Attributes:
_config: The main Config object to use.
_keyconfig: The KeyConfig object.
load_autoconfig: Whether autoconfig.yml should be loaded.
errors: Errors which occurred while setting options.
configdir: The qutebrowser config directory, as pathlib.Path.
datadir: The qutebrowser data directory, as pathlib.Path.
"""
def __init__(self, conf, keyconfig):
self._config = conf
self._keyconfig = keyconfig
self.load_autoconfig = True
self.errors = []
self.configdir = pathlib.Path(standarddir.config())
self.datadir = pathlib.Path(standarddir.data())
@contextlib.contextmanager
def _handle_error(self, action, name):
try:
yield
except configexc.ConfigFileErrors as e:
for err in e.errors:
new_err = err.with_text(e.basename)
self.errors.append(new_err)
except configexc.Error as e:
text = "While {} '{}'".format(action, name)
self.errors.append(configexc.ConfigErrorDesc(text, e))
@ -190,6 +224,10 @@ class ConfigAPI:
"""Do work which needs to be done after reading config.py."""
self._config.update_mutables()
def load_autoconfig(self):
with self._handle_error('reading', 'autoconfig.yml'):
read_autoconfig()
def get(self, name):
with self._handle_error('getting', name):
return self._config.get_obj(name)
@ -198,24 +236,24 @@ class ConfigAPI:
with self._handle_error('setting', name):
self._config.set_obj(name, value)
def bind(self, key, command, mode='normal', *, force=False):
def bind(self, key, command, mode='normal'):
with self._handle_error('binding', key):
self._keyconfig.bind(key, command, mode=mode, force=force)
self._keyconfig.bind(key, command, mode=mode)
def unbind(self, key, mode='normal'):
with self._handle_error('unbinding', key):
self._keyconfig.unbind(key, mode=mode)
def read_config_py(filename=None):
"""Read a config.py file."""
def read_config_py(filename, raising=False):
"""Read a config.py file.
Arguments;
filename: The name of the file to read.
raising: Raise exceptions happening in config.py.
This is needed during tests to use pytest's inspection.
"""
api = ConfigAPI(config.instance, config.key_instance)
if filename is None:
filename = os.path.join(standarddir.config(), 'config.py')
if not os.path.exists(filename):
return api
container = config.ConfigContainer(config.instance, configapi=api)
basename = os.path.basename(filename)
@ -234,7 +272,7 @@ def read_config_py(filename=None):
try:
code = compile(source, filename, 'exec')
except (ValueError, TypeError) as e:
except ValueError as e:
# source contains NUL bytes
desc = configexc.ConfigErrorDesc("Error while compiling", e)
raise configexc.ConfigFileErrors(basename, [desc])
@ -244,14 +282,51 @@ def read_config_py(filename=None):
raise configexc.ConfigFileErrors(basename, [desc])
try:
exec(code, module.__dict__)
# Save and restore sys variables
with saved_sys_properties():
# Add config directory to python path, so config.py can import
# other files in logical places
config_dir = os.path.dirname(filename)
if config_dir not in sys.path:
sys.path.insert(0, config_dir)
exec(code, module.__dict__)
except Exception as e:
if raising:
raise
api.errors.append(configexc.ConfigErrorDesc(
"Unhandled exception",
exception=e, traceback=traceback.format_exc()))
api.finalize()
return api
if api.errors:
raise configexc.ConfigFileErrors('config.py', api.errors)
def read_autoconfig():
"""Read the autoconfig.yml file."""
try:
config.instance.read_yaml()
except configexc.ConfigFileErrors as e:
raise # caught in outer block
except configexc.Error as e:
desc = configexc.ConfigErrorDesc("Error", e)
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
@contextlib.contextmanager
def saved_sys_properties():
"""Save various sys properties such as sys.path and sys.modules."""
old_path = sys.path.copy()
old_modules = sys.modules.copy()
try:
yield
finally:
sys.path = old_path
for module in set(sys.modules).difference(old_modules):
del sys.modules[module]
def init():

View File

@ -0,0 +1,132 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Initialization of the configuration."""
import os.path
import sys
from PyQt5.QtWidgets import QMessageBox
from qutebrowser.config import (config, configdata, configfiles, configtypes,
configexc, configcommands)
from qutebrowser.utils import objreg, usertypes, log, standarddir, message
from qutebrowser.misc import msgbox, objects
# Error which happened during init, so we can show a message box.
_init_errors = None
def early_init(args):
"""Initialize the part of the config which works without a QApplication."""
configdata.init()
yaml_config = configfiles.YamlConfig()
config.instance = config.Config(yaml_config=yaml_config)
config.val = config.ConfigContainer(config.instance)
config.key_instance = config.KeyConfig(config.instance)
yaml_config.setParent(config.instance)
for cf in config.change_filters:
cf.validate()
configtypes.Font.monospace_fonts = config.val.fonts.monospace
config_commands = configcommands.ConfigCommands(
config.instance, config.key_instance)
objreg.register('config-commands', config_commands)
config_file = os.path.join(standarddir.config(), 'config.py')
try:
if os.path.exists(config_file):
configfiles.read_config_py(config_file)
else:
configfiles.read_autoconfig()
except configexc.ConfigFileErrors as e:
log.config.exception("Error while loading {}".format(e.basename))
global _init_errors
_init_errors = e
configfiles.init()
objects.backend = get_backend(args)
for opt, val in args.temp_settings:
try:
config.instance.set_str(opt, val)
except configexc.Error as e:
message.error("set: {} - {}".format(e.__class__.__name__, e))
if (objects.backend == usertypes.Backend.QtWebEngine and
config.val.force_software_rendering):
os.environ['QT_XCB_FORCE_SOFTWARE_OPENGL'] = '1'
def get_backend(args):
"""Find out what backend to use based on available libraries."""
str_to_backend = {
'webkit': usertypes.Backend.QtWebKit,
'webengine': usertypes.Backend.QtWebEngine,
}
if args.backend is not None:
return str_to_backend[args.backend]
else:
return str_to_backend[config.val.backend]
def late_init(save_manager):
"""Initialize the rest of the config after the QApplication is created."""
global _init_errors
if _init_errors is not None:
errbox = msgbox.msgbox(parent=None,
title="Error while reading config",
text=_init_errors.to_html(),
icon=QMessageBox.Warning,
plain_text=False)
errbox.exec_()
_init_errors = None
config.instance.init_save_manager(save_manager)
configfiles.state.init_save_manager(save_manager)
def qt_args(namespace):
"""Get the Qt QApplication arguments based on an argparse namespace.
Args:
namespace: The argparse namespace.
Return:
The argv list to be passed to Qt.
"""
argv = [sys.argv[0]]
if namespace.qt_flag is not None:
argv += ['--' + flag[0] for flag in namespace.qt_flag]
if namespace.qt_arg is not None:
for name, value in namespace.qt_arg:
argv += ['--' + name, value]
argv += ['--' + arg for arg in config.val.qt_args]
return argv

View File

@ -59,7 +59,7 @@ from PyQt5.QtCore import QUrl, Qt
from PyQt5.QtGui import QColor, QFont
from PyQt5.QtWidgets import QTabWidget, QTabBar
from qutebrowser.commands import cmdutils, runners, cmdexc
from qutebrowser.commands import cmdutils
from qutebrowser.config import configexc
from qutebrowser.utils import standarddir, utils, qtutils, urlutils
@ -227,6 +227,10 @@ class BaseType:
return None
return value
def from_obj(self, value):
"""Get the setting value from a config.py/YAML object."""
return value
def to_py(self, value):
"""Get the setting value from a Python value.
@ -257,9 +261,10 @@ class BaseType:
This currently uses asciidoc syntax.
"""
utils.unused(indent) # only needed for Dict/List
if not value:
str_value = self.to_str(value)
if not str_value:
return 'empty'
return '+pass:[{}]+'.format(html.escape(self.to_str(value)))
return '+pass:[{}]+'.format(html.escape(str_value))
def complete(self):
"""Return a list of possible values for completion.
@ -440,6 +445,11 @@ class List(BaseType):
self.to_py(yaml_val)
return yaml_val
def from_obj(self, value):
if value is None:
return []
return value
def to_py(self, value):
self._basic_py_validation(value, list)
if not value:
@ -475,6 +485,72 @@ class List(BaseType):
return '\n'.join(lines)
class ListOrValue(BaseType):
"""A list of values, or a single value.
//
Internally, the value is stored as either a value (of valtype), or a list.
to_py() then ensures that it's always a list.
"""
_show_valtype = True
def __init__(self, valtype, none_ok=False, *args, **kwargs):
super().__init__(none_ok)
assert not isinstance(valtype, (List, ListOrValue)), valtype
self.listtype = List(valtype, none_ok=none_ok, *args, **kwargs)
self.valtype = valtype
def get_name(self):
return self.listtype.get_name() + ', or ' + self.valtype.get_name()
def get_valid_values(self):
return self.valtype.get_valid_values()
def from_str(self, value):
try:
return self.listtype.from_str(value)
except configexc.ValidationError:
return self.valtype.from_str(value)
def from_obj(self, value):
if value is None:
return []
return value
def to_py(self, value):
try:
return [self.valtype.to_py(value)]
except configexc.ValidationError:
return self.listtype.to_py(value)
def to_str(self, value):
if value is None:
return ''
if isinstance(value, list):
if len(value) == 1:
return self.valtype.to_str(value[0])
else:
return self.listtype.to_str(value)
else:
return self.valtype.to_str(value)
def to_doc(self, value, indent=0):
if value is None:
return 'empty'
if isinstance(value, list):
if len(value) == 1:
return self.valtype.to_doc(value[0], indent)
else:
return self.listtype.to_doc(value, indent)
else:
return self.valtype.to_doc(value, indent)
class FlagList(List):
"""A list of flags.
@ -773,33 +849,13 @@ class PercOrInt(_Numeric):
class Command(BaseType):
"""Base class for a command value with arguments."""
"""A qutebrowser command with arguments.
# See to_py for details
unvalidated = False
//
def to_py(self, value):
self._basic_py_validation(value, str)
if not value:
return None
# This requires some trickery, as runners.CommandParser uses
# conf.val.aliases, which in turn map to a command again,
# leading to an endless recursion.
# To fix that, we turn off validating other commands (alias values)
# while validating a command.
if not Command.unvalidated:
Command.unvalidated = True
try:
parser = runners.CommandParser()
try:
parser.parse_all(value)
except cmdexc.Error as e:
raise configexc.ValidationError(value, str(e))
finally:
Command.unvalidated = False
return value
Since validation is quite tricky here, we don't do so, and instead let
invalid commands (in bindings/aliases) fail when used.
"""
def complete(self):
out = []
@ -807,6 +863,10 @@ class Command(BaseType):
out.append((cmdname, obj.desc))
return out
def to_py(self, value):
self._basic_py_validation(value, str)
return value
class ColorSystem(MappingType):
@ -1130,6 +1190,11 @@ class Dict(BaseType):
self.to_py(yaml_val)
return yaml_val
def from_obj(self, value):
if value is None:
return {}
return value
def _fill_fixed_keys(self, value):
"""Fill missing fixed keys with a None-value."""
if self.fixed_keys is None:
@ -1260,6 +1325,8 @@ class ShellCommand(List):
placeholder: If there should be a placeholder.
"""
_show_valtype = False
def __init__(self, placeholder=False, none_ok=False):
super().__init__(valtype=String(), none_ok=none_ok)
self.placeholder = placeholder

View File

@ -17,6 +17,9 @@ pre { margin: 2px; }
th, td { border: 1px solid grey; padding: 0px 5px; }
th { background: lightgrey; }
th pre { color: grey; text-align: left; }
input { width: 98%; }
.setting { width: 75%; }
.value { width: 25%; text-align: center; }
.noscript, .noscript-text { color:red; }
.noscript-text { margin-bottom: 5cm; }
.option_description { margin: .5ex 0; color: grey; font-size: 80%; font-style: italic; white-space: pre-line; }
@ -26,15 +29,19 @@ th pre { color: grey; text-align: left; }
<noscript><h1 class="noscript">View Only</h1><p class="noscript-text">Changing settings requires javascript to be enabled!</p></noscript>
<header><h1>{{ title }}</h1></header>
<table>
<tr>
<th>Setting</th>
<th>Value</th>
</tr>
{% for option in configdata.DATA.values() %}
<tr>
<!-- FIXME: convert to string properly -->
<td>{{ option.name }} (Current: {{ confget(option.name) | string |truncate(100) }})
<td class="setting">{{ option.name }} (Current: {{ confget(option.name) | string |truncate(100) }})
{% if option.description %}
<p class="option_description">{{ option.description|e }}</p>
{% endif %}
</td>
<td>
<td class="value">
<input type="text"
id="input-{{ option.name }}"
onblur="cset('{{ option.name }}', this.value)"

View File

@ -18,8 +18,6 @@
* along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable max-len */
/**
* Snippet to position caret at top of the page when caret mode is enabled.
* Some code was borrowed from:
@ -28,8 +26,6 @@
* https://github.com/1995eaton/chromium-vim/blob/master/content_scripts/visual.js
*/
/* eslint-enable max-len */
"use strict";
(function() {

View File

@ -122,37 +122,40 @@ class BaseKeyParser(QObject):
self._debug_log("Ignoring only-modifier keyeevent.")
return False
key_mappings = config.val.bindings.key_mappings
try:
binding = key_mappings['<{}>'.format(binding)][1:-1]
except KeyError:
pass
if binding not in self.special_bindings:
key_mappings = config.val.bindings.key_mappings
try:
binding = key_mappings['<{}>'.format(binding)][1:-1]
except KeyError:
pass
try:
cmdstr = self.special_bindings[binding]
except KeyError:
self._debug_log("No special binding found for {}.".format(binding))
return False
count, _command = self._split_count()
count, _command = self._split_count(self._keystring)
self.execute(cmdstr, self.Type.special, count)
self.clear_keystring()
return True
def _split_count(self):
def _split_count(self, keystring):
"""Get count and command from the current keystring.
Args:
keystring: The key string to split.
Return:
A (count, command) tuple.
"""
if self._supports_count:
(countstr, cmd_input) = re.match(r'^(\d*)(.*)',
self._keystring).groups()
(countstr, cmd_input) = re.match(r'^(\d*)(.*)', keystring).groups()
count = int(countstr) if countstr else None
if count == 0 and not cmd_input:
cmd_input = self._keystring
cmd_input = keystring
count = None
else:
cmd_input = self._keystring
cmd_input = keystring
count = None
return count, cmd_input
@ -183,18 +186,17 @@ class BaseKeyParser(QObject):
self._debug_log("Ignoring, no text char")
return self.Match.none
key_mappings = config.val.bindings.key_mappings
txt = key_mappings.get(txt, txt)
self._keystring += txt
count, cmd_input = self._split_count()
if not cmd_input:
# Only a count, no command yet, but we handled it
return self.Match.other
count, cmd_input = self._split_count(self._keystring + txt)
match, binding = self._match_key(cmd_input)
if match == self.Match.none:
mappings = config.val.bindings.key_mappings
mapped = mappings.get(txt, None)
if mapped is not None:
txt = mapped
count, cmd_input = self._split_count(self._keystring + txt)
match, binding = self._match_key(cmd_input)
self._keystring += txt
if match == self.Match.definitive:
self._debug_log("Definitive match for '{}'.".format(
self._keystring))
@ -207,6 +209,8 @@ class BaseKeyParser(QObject):
self._debug_log("Giving up with '{}', no matches".format(
self._keystring))
self.clear_keystring()
elif match == self.Match.other:
pass
else:
raise AssertionError("Invalid match value {!r}".format(match))
return match
@ -223,6 +227,9 @@ class BaseKeyParser(QObject):
binding: - None with Match.partial/Match.none.
- The found binding with Match.definitive.
"""
if not cmd_input:
# Only a count, no command yet, but we handled it
return (self.Match.other, None)
# A (cmd_input, binding) tuple (k, v of bindings) or None.
definitive_match = None
partial_match = False

View File

@ -437,7 +437,8 @@ class MainWindow(QWidget):
# commands
keyparsers[usertypes.KeyMode.normal].keystring_updated.connect(
status.keystring.setText)
cmd.got_cmd.connect(self._commandrunner.run_safely)
cmd.got_cmd[str].connect(self._commandrunner.run_safely)
cmd.got_cmd[str, int].connect(self._commandrunner.run_safely)
cmd.returnPressed.connect(tabs.on_cmd_return_pressed)
# key hint popup

View File

@ -28,7 +28,8 @@ import sip
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelIndex,
QItemSelectionModel, QObject, QEventLoop)
from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit,
QLabel, QFileSystemModel, QTreeView, QSizePolicy)
QLabel, QFileSystemModel, QTreeView, QSizePolicy,
QSpacerItem)
from qutebrowser.browser import downloads
from qutebrowser.config import config
@ -256,11 +257,21 @@ class PromptContainer(QWidget):
background-color: {{ conf.colors.prompts.bg }};
}
QTreeView {
selection-background-color: {{ conf.colors.prompts.selected.bg }};
QLineEdit {
border: {{ conf.colors.prompts.border }};
}
QTreeView::item:selected, QTreeView::item:selected:hover {
QTreeView {
selection-background-color: {{ conf.colors.prompts.selected.bg }};
border: {{ conf.colors.prompts.border }};
}
QTreeView::branch {
background-color: {{ conf.colors.prompts.bg }};
}
QTreeView::item:selected, QTreeView::item:selected:hover,
QTreeView::branch:selected {
background-color: {{ conf.colors.prompts.selected.bg }};
}
"""
@ -433,7 +444,6 @@ class LineEdit(QLineEdit):
super().__init__(parent)
self.setStyleSheet("""
QLineEdit {
border: 1px solid grey;
background-color: transparent;
}
""")
@ -511,6 +521,9 @@ class _BasePrompt(QWidget):
self._key_grid.addWidget(key_label, i, 0)
self._key_grid.addWidget(text_label, i, 1)
spacer = QSpacerItem(0, 0, QSizePolicy.Expanding)
self._key_grid.addItem(spacer, 0, 2)
self._vbox.addLayout(self._key_grid)
def accept(self, value=None):
@ -559,8 +572,7 @@ class FilenamePrompt(_BasePrompt):
def __init__(self, question, parent=None):
super().__init__(question, parent)
self._init_texts(question)
self._init_fileview()
self._set_fileview_root(question.default)
self._init_key_label()
self._lineedit = LineEdit(self)
if question.default:
@ -569,7 +581,9 @@ class FilenamePrompt(_BasePrompt):
self._vbox.addWidget(self._lineedit)
self.setFocusProxy(self._lineedit)
self._init_key_label()
self._init_fileview()
self._set_fileview_root(question.default)
if config.val.prompt.filebrowser:
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)

View File

@ -38,7 +38,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
Signals:
got_cmd: Emitted when a command is triggered by the user.
arg: The command string.
arg: The command string and also potentially the count.
clear_completion_selection: Emitted before the completion widget is
hidden.
hide_completion: Emitted when the completion widget should be hidden.
@ -47,7 +47,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
hide_cmd: Emitted when command input can be hidden.
"""
got_cmd = pyqtSignal(str)
got_cmd = pyqtSignal([str], [str, int])
clear_completion_selection = pyqtSignal()
hide_completion = pyqtSignal()
update_completion = pyqtSignal()
@ -91,7 +91,9 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
@cmdutils.register(instance='status-command', name='set-cmd-text',
scope='window', maxsplit=0)
def set_cmd_text_command(self, text, space=False, append=False):
@cmdutils.argument('count', count=True)
def set_cmd_text_command(self, text, count=None, space=False, append=False,
run_on_count=False):
"""Preset the statusbar to some text.
//
@ -101,8 +103,11 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
Args:
text: The commandline to set.
count: The count if given.
space: If given, a space is added to the end.
append: If given, the text is appended to the current text.
run_on_count: If given with a count, the command is run with the
given count rather than setting the command text.
"""
if space:
text += ' '
@ -114,7 +119,10 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
if not text or text[0] not in modeparsers.STARTCHARS:
raise cmdexc.CommandError(
"Invalid command text '{}'.".format(text))
self.set_cmd_text(text)
if run_on_count and count is not None:
self.got_cmd[str, int].emit(text, count)
else:
self.set_cmd_text(text)
@cmdutils.register(instance='status-command', hide=True,
modes=[usertypes.KeyMode.command], scope='window')
@ -156,7 +164,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
text = self.text()
self.history.append(text)
modeman.leave(self._win_id, usertypes.KeyMode.command, 'cmd accept')
self.got_cmd.emit(prefixes[text[0]] + text[1:])
self.got_cmd[str].emit(prefixes[text[0]] + text[1:])
@pyqtSlot(usertypes.KeyMode)
def on_mode_left(self, mode):

View File

@ -272,10 +272,6 @@ class TabbedBrowser(tabwidget.TabWidget):
if last_close == 'ignore' and count == 1:
return
# If we are removing a pinned tab, decrease count
if tab.data.pinned:
self.tabBar().pinned_count -= 1
self._remove_tab(tab, add_undo=add_undo)
if count == 1: # We just closed the last tab above.
@ -689,6 +685,7 @@ class TabbedBrowser(tabwidget.TabWidget):
self._update_tab_title(idx)
if idx == self.currentIndex():
self._update_window_title()
tab.handle_auto_insert_mode(ok)
@pyqtSlot()
def on_scroll_pos_changed(self):

View File

@ -92,26 +92,16 @@ class TabWidget(QTabWidget):
bar.update(bar.tabRect(idx))
def set_tab_pinned(self, tab: QWidget,
pinned: bool, *, loading: bool = False) -> None:
pinned: bool) -> None:
"""Set the tab status as pinned.
Args:
tab: The tab to pin
pinned: Pinned tab state to set.
loading: Whether to ignore current data state when
counting pinned_count.
"""
bar = self.tabBar()
idx = self.indexOf(tab)
# Only modify pinned_count if we had a change
# always modify pinned_count if we are loading
if tab.data.pinned != pinned or loading:
if pinned:
bar.pinned_count += 1
elif not pinned:
bar.pinned_count -= 1
bar.set_tab_data(idx, 'pinned', pinned)
tab.data.pinned = pinned
self._update_tab_title(idx)
@ -310,7 +300,6 @@ class TabBar(QTabBar):
self._on_show_switching_delay_changed()
self.setAutoFillBackground(True)
self._set_colors()
self.pinned_count = 0
QTimer.singleShot(0, self.maybe_hide)
def __repr__(self):
@ -435,18 +424,25 @@ class TabBar(QTabBar):
return
super().mousePressEvent(e)
def minimumTabSizeHint(self, index):
def minimumTabSizeHint(self, index, ellipsis: bool = True):
"""Set the minimum tab size to indicator/icon/... text.
Args:
index: The index of the tab to get a size hint for.
ellipsis: Whether to use ellipsis to calculate width
instead of the tab's text.
Return:
A QSize.
A QSize of the smallest tab size we can make.
"""
text = '\u2026' if ellipsis else self.tabText(index)
# Don't ever shorten if text is shorter than the ellipsis
text_width = min(self.fontMetrics().width(text),
self.fontMetrics().width(self.tabText(index)))
icon = self.tabIcon(index)
padding = config.val.tabs.padding
indicator_padding = config.val.tabs.indicator_padding
padding_h = padding.left + padding.right
padding_h += indicator_padding.left + indicator_padding.right
padding_v = padding.top + padding.bottom
if icon.isNull():
icon_size = QSize(0, 0)
@ -454,15 +450,32 @@ class TabBar(QTabBar):
extent = self.style().pixelMetric(QStyle.PM_TabBarIconSize, None,
self)
icon_size = icon.actualSize(QSize(extent, extent))
padding_h += self.style().pixelMetric(
PixelMetrics.icon_padding, None, self)
height = self.fontMetrics().height() + padding_v
width = (self.fontMetrics().width('\u2026') + icon_size.width() +
width = (text_width + icon_size.width() +
padding_h + config.val.tabs.width.indicator)
return QSize(width, height)
def tabSizeHint(self, index):
"""Override tabSizeHint so all tabs are the same size.
def _tab_total_width_pinned(self):
"""Get the current total width of pinned tabs.
This width is calculated assuming no shortening due to ellipsis."""
return sum(self.minimumTabSizeHint(idx, ellipsis=False).width()
for idx in range(self.count())
if self._tab_pinned(idx))
def _pinnedCount(self) -> int:
"""Get the number of pinned tabs."""
return sum(self._tab_pinned(idx) for idx in range(self.count()))
def _tab_pinned(self, index: int) -> bool:
"""Return True if tab is pinned."""
try:
return self.tab_data(index, 'pinned')
except KeyError:
return False
def tabSizeHint(self, index: int):
"""Override tabSizeHint to customize qb's tab size.
https://wiki.python.org/moin/PyQt/Customising%20tab%20bars
@ -490,43 +503,17 @@ class TabBar(QTabBar):
# want to ensure it's valid in this special case.
return QSize()
else:
try:
pinned = self.tab_data(index, 'pinned')
except KeyError:
pinned = False
no_pinned_count = self.count() - self.pinned_count
pinned_width = config.val.tabs.width.pinned * self.pinned_count
pinned = self._tab_pinned(index)
no_pinned_count = self.count() - self._pinnedCount()
pinned_width = self._tab_total_width_pinned()
no_pinned_width = self.width() - pinned_width
if pinned:
size = QSize(config.val.tabs.width.pinned, height)
qtutils.ensure_valid(size)
return size
# If we *do* have enough space, tabs should occupy the whole window
# width. If there are pinned tabs their size will be subtracted
# from the total window width.
# During shutdown the self.count goes down,
# but the self.pinned_count not - this generates some odd behavior.
# To avoid this we compare self.count against self.pinned_count.
if self.pinned_count > 0 and self.count() > self.pinned_count:
pinned_width = config.val.tabs.width.pinned * self.pinned_count
no_pinned_width = self.width() - pinned_width
width = no_pinned_width / (self.count() - self.pinned_count)
# Give pinned tabs the minimum size they need to display their
# titles, let Qt handle scaling it down if we get too small.
width = self.minimumTabSizeHint(index, ellipsis=False).width()
else:
# Tabs should attempt to occupy the whole window width. If
# there are pinned tabs their size will be subtracted from the
# total window width. During shutdown the self.count goes
# down, but the self.pinned_count not - this generates some odd
# behavior. To avoid this we compare self.count against
# self.pinned_count. If we end up having too little space, we
# set the minimum size below.
if self.pinned_count > 0 and no_pinned_count > 0:
width = no_pinned_width / no_pinned_count
else:
width = self.width() / self.count()
width = no_pinned_width / no_pinned_count
# If no_pinned_width is not divisible by no_pinned_count, add a
# pixel to some tabs so that there is no ugly leftover space.

View File

@ -0,0 +1,335 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Dialogs shown when there was a problem with a backend choice."""
import os
import sys
import functools
import html
import attr
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (QApplication, QDialog, QPushButton, QHBoxLayout,
QVBoxLayout, QLabel, QMessageBox)
from PyQt5.QtNetwork import QSslSocket
from qutebrowser.config import config
from qutebrowser.utils import usertypes, objreg, version, qtutils, log
from qutebrowser.misc import objects, msgbox
_Result = usertypes.enum(
'_Result',
['quit', 'restart', 'restart_webkit', 'restart_webengine'],
is_int=True, start=QDialog.Accepted + 1)
@attr.s
class _Button:
"""A button passed to BackendProblemDialog."""
text = attr.ib()
setting = attr.ib()
value = attr.ib()
default = attr.ib(default=False)
def _other_backend(backend):
"""Get the other backend enum/setting for a given backend."""
other_backend = {
usertypes.Backend.QtWebKit: usertypes.Backend.QtWebEngine,
usertypes.Backend.QtWebEngine: usertypes.Backend.QtWebKit,
}[backend]
other_setting = other_backend.name.lower()[2:]
return (other_backend, other_setting)
def _error_text(because, text, backend):
"""Get an error text for the given information."""
other_backend, other_setting = _other_backend(backend)
return ("<b>Failed to start with the {backend} backend!</b>"
"<p>qutebrowser tried to start with the {backend} backend but "
"failed because {because}.</p>{text}"
"<p><b>Forcing the {other_backend.name} backend</b></p>"
"<p>This forces usage of the {other_backend.name} backend by "
"setting the <i>backend = '{other_setting}'</i> option "
"(if you have a <i>config.py</i> file, you'll need to set "
"this manually).</p>".format(
backend=backend.name, because=because, text=text,
other_backend=other_backend, other_setting=other_setting))
class _Dialog(QDialog):
"""A dialog which gets shown if there are issues with the backend."""
def __init__(self, because, text, backend, buttons=None, parent=None):
super().__init__(parent)
vbox = QVBoxLayout(self)
other_backend, other_setting = _other_backend(backend)
text = _error_text(because, text, backend)
label = QLabel(text, wordWrap=True)
label.setTextFormat(Qt.RichText)
vbox.addWidget(label)
hbox = QHBoxLayout()
buttons = [] if buttons is None else buttons
quit_button = QPushButton("Quit")
quit_button.clicked.connect(lambda: self.done(_Result.quit))
hbox.addWidget(quit_button)
backend_button = QPushButton("Force {} backend".format(
other_backend.name))
backend_button.clicked.connect(functools.partial(
self._change_setting, 'backend', other_setting))
hbox.addWidget(backend_button)
for button in buttons:
btn = QPushButton(button.text, default=button.default)
btn.clicked.connect(functools.partial(
self._change_setting, button.setting, button.value))
hbox.addWidget(btn)
vbox.addLayout(hbox)
def _change_setting(self, setting, value):
"""Change the given setting and restart."""
config.instance.set_obj(setting, value, save_yaml=True)
save_manager = objreg.get('save-manager')
save_manager.save_all(is_exit=True)
if setting == 'backend' and value == 'webkit':
self.done(_Result.restart_webkit)
elif setting == 'backend' and value == 'webengine':
self.done(_Result.restart_webengine)
else:
self.done(_Result.restart)
def _show_dialog(*args, **kwargs):
"""Show a dialog for a backend problem."""
cmd_args = objreg.get('args')
if cmd_args.no_err_windows:
text = _error_text(*args, **kwargs)
print(text, file=sys.stderr)
sys.exit(usertypes.Exit.err_init)
dialog = _Dialog(*args, **kwargs)
status = dialog.exec_()
quitter = objreg.get('quitter')
if status in [_Result.quit, QDialog.Rejected]:
pass
elif status == _Result.restart_webkit:
quitter.restart(override_args={'backend': 'webkit'})
elif status == _Result.restart_webengine:
quitter.restart(override_args={'backend': 'webengine'})
elif status == _Result.restart:
quitter.restart()
else:
assert False, status
sys.exit(usertypes.Exit.err_init)
def _handle_nouveau_graphics():
assert objects.backend == usertypes.Backend.QtWebEngine, objects.backend
if version.opengl_vendor() != 'nouveau':
return
if (os.environ.get('LIBGL_ALWAYS_SOFTWARE') == '1' or
'QT_XCB_FORCE_SOFTWARE_OPENGL' in os.environ):
return
button = _Button("Force software rendering", 'force_software_rendering',
True)
_show_dialog(
backend=usertypes.Backend.QtWebEngine,
because="you're using Nouveau graphics",
text="<p>There are two ways to fix this:</p>"
"<p><b>Forcing software rendering</b></p>"
"<p>This allows you to use the newer QtWebEngine backend (based "
"on Chromium) but could have noticable performance impact "
"(depending on your hardware). "
"This sets the <i>force_software_rendering = True</i> option "
"(if you have a <i>config.py</i> file, you'll need to set this "
"manually).</p>",
buttons=[button],
)
# Should never be reached
assert False
def _handle_wayland():
assert objects.backend == usertypes.Backend.QtWebEngine, objects.backend
platform = QApplication.instance().platformName()
if platform not in ['wayland', 'wayland-egl']:
return
_show_dialog(
backend=usertypes.Backend.QtWebEngine,
because="you're using Wayland",
text="<p>There are two ways to fix this:</p>"
"<p><b>Set up XWayland</b></p>"
"<p>This allows you to use the newer QtWebEngine backend (based "
"on Chromium). "
)
# Should never be reached
assert False
@attr.s
class BackendImports:
"""Whether backend modules could be imported."""
webkit_available = attr.ib(default=None)
webengine_available = attr.ib(default=None)
webkit_error = attr.ib(default=None)
webengine_error = attr.ib(default=None)
def _try_import_backends():
"""Check whether backends can be imported and return BackendImports."""
# pylint: disable=unused-variable
results = BackendImports()
try:
from PyQt5 import QtWebKit
from PyQt5 import QtWebKitWidgets
except ImportError as e:
results.webkit_available = False
results.webkit_error = str(e)
else:
if qtutils.is_new_qtwebkit():
results.webkit_available = True
else:
results.webkit_available = False
results.webkit_error = "Unsupported legacy QtWebKit found"
try:
from PyQt5 import QtWebEngineWidgets
except ImportError as e:
results.webengine_available = False
results.webengine_error = str(e)
else:
results.webengine_available = True
assert results.webkit_available is not None
assert results.webengine_available is not None
if not results.webkit_available:
assert results.webkit_error is not None
if not results.webengine_available:
assert results.webengine_error is not None
return results
def _handle_ssl_support(fatal=False):
"""Check for full SSL availability.
If "fatal" is given, show an error and exit.
"""
text = ("Could not initialize QtNetwork SSL support. If you use "
"OpenSSL 1.1 with a PyQt package from PyPI (e.g. on Archlinux "
"or Debian Stretch), you need to set LD_LIBRARY_PATH to the path "
"of OpenSSL 1.0. This only affects downloads.")
if QSslSocket.supportsSsl():
return
if fatal:
errbox = msgbox.msgbox(parent=None,
title="SSL error",
text="Could not initialize SSL support.",
icon=QMessageBox.Critical,
plain_text=False)
errbox.exec_()
sys.exit(usertypes.Exit.err_init)
assert not fatal
log.init.warning(text)
def _check_backend_modules():
"""Check for the modules needed for QtWebKit/QtWebEngine."""
imports = _try_import_backends()
if imports.webkit_available and imports.webengine_available:
return
elif not imports.webkit_available and not imports.webengine_available:
text = ("<p>qutebrowser needs QtWebKit or QtWebEngine, but neither "
"could be imported!</p>"
"<p>The errors encountered were:<ul>"
"<li><b>QtWebKit:</b> {webkit_error}"
"<li><b>QtWebEngine:</b> {webengine_error}"
"</ul></p>".format(
webkit_error=html.escape(imports.webkit_error),
webengine_error=html.escape(imports.webengine_error)))
errbox = msgbox.msgbox(parent=None,
title="No backend library found!",
text=text,
icon=QMessageBox.Critical,
plain_text=False)
errbox.exec_()
sys.exit(usertypes.Exit.err_init)
elif objects.backend == usertypes.Backend.QtWebKit:
if imports.webkit_available:
return
assert imports.webengine_available
_show_dialog(
backend=usertypes.Backend.QtWebKit,
because="QtWebKit could not be imported",
text="<p><b>The error encountered was:</b><br/>{}</p>".format(
html.escape(imports.webkit_error))
)
elif objects.backend == usertypes.Backend.QtWebEngine:
if imports.webengine_available:
return
assert imports.webkit_available
_show_dialog(
backend=usertypes.Backend.QtWebEngine,
because="QtWebEngine could not be imported",
text="<p><b>The error encountered was:</b><br/>{}</p>".format(
html.escape(imports.webengine_error))
)
# Should never be reached
assert False
def init():
_check_backend_modules()
if objects.backend == usertypes.Backend.QtWebEngine:
_handle_ssl_support()
_handle_wayland()
_handle_nouveau_graphics()
else:
assert objects.backend == usertypes.Backend.QtWebKit, objects.backend
_handle_ssl_support(fatal=True)

View File

@ -32,15 +32,19 @@ import pkg_resources
from PyQt5.QtCore import pyqtSlot, Qt, QSize
from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton,
QVBoxLayout, QHBoxLayout, QCheckBox,
QDialogButtonBox, QMessageBox, QApplication)
QDialogButtonBox, QApplication)
import qutebrowser
from qutebrowser.utils import version, log, utils, objreg, usertypes
from qutebrowser.misc import (miscwidgets, autoupdate, msgbox, httpclient,
pastebin, objects)
pastebin)
from qutebrowser.config import config, configfiles
Result = usertypes.enum('Result', ['restore', 'no_restore'], is_int=True,
start=QDialog.Accepted + 1)
def parse_fatal_stacktrace(text):
"""Get useful information from a fatal faulthandler stacktrace.
@ -65,41 +69,6 @@ def parse_fatal_stacktrace(text):
return (m.group(1), m.group(3))
def get_fatal_crash_dialog(debug, data):
"""Get a fatal crash dialog based on a crash log.
If the crash is a segfault in qt_mainloop and we're on an old Qt version
this is a simple error dialog which lets the user know they should upgrade
if possible.
If it's anything else, it's a normal FatalCrashDialog with the possibility
to report the crash.
Args:
debug: Whether the debug flag (--debug) was given.
data: The crash log data.
"""
ignored_frames = ['qt_mainloop', 'paintEvent']
errtype, frame = parse_fatal_stacktrace(data)
if (errtype == 'Segmentation fault' and
frame in ignored_frames and
objects.backend == usertypes.Backend.QtWebKit):
title = "qutebrowser was restarted after a fatal crash!"
text = ("<b>qutebrowser was restarted after a fatal crash!</b><br/>"
"Unfortunately, this crash occurred in Qt (the library "
"qutebrowser uses), and QtWebKit (the current backend) is not "
"maintained anymore.<br/><br/>Since I can't do much about "
"those crashes I disabled the crash reporter for this case, "
"but this will likely be resolved in the future with the new "
"QtWebEngine backend.")
box = QMessageBox(QMessageBox.Critical, title, text, QMessageBox.Ok)
box.setAttribute(Qt.WA_DeleteOnClose)
return box
else:
return FatalCrashDialog(debug, data)
def _get_environment_vars():
"""Gather environment variables for the crash info."""
masks = ('DESKTOP_SESSION', 'DE', 'QT_*', 'PYTHON*', 'LC_*', 'LANG',
@ -478,9 +447,9 @@ class ExceptionCrashDialog(_CrashDialog):
def finish(self):
self._save_contact_info()
if self._chk_restore.isChecked():
self.accept()
self.done(Result.restore)
else:
self.reject()
self.done(Result.no_restore)
class FatalCrashDialog(_CrashDialog):

View File

@ -36,10 +36,10 @@ except ImportError:
import attr
from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QObject,
QSocketNotifier, QTimer, QUrl)
from PyQt5.QtWidgets import QApplication, QDialog
from PyQt5.QtWidgets import QApplication
from qutebrowser.commands import cmdutils
from qutebrowser.misc import earlyinit, crashdialog
from qutebrowser.misc import earlyinit, crashdialog, ipc
from qutebrowser.utils import usertypes, standarddir, log, objreg, debug, utils
@ -94,7 +94,7 @@ class CrashHandler(QObject):
if data:
# Crashlog exists and has data in it, so something crashed
# previously.
self._crash_dialog = crashdialog.get_fatal_crash_dialog(
self._crash_dialog = crashdialog.FatalCrashDialog(
self._args.debug, data)
self._crash_dialog.show()
else:
@ -236,7 +236,7 @@ class CrashHandler(QObject):
info = self._get_exception_info()
try:
objreg.get('ipc-server').ignored = True
ipc.server.ignored = True
except Exception:
log.destroy.exception("Error while ignoring ipc")
@ -258,7 +258,7 @@ class CrashHandler(QObject):
self._args.debug, info.pages, info.cmd_history, exc,
info.objects)
ret = self._crash_dialog.exec_()
if ret == QDialog.Accepted: # restore
if ret == crashdialog.Result.restore:
self._quitter.restart(info.pages)
# We might risk a segfault here, but that's better than continuing to

View File

@ -47,33 +47,25 @@ except ImportError:
START_TIME = datetime.datetime.now()
def _missing_str(name, *, windows=None, pip=None, webengine=False):
def _missing_str(name, *, webengine=False):
"""Get an error string for missing packages.
Args:
name: The name of the package.
windows: String to be displayed for Windows.
pip: pypi package name.
webengine: Whether this is checking the QtWebEngine package
"""
blocks = ["Fatal error: <b>{}</b> is required to run qutebrowser but "
"could not be imported! Maybe it's not installed?".format(name),
"<b>The error encountered was:</b><br />%ERROR%"]
lines = ['Please search for the python3 version of {} in your '
'distributions packages, or install it via pip.'.format(name)]
'distributions packages, or see '
'https://github.com/qutebrowser/qutebrowser/blob/master/doc/install.asciidoc'
.format(name)]
blocks.append('<br />'.join(lines))
if not webengine:
lines = ['<b>If you installed a qutebrowser package for your '
'distribution, please report this as a bug.</b>']
blocks.append('<br />'.join(lines))
if windows is not None:
lines = ["<b>On Windows:</b>"]
lines += windows.splitlines()
blocks.append('<br />'.join(lines))
if pip is not None:
lines = ["<b>Using pip:</b>"]
lines.append("pip3 install {}".format(pip))
blocks.append('<br />'.join(lines))
return '<br /><br />'.join(blocks)
@ -142,11 +134,7 @@ def check_pyqt_core():
try:
import PyQt5.QtCore # pylint: disable=unused-variable
except ImportError as e:
text = _missing_str('PyQt5',
windows="Use the installer by Riverbank computing "
"or the standalone qutebrowser exe.<br />"
"http://www.riverbankcomputing.co.uk/"
"software/pyqt/download5")
text = _missing_str('PyQt5')
text = text.replace('<b>', '')
text = text.replace('</b>', '')
text = text.replace('<br />', '\n')
@ -199,23 +187,6 @@ def check_ssl_support():
_die("Fatal error: Your Qt is built without SSL support.")
def check_backend_ssl_support(backend):
"""Check for full SSL availability when we know the backend."""
from PyQt5.QtNetwork import QSslSocket
from qutebrowser.utils import log, usertypes
text = ("Could not initialize QtNetwork SSL support. If you use "
"OpenSSL 1.1 with a PyQt package from PyPI (e.g. on Archlinux "
"or Debian Stretch), you need to set LD_LIBRARY_PATH to the path "
"of OpenSSL 1.0. This only affects downloads.")
if not QSslSocket.supportsSsl():
if backend == usertypes.Backend.QtWebKit:
_die("Could not initialize SSL support.")
else:
assert backend == usertypes.Backend.QtWebEngine
log.init.warning(text)
def _check_modules(modules):
"""Make sure the given modules are available."""
from qutebrowser.utils import log
@ -230,7 +201,14 @@ def _check_modules(modules):
'Flags not at the start of the expression']
with log.ignore_py_warnings(
category=DeprecationWarning,
message=r'({})'.format('|'.join(messages))):
message=r'({})'.format('|'.join(messages))
), log.ignore_py_warnings(
category=PendingDeprecationWarning,
module='imp'
), log.ignore_py_warnings(
category=ImportWarning,
message=r'Not importing directory .*: missing __init__'
):
importlib.import_module(name)
except ImportError as e:
_die(text, e)
@ -239,31 +217,12 @@ def _check_modules(modules):
def check_libraries():
"""Check if all needed Python libraries are installed."""
modules = {
'pkg_resources':
_missing_str("pkg_resources/setuptools",
windows="Run python -m ensurepip."),
'pypeg2':
_missing_str("pypeg2",
pip="pypeg2"),
'jinja2':
_missing_str("jinja2",
windows="Install from http://www.lfd.uci.edu/"
"~gohlke/pythonlibs/#jinja2 or via pip.",
pip="jinja2"),
'pygments':
_missing_str("pygments",
windows="Install from http://www.lfd.uci.edu/"
"~gohlke/pythonlibs/#pygments or via pip.",
pip="pygments"),
'yaml':
_missing_str("PyYAML",
windows="Use the installers at "
"http://pyyaml.org/download/pyyaml/ (py3.4) "
"or Install via pip.",
pip="PyYAML"),
'attr':
_missing_str("attrs",
pip="attrs"),
'pkg_resources': _missing_str("pkg_resources/setuptools"),
'pypeg2': _missing_str("pypeg2"),
'jinja2': _missing_str("jinja2"),
'pygments': _missing_str("pygments"),
'yaml': _missing_str("PyYAML"),
'attr': _missing_str("attrs"),
'PyQt5.QtQml': _missing_str("PyQt5.QtQml"),
'PyQt5.QtSql': _missing_str("PyQt5.QtSql"),
'PyQt5.QtOpenGL': _missing_str("PyQt5.QtOpenGL"),
@ -271,35 +230,6 @@ def check_libraries():
_check_modules(modules)
def check_backend_libraries(backend):
"""Make sure the libraries needed by the given backend are available.
Args:
backend: The backend as usertypes.Backend member.
"""
from qutebrowser.utils import usertypes
if backend == usertypes.Backend.QtWebEngine:
modules = {
'PyQt5.QtWebEngineWidgets':
_missing_str("QtWebEngine", webengine=True),
}
else:
assert backend == usertypes.Backend.QtWebKit, backend
modules = {
'PyQt5.QtWebKit': _missing_str("PyQt5.QtWebKit"),
'PyQt5.QtWebKitWidgets': _missing_str("PyQt5.QtWebKitWidgets"),
}
_check_modules(modules)
def check_new_webkit(backend):
"""Make sure we use QtWebEngine or a new QtWebKit."""
from qutebrowser.utils import usertypes, qtutils
if backend == usertypes.Backend.QtWebKit and not qtutils.is_new_qtwebkit():
_die("qutebrowser does not support legacy QtWebKit versions anymore, "
"see the installation docs for details.")
def remove_inputhook():
"""Remove the PyQt input hook.
@ -352,16 +282,3 @@ def early_init(args):
remove_inputhook()
check_ssl_support()
check_optimize_flag()
def init_with_backend(backend):
"""Do later stages of init when we know the backend.
Args:
backend: The backend as usertypes.Backend member.
"""
assert not isinstance(backend, str), backend
assert backend is not None
check_backend_libraries(backend)
check_backend_ssl_support(backend)
check_new_webkit(backend)

View File

@ -35,8 +35,9 @@ class ExternalEditor(QObject):
Attributes:
_text: The current text before the editor is opened.
_file: The file handle as tempfile.NamedTemporaryFile. Note that this
handle will be closed after the initial file has been created.
_filename: The name of the file to be edited.
_remove_file: Whether the file should be removed when the editor is
closed.
_proc: The GUIProcess of the editor.
"""
@ -44,18 +45,20 @@ class ExternalEditor(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self._text = None
self._file = None
self._filename = None
self._proc = None
self._remove_file = None
def _cleanup(self):
"""Clean up temporary files after the editor closed."""
if self._file is None:
assert self._remove_file is not None
if self._filename is None or not self._remove_file:
# Could not create initial file.
return
try:
if self._proc.exit_status() != QProcess.CrashExit:
os.remove(self._file.name)
os.remove(self._filename)
except OSError as e:
# NOTE: Do not replace this with "raise CommandError" as it's
# executed async.
@ -77,7 +80,7 @@ class ExternalEditor(QObject):
return
encoding = config.val.editor.encoding
try:
with open(self._file.name, 'r', encoding=encoding) as f:
with open(self._filename, 'r', encoding=encoding) as f:
text = f.read()
except OSError as e:
# NOTE: Do not replace this with "raise CommandError" as it's
@ -99,9 +102,8 @@ class ExternalEditor(QObject):
Args:
text: The initial text to edit.
"""
if self._text is not None:
if self._filename is not None:
raise ValueError("Already editing a file!")
self._text = text
try:
# Close while the external process is running, as otherwise systems
# with exclusive write access (e.g. Windows) may fail to update
@ -113,15 +115,27 @@ class ExternalEditor(QObject):
delete=False) as fobj:
if text:
fobj.write(text)
self._file = fobj
self._filename = fobj.name
except OSError as e:
message.error("Failed to create initial file: {}".format(e))
return
self._remove_file = True
self._start_editor()
def edit_file(self, filename):
"""Edit the file with the given filename."""
self._filename = filename
self._remove_file = False
self._start_editor()
def _start_editor(self):
"""Start the editor with the file opened as self._filename."""
self._proc = guiprocess.GUIProcess(what='editor', parent=self)
self._proc.finished.connect(self.on_proc_closed)
self._proc.error.connect(self.on_proc_error)
editor = config.val.editor.command
executable = editor[0]
args = [arg.replace('{}', self._file.name) for arg in editor[1:]]
args = [arg.replace('{}', self._filename) for arg in editor[1:]]
log.procs.debug("Calling \"{}\" with args {}".format(executable, args))
self._proc.start(executable, args)

View File

@ -30,7 +30,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, Qt
from PyQt5.QtNetwork import QLocalSocket, QLocalServer, QAbstractSocket
import qutebrowser
from qutebrowser.utils import log, usertypes, error, objreg, standarddir, utils
from qutebrowser.utils import log, usertypes, error, standarddir, utils
CONNECT_TIMEOUT = 100 # timeout for connecting/disconnecting
@ -40,6 +40,10 @@ ATIME_INTERVAL = 60 * 60 * 3 * 1000 # 3 hours
PROTOCOL_VERSION = 1
# The ipc server instance
server = None
def _get_socketname_windows(basedir):
"""Get a socketname to use for Windows."""
parts = ['qutebrowser', getpass.getuser()]
@ -109,15 +113,15 @@ class ListenError(Error):
message: The error message.
"""
def __init__(self, server):
def __init__(self, local_server):
"""Constructor.
Args:
server: The QLocalServer which has the error set.
local_server: The QLocalServer which has the error set.
"""
super().__init__()
self.code = server.serverError()
self.message = server.errorString()
self.code = local_server.serverError()
self.message = local_server.errorString()
def __str__(self):
return "Error while listening to IPC server: {} (error {})".format(
@ -482,6 +486,7 @@ def send_or_listen(args):
The IPCServer instance if no running instance was detected.
None if an instance was running and received our request.
"""
global server
socketname = _get_socketname(args.basedir)
try:
try:
@ -492,7 +497,6 @@ def send_or_listen(args):
log.init.debug("Starting IPC server...")
server = IPCServer(socketname)
server.listen()
objreg.register('ipc-server', server)
return server
except AddressInUseError as e:
# This could be a race condition...

View File

@ -26,12 +26,14 @@ It is intended to help discoverability of keybindings.
import html
import fnmatch
import re
from PyQt5.QtWidgets import QLabel, QSizePolicy
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt
from qutebrowser.config import config
from qutebrowser.utils import utils, usertypes
from qutebrowser.commands import cmdutils
class KeyHintView(QLabel):
@ -85,6 +87,7 @@ class KeyHintView(QLabel):
Args:
prefix: The current partial keystring.
"""
countstr, prefix = re.match(r'^(\d*)(.*)', prefix).groups()
if not prefix:
self._show_timer.stop()
self.hide()
@ -94,11 +97,18 @@ class KeyHintView(QLabel):
return any(fnmatch.fnmatchcase(keychain, glob)
for glob in config.val.keyhint.blacklist)
def takes_count(cmdstr):
"""Return true iff this command can take a count argument."""
cmdname = cmdstr.split(' ')[0]
cmd = cmdutils.cmd_dict.get(cmdname)
return cmd and cmd.takes_count()
bindings_dict = config.key_instance.get_bindings_for(modename)
bindings = [(k, v) for (k, v) in sorted(bindings_dict.items())
if k.startswith(prefix) and
not utils.is_special_key(k) and
not blacklisted(k)]
not blacklisted(k) and
(takes_count(v) or not countstr)]
if not bindings:
self._show_timer.stop()

View File

@ -19,10 +19,21 @@
"""Convenience functions to show message boxes."""
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QMessageBox
from qutebrowser.utils import objreg
class DummyBox:
"""A dummy QMessageBox returned when --no-err-windows is used."""
def exec_(self):
pass
def msgbox(parent, title, text, *, icon, buttons=QMessageBox.Ok,
on_finished=None, plain_text=None):
@ -40,6 +51,11 @@ def msgbox(parent, title, text, *, icon, buttons=QMessageBox.Ok,
Return:
A new QMessageBox.
"""
args = objreg.get('args')
if args.no_err_windows:
print('Message box: {}; {}'.format(title, text), file=sys.stderr)
return DummyBox()
box = QMessageBox(parent)
box.setAttribute(Qt.WA_DeleteOnClose)
box.setIcon(icon)

View File

@ -164,6 +164,11 @@ class SaveManager(QObject):
self.saveables[name].save(is_exit=is_exit, explicit=explicit,
silent=silent, force=force)
def save_all(self, *args, **kwargs):
"""Save all saveables."""
for saveable in self.saveables:
self.save(saveable, *args, **kwargs)
@pyqtSlot()
def autosave(self):
"""Slot used when the configs are auto-saved."""

View File

@ -393,8 +393,7 @@ class SessionManager(QObject):
if tab.get('active', False):
tab_to_focus = i
if new_tab.data.pinned:
tabbed_browser.set_tab_pinned(
new_tab, new_tab.data.pinned, loading=True)
tabbed_browser.set_tab_pinned(new_tab, new_tab.data.pinned)
if tab_to_focus is not None:
tabbed_browser.setCurrentIndex(tab_to_focus)
if win.get('active', False):

View File

@ -22,12 +22,12 @@
import collections
from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtSql import QSqlDatabase, QSqlQuery
from PyQt5.QtSql import QSqlDatabase, QSqlQuery, QSqlError
from qutebrowser.utils import log
from qutebrowser.utils import log, debug
class SqlException(Exception):
class SqlError(Exception):
"""Raised on an error interacting with the SQL database."""
@ -38,12 +38,14 @@ def init(db_path):
"""Initialize the SQL database connection."""
database = QSqlDatabase.addDatabase('QSQLITE')
if not database.isValid():
raise SqlException('Failed to add database. '
raise SqlError('Failed to add database. '
'Are sqlite and Qt sqlite support installed?')
database.setDatabaseName(db_path)
if not database.open():
raise SqlException("Failed to open sqlite database at {}: {}"
.format(db_path, database.lastError().text()))
error = database.lastError()
_log_error(error)
raise SqlError("Failed to open sqlite database at {}: {}"
.format(db_path, error.text()))
def close():
@ -60,10 +62,32 @@ def version():
close()
return ver
return Query("select sqlite_version()").run().value()
except SqlException as e:
except SqlError as e:
return 'UNAVAILABLE ({})'.format(e)
def _log_error(error):
"""Log informations about a SQL error to the debug log."""
log.sql.debug("SQL error:")
log.sql.debug("type: {}".format(debug.qenum_key(QSqlError, error.type())))
log.sql.debug("database text: {}".format(error.databaseText()))
log.sql.debug("driver text: {}".format(error.driverText()))
log.sql.debug("error code: {}".format(error.nativeErrorCode()))
def _handle_query_error(what, query, error):
"""Handle a sqlite error.
Arguments:
what: What we were doing when the error happened.
query: The query which was executed.
error: The QSqlError object.
"""
_log_error(error)
msg = 'Failed to {} query "{}": "{}"'.format(what, query, error.text())
raise SqlError(msg)
class Query(QSqlQuery):
"""A prepared SQL Query."""
@ -79,13 +103,12 @@ class Query(QSqlQuery):
super().__init__(QSqlDatabase.database())
log.sql.debug('Preparing SQL query: "{}"'.format(querystr))
if not self.prepare(querystr):
raise SqlException('Failed to prepare query "{}": "{}"'.format(
querystr, self.lastError().text()))
_handle_query_error('prepare', querystr, self.lastError())
self.setForwardOnly(forward_only)
def __iter__(self):
if not self.isActive():
raise SqlException("Cannot iterate inactive query")
raise SqlError("Cannot iterate inactive query")
rec = self.record()
fields = [rec.fieldName(i) for i in range(rec.count())]
rowtype = collections.namedtuple('ResultRow', fields)
@ -101,14 +124,13 @@ class Query(QSqlQuery):
self.bindValue(':{}'.format(key), val)
log.sql.debug('query bindings: {}'.format(self.boundValues()))
if not self.exec_():
raise SqlException('Failed to exec query "{}": "{}"'.format(
self.lastQuery(), self.lastError().text()))
_handle_query_error('exec', self.lastQuery(), self.lastError())
return self
def value(self):
"""Return the result of a single-value query (e.g. an EXISTS)."""
if not self.next():
raise SqlException("No result for single-result query")
raise SqlError("No result for single-result query")
return self.record().value(0)
@ -128,7 +150,7 @@ class SqlTable(QObject):
def __init__(self, name, fields, constraints=None, parent=None):
"""Create a new table in the sql database.
Raises SqlException if the table already exists.
Does nothing if the table already exists.
Args:
name: Name of the table.
@ -228,8 +250,7 @@ class SqlTable(QObject):
db = QSqlDatabase.database()
db.transaction()
if not q.execBatch():
raise SqlException('Failed to exec query "{}": "{}"'.format(
q.lastQuery(), q.lastError().text()))
_handle_query_error('exec', q.lastQuery(), q.lastError())
db.commit()
self.changed.emit()

View File

@ -171,12 +171,15 @@ def debug_cache_stats():
prefix_info = configdata.is_valid_prefix.cache_info()
# pylint: disable=protected-access
render_stylesheet_info = config._render_stylesheet.cache_info()
history_info = None
try:
from PyQt5.QtWebKit import QWebHistoryInterface
interface = QWebHistoryInterface.defaultInterface()
history_info = interface.historyContains.cache_info()
if interface is not None:
history_info = interface.historyContains.cache_info()
except ImportError:
history_info = None
pass
log.misc.debug('is_valid_prefix: {}'.format(prefix_info))
log.misc.debug('_render_stylesheet: {}'.format(render_stylesheet_info))
@ -339,3 +342,12 @@ def window_only(current_win_id):
def nop():
"""Do nothing."""
return
@cmdutils.register()
@cmdutils.argument('win_id', win_id=True)
def version(win_id):
"""Show version information."""
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
tabbed_browser.openurl(QUrl('qute://version'), newtab=True)

View File

@ -17,7 +17,21 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Early initialization and main entry point."""
"""Early initialization and main entry point.
qutebrowser's initialization process roughly looks like this:
- This file gets imported, either via the setuptools entry point or
__main__.py.
- At import time, we check for the correct Python version and show an error if
it's too old.
- The main() function in this file gets invoked
- Argument parsing takes place
- earlyinit.early_init() gets invoked to do various low-level initialization
and checks whether all dependencies are met.
- app.run() gets called, which takes over.
See the docstring of app.py for details.
"""
import sys
import json
@ -95,8 +109,6 @@ def get_argparser():
action='store_false', dest='color')
debug.add_argument('--force-color', help="Force colored logging",
action='store_true')
debug.add_argument('--relaxed-config', action='store_true',
help="Silently remove unknown config options.")
debug.add_argument('--nowindow', action='store_true', help="Don't show "
"the main window.")
debug.add_argument('--temp-basedir', action='store_true', help="Use a "

View File

@ -39,6 +39,7 @@ except ImportError:
colorama = None
_log_inited = False
_args = None
COLORS = ['black', 'red', 'green', 'yellow', 'blue', 'purple', 'cyan', 'white']
COLOR_ESCAPES = {color: '\033[{}m'.format(i)
@ -189,8 +190,9 @@ def init_log(args):
logging.captureWarnings(True)
_init_py_warnings()
QtCore.qInstallMessageHandler(qt_message_handler)
global _log_inited
global _log_inited, _args
_log_inited = True
_args = args
def _init_py_warnings():
@ -442,7 +444,11 @@ def qt_message_handler(msg_type, context, msg):
msg += ("\n\nOn Archlinux, this should fix the problem:\n"
" pacman -S libxkbcommon-x11")
faulthandler.disable()
stack = ''.join(traceback.format_stack())
if _args.debug:
stack = ''.join(traceback.format_stack())
else:
stack = None
record = qt.makeRecord(name, level, context.file, context.line, msg, None,
None, func, sinfo=stack)
qt.handle(record)

View File

@ -36,8 +36,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
os.pardir))
import qutebrowser
from qutebrowser.utils import utils
from scripts import utils as scriptutils
from scripts import utils
# from scripts.dev import update_3rdparty
@ -71,7 +70,7 @@ def call_tox(toxenv, *args, python=sys.executable):
def run_asciidoc2html(args):
"""Common buildsteps used for all OS'."""
scriptutils.print_title("Running asciidoc2html.py")
utils.print_title("Running asciidoc2html.py")
if args.asciidoc is not None:
a2h_args = ['--asciidoc'] + args.asciidoc
else:
@ -128,7 +127,7 @@ def patch_mac_app():
def build_mac():
"""Build macOS .dmg/.app."""
scriptutils.print_title("Cleaning up...")
utils.print_title("Cleaning up...")
for f in ['wc.dmg', 'template.dmg']:
try:
os.remove(f)
@ -136,20 +135,20 @@ def build_mac():
pass
for d in ['dist', 'build']:
shutil.rmtree(d, ignore_errors=True)
scriptutils.print_title("Updating 3rdparty content")
utils.print_title("Updating 3rdparty content")
# Currently disabled because QtWebEngine has no pdfjs support
# update_3rdparty.run(ace=False, pdfjs=True, fancy_dmg=False)
scriptutils.print_title("Building .app via pyinstaller")
utils.print_title("Building .app via pyinstaller")
call_tox('pyinstaller', '-r')
scriptutils.print_title("Patching .app")
utils.print_title("Patching .app")
patch_mac_app()
scriptutils.print_title("Building .dmg")
utils.print_title("Building .dmg")
subprocess.check_call(['make', '-f', 'scripts/dev/Makefile-dmg'])
dmg_name = 'qutebrowser-{}.dmg'.format(qutebrowser.__version__)
os.rename('qutebrowser.dmg', dmg_name)
scriptutils.print_title("Running smoke test")
utils.print_title("Running smoke test")
try:
with tempfile.TemporaryDirectory() as tmpdir:
@ -178,11 +177,11 @@ def patch_windows(out_dir):
def build_windows():
"""Build windows executables/setups."""
scriptutils.print_title("Updating 3rdparty content")
utils.print_title("Updating 3rdparty content")
# Currently disabled because QtWebEngine has no pdfjs support
# update_3rdparty.run(ace=False, pdfjs=True, fancy_dmg=False)
scriptutils.print_title("Building Windows binaries")
utils.print_title("Building Windows binaries")
parts = str(sys.version_info.major), str(sys.version_info.minor)
ver = ''.join(parts)
python_x86 = r'C:\Python{}-32\python.exe'.format(ver)
@ -195,19 +194,19 @@ def build_windows():
artifacts = []
scriptutils.print_title("Running pyinstaller 32bit")
utils.print_title("Running pyinstaller 32bit")
_maybe_remove(out_32)
call_tox('pyinstaller', '-r', python=python_x86)
shutil.move(out_pyinstaller, out_32)
patch_windows(out_32)
scriptutils.print_title("Running pyinstaller 64bit")
utils.print_title("Running pyinstaller 64bit")
_maybe_remove(out_64)
call_tox('pyinstaller', '-r', python=python_x64)
shutil.move(out_pyinstaller, out_64)
patch_windows(out_64)
scriptutils.print_title("Building installers")
utils.print_title("Building installers")
subprocess.check_call(['makensis.exe',
'/DVERSION={}'.format(qutebrowser.__version__),
'misc/qutebrowser.nsi'])
@ -228,12 +227,12 @@ def build_windows():
'Windows 64bit installer'),
]
scriptutils.print_title("Running 32bit smoke test")
utils.print_title("Running 32bit smoke test")
smoke_test(os.path.join(out_32, 'qutebrowser.exe'))
scriptutils.print_title("Running 64bit smoke test")
utils.print_title("Running 64bit smoke test")
smoke_test(os.path.join(out_64, 'qutebrowser.exe'))
scriptutils.print_title("Zipping 32bit standalone...")
utils.print_title("Zipping 32bit standalone...")
name = 'qutebrowser-{}-windows-standalone-win32'.format(
qutebrowser.__version__)
shutil.make_archive(name, 'zip', 'dist', os.path.basename(out_32))
@ -241,7 +240,7 @@ def build_windows():
'application/zip',
'Windows 32bit standalone'))
scriptutils.print_title("Zipping 64bit standalone...")
utils.print_title("Zipping 64bit standalone...")
name = 'qutebrowser-{}-windows-standalone-amd64'.format(
qutebrowser.__version__)
shutil.make_archive(name, 'zip', 'dist', os.path.basename(out_64))
@ -254,7 +253,7 @@ def build_windows():
def build_sdist():
"""Build an sdist and list the contents."""
scriptutils.print_title("Building sdist")
utils.print_title("Building sdist")
_maybe_remove('dist')
@ -277,10 +276,10 @@ def build_sdist():
assert '.pyc' not in by_ext
scriptutils.print_title("sdist contents")
utils.print_title("sdist contents")
for ext, files in sorted(by_ext.items()):
scriptutils.print_subtitle(ext)
utils.print_subtitle(ext)
print('\n'.join(files))
filename = 'qutebrowser-{}.tar.gz'.format(qutebrowser.__version__)
@ -309,7 +308,7 @@ def github_upload(artifacts, tag):
tag: The name of the release tag
"""
import github3
scriptutils.print_title("Uploading to github...")
utils.print_title("Uploading to github...")
token = read_github_token()
gh = github3.login(token=token)
@ -344,7 +343,7 @@ def main():
parser.add_argument('--upload', help="Tag to upload the release for",
nargs=1, required=False, metavar='TAG')
args = parser.parse_args()
scriptutils.change_cwd()
utils.change_cwd()
upload_to_pypi = False
@ -354,7 +353,8 @@ def main():
import github3 # pylint: disable=unused-variable
read_github_token()
if utils.is_windows:
run_asciidoc2html(args)
if os.name == 'nt':
if sys.maxsize > 2**32:
# WORKAROUND
print("Due to a python/Windows bug, this script needs to be run ")
@ -363,21 +363,24 @@ def main():
print("See http://bugs.python.org/issue24493 and ")
print("https://github.com/pypa/virtualenv/issues/774")
sys.exit(1)
run_asciidoc2html(args)
artifacts = build_windows()
elif utils.is_mac:
run_asciidoc2html(args)
elif sys.platform == 'darwin':
artifacts = build_mac()
else:
artifacts = build_sdist()
upload_to_pypi = True
if args.upload is not None:
scriptutils.print_title("Press enter to release...")
utils.print_title("Press enter to release...")
input()
github_upload(artifacts, args.upload[0])
if upload_to_pypi:
pypi_upload(artifacts)
else:
print()
utils.print_title("Artifacts")
for artifact in artifacts:
print(artifact)
if __name__ == '__main__':

View File

@ -141,6 +141,10 @@ PERFECT_FILES = [
'config/configfiles.py'),
('tests/unit/config/test_configtypes.py',
'config/configtypes.py'),
('tests/unit/config/test_configinit.py',
'config/configinit.py'),
('tests/unit/config/test_configcommands.py',
'config/configcommands.py'),
('tests/unit/utils/test_qtutils.py',
'utils/qtutils.py'),

View File

@ -5,7 +5,7 @@ if [[ $DOCKER ]]; then
elif [[ $TESTENV == eslint ]]; then
# Can't run this via tox as we can't easily install tox in the javascript travis env
cd qutebrowser/javascript || exit 1
eslint --color .
eslint --color --report-unused-disable-directives .
else
args=()
[[ $TRAVIS_OS_NAME == osx ]] && args=('--qute-bdd-webengine' '--no-xvfb')

View File

@ -41,7 +41,7 @@ from qutebrowser.browser import qutescheme
from qutebrowser.config import configtypes
def whitelist_generator():
def whitelist_generator(): # noqa
"""Generator which yields lines to add to a vulture whitelist."""
# qutebrowser commands
for cmd in cmdutils.cmd_dict.values():
@ -108,6 +108,8 @@ def whitelist_generator():
yield 'qutebrowser.config.configexc.ConfigErrorDesc.traceback'
yield 'qutebrowser.config.configfiles.ConfigAPI.load_autoconfig'
yield 'types.ModuleType.c' # configfiles:read_config_py
for name in ['configdir', 'datadir']:
yield 'qutebrowser.config.configfiles.ConfigAPI.' + name
yield 'include_aliases'

View File

@ -416,8 +416,8 @@ def _generate_setting_option(f, opt):
f.write("=== {}".format(opt.name) + "\n")
f.write(opt.description + "\n")
f.write("\n")
f.write('Type: <<types,{typ}>>\n'.format(
typ=opt.typ.__class__.__name__))
typ = opt.typ.get_name().replace(',', '&#44;')
f.write('Type: <<types,{typ}>>\n'.format(typ=typ))
f.write("\n")
valid_values = opt.typ.get_valid_values()

View File

@ -18,11 +18,9 @@
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Data used by setup.py and scripts/freeze.py."""
"""Data used by setup.py and the PyInstaller qutebrowser.spec."""
import sys
import re
import ast
import os
import os.path
import subprocess
@ -30,42 +28,16 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
if sys.hexversion >= 0x03000000:
_open = open
open_file = open
else:
import codecs
_open = codecs.open
open_file = codecs.open
BASEDIR = os.path.join(os.path.dirname(os.path.realpath(__file__)),
os.path.pardir)
def read_file(name):
"""Get the string contained in the file named name."""
with _open(name, 'r', encoding='utf-8') as f:
return f.read()
def _get_constant(name):
"""Read a __magic__ constant from qutebrowser/__init__.py.
We don't import qutebrowser here because it can go wrong for multiple
reasons. Instead we use re/ast to get the value directly from the source
file.
Args:
name: The name of the argument to get.
Return:
The value of the argument.
"""
field_re = re.compile(r'__{}__\s+=\s+(.*)'.format(re.escape(name)))
path = os.path.join(BASEDIR, 'qutebrowser', '__init__.py')
line = field_re.search(read_file(path)).group(1)
value = ast.literal_eval(line)
return value
def _git_str():
"""Try to find out git version.
@ -95,37 +67,5 @@ def write_git_file():
if gitstr is None:
gitstr = ''
path = os.path.join(BASEDIR, 'qutebrowser', 'git-commit-id')
with _open(path, 'w', encoding='ascii') as f:
with open_file(path, 'w', encoding='ascii') as f:
f.write(gitstr)
setupdata = {
'name': 'qutebrowser',
'version': '.'.join(str(e) for e in _get_constant('version_info')),
'description': _get_constant('description'),
'long_description': read_file('README.asciidoc'),
'url': 'https://www.qutebrowser.org/',
'requires': ['pypeg2', 'jinja2', 'pygments', 'PyYAML', 'attrs'],
'author': _get_constant('author'),
'author_email': _get_constant('email'),
'license': _get_constant('license'),
'classifiers': [
'Development Status :: 3 - Alpha',
'Environment :: X11 Applications :: Qt',
'Intended Audience :: End Users/Desktop',
'License :: OSI Approved :: GNU General Public License v3 or later '
'(GPLv3+)',
'Natural Language :: English',
'Operating System :: Microsoft :: Windows',
'Operating System :: Microsoft :: Windows :: Windows XP',
'Operating System :: Microsoft :: Windows :: Windows 7',
'Operating System :: POSIX :: Linux',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Topic :: Internet',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Internet :: WWW/HTTP :: Browsers',
],
'keywords': 'pyqt browser web qt webkit',
}

View File

@ -21,6 +21,8 @@
"""setuptools installer script for qutebrowser."""
import re
import ast
import os
import os.path
@ -35,6 +37,32 @@ except NameError:
BASEDIR = None
def read_file(name):
"""Get the string contained in the file named name."""
with common.open_file(name, 'r', encoding='utf-8') as f:
return f.read()
def _get_constant(name):
"""Read a __magic__ constant from qutebrowser/__init__.py.
We don't import qutebrowser here because it can go wrong for multiple
reasons. Instead we use re/ast to get the value directly from the source
file.
Args:
name: The name of the argument to get.
Return:
The value of the argument.
"""
field_re = re.compile(r'__{}__\s+=\s+(.*)'.format(re.escape(name)))
path = os.path.join(BASEDIR, 'qutebrowser', '__init__.py')
line = field_re.search(read_file(path)).group(1)
value = ast.literal_eval(line)
return value
try:
common.write_git_file()
setuptools.setup(
@ -42,10 +70,35 @@ try:
include_package_data=True,
entry_points={'gui_scripts':
['qutebrowser = qutebrowser.qutebrowser:main']},
test_suite='qutebrowser.test',
zip_safe=True,
install_requires=['pypeg2', 'jinja2', 'pygments', 'PyYAML', 'attrs'],
**common.setupdata
name='qutebrowser',
version='.'.join(str(e) for e in _get_constant('version_info')),
description=_get_constant('description'),
long_description=read_file('README.asciidoc'),
url='https://www.qutebrowser.org/',
author=_get_constant('author'),
author_email=_get_constant('email'),
license=_get_constant('license'),
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: X11 Applications :: Qt',
'Intended Audience :: End Users/Desktop',
'License :: OSI Approved :: GNU General Public License v3 or later '
'(GPLv3+)',
'Natural Language :: English',
'Operating System :: Microsoft :: Windows',
'Operating System :: POSIX :: Linux',
'Operating System :: MacOS',
'Operating System :: POSIX :: BSD',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Topic :: Internet',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Internet :: WWW/HTTP :: Browsers',
],
keywords='pyqt browser web qt webkit qtwebkit qtwebengine',
)
finally:
if BASEDIR is not None:

View File

@ -43,7 +43,7 @@ import qutebrowser.app # To register commands
# Set hypothesis settings
hypothesis.settings.register_profile('default',
hypothesis.settings(deadline=400))
hypothesis.settings(deadline=600))
hypothesis.settings.load_profile('default')

View File

@ -1,8 +0,0 @@
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
Feature: Ad blocking
Scenario: Simple adblock update
When I set up "simple" as block lists
And I run :adblock-update
Then the message "adblock: Read 1 hosts from 1 sources." should be shown

View File

@ -76,7 +76,9 @@ Feature: Using completion
And I open data/hello2.txt in a new tab
And I run :set-cmd-text -s :buffer
And I run :completion-item-focus next
And I wait for "setting text = ':buffer 0/1', *" in the log
And I run :completion-item-focus next
And I wait for "setting text = ':buffer 0/2', *" in the log
And I run :completion-item-del
Then the following tabs should be open:
- data/hello.txt (active)

View File

@ -246,7 +246,7 @@ Feature: Using hints
Scenario: Ignoring key presses after auto-following hints
When I set hints.auto_follow_timeout to 1000
And I set hints.mode to number
And I run :bind --force , message-error "This error message was triggered via a keybinding which should have been inhibited"
And I run :bind , message-error "This error message was triggered via a keybinding which should have been inhibited"
And I open data/hints/html/simple.html
And I hint with args "all"
And I press the key "f"
@ -259,7 +259,7 @@ Feature: Using hints
Scenario: Turning off auto_follow_timeout
When I set hints.auto_follow_timeout to 0
And I set hints.mode to number
And I run :bind --force , message-info "Keypress worked!"
And I run :bind , message-info "Keypress worked!"
And I open data/hints/html/simple.html
And I hint with args "all"
And I press the key "f"
@ -362,6 +362,7 @@ Feature: Using hints
And I set hints.mode to letter
And I hint with args "--mode number all"
And I press the key "s"
And I wait for "Filtering hints on 's'" in the log
And I run :follow-hint 1
Then data/numbers/7.txt should be loaded

View File

@ -21,7 +21,7 @@ Feature: Setting positional marks
Scenario: Jumping back after jumping to a particular percentage
When I run :scroll-px 10 20
And I wait until the scroll position changed to 10/20
And I run :scroll-perc 100
And I run :scroll-to-perc 100
And I wait until the scroll position changed
And I run :jump-mark "'"
And I wait until the scroll position changed to 10/20
@ -116,7 +116,7 @@ Feature: Setting positional marks
Scenario: Hovering a hint does not set the ' mark
When I run :scroll-px 30 20
And I wait until the scroll position changed to 30/20
And I run :scroll-perc 0
And I run :scroll-to-perc 0
And I wait until the scroll position changed
And I hint with args "links hover" and follow s
And I run :jump-mark "'"

View File

@ -47,6 +47,14 @@ Feature: Various utility commands.
When I run :set-cmd-text foo
Then the error "Invalid command text 'foo'." should be shown
Scenario: :set-cmd-text with run on count flag and no count
When I run :set-cmd-text --run-on-count :message-info "Hello World"
Then "message:info:86 Hello World" should not be logged
Scenario: :set-cmd-text with run on count flag and a count
When I run :set-cmd-text --run-on-count :message-info "Hello World" with count 1
Then the message "Hello World" should be shown
## :jseval
Scenario: :jseval
@ -399,6 +407,7 @@ Feature: Various utility commands.
When I open data/hello2.txt in a new tab
And I open data/hello3.txt in a new window
And I run :window-only
And I wait for "Closing window *" in the log
Then the session should look like:
windows:
- tabs:
@ -527,6 +536,11 @@ Feature: Various utility commands.
And I open data/numbers/3.txt
Then no crash should happen
Scenario: Simple adblock update
When I set up "simple" as block lists
And I run :adblock-update
Then the message "adblock: Read 1 hosts from 1 sources." should be shown
## Spellcheck
@qtwebkit_skip @qt>=5.8 @cannot_have_dict=af-ZA
@ -537,4 +551,4 @@ Feature: Various utility commands.
@qtwebkit_skip @qt>=5.8 @must_have_dict=en-US
Scenario: Set valid and installed language
When I run :set spellcheck.languages ["en-US"]
Then the option spellcheck.languages should be set to ["en-US"]
Then the option spellcheck.languages should be set to ["en-US"]

View File

@ -77,7 +77,7 @@ Feature: Special qute:// pages
When I set ignore_case to never
And I open qute://settings
# scroll to the right - the table does not fit in the default screen
And I run :scroll-perc -x 100
And I run :scroll-to-perc -x 100
And I run :jseval document.getElementById('input-ignore_case').value = ''
And I run :click-element id input-ignore_case
And I wait for "Entering mode KeyMode.insert *" in the log
@ -91,7 +91,7 @@ Feature: Special qute:// pages
Scenario: Focusing input fields in qute://settings and entering invalid value
When I open qute://settings
# scroll to the right - the table does not fit in the default screen
And I run :scroll-perc -x 100
And I run :scroll-to-perc -x 100
And I run :jseval document.getElementById('input-ignore_case').value = ''
And I run :click-element id input-ignore_case
And I wait for "Entering mode KeyMode.insert *" in the log

View File

@ -156,86 +156,86 @@ Feature: Scrolling
And I run :scroll down
Then the page should not be scrolled
## :scroll-perc
## :scroll-to-perc
Scenario: Scrolling to bottom with :scroll-perc
When I run :scroll-perc 100
Scenario: Scrolling to bottom with :scroll-to-perc
When I run :scroll-to-perc 100
Then the page should be scrolled vertically
Scenario: Scrolling to bottom and to top with :scroll-perc
When I run :scroll-perc 100
Scenario: Scrolling to bottom and to top with :scroll-to-perc
When I run :scroll-to-perc 100
And I wait until the scroll position changed
And I run :scroll-perc 0
And I run :scroll-to-perc 0
And I wait until the scroll position changed to 0/0
Then the page should not be scrolled
Scenario: Scrolling to middle with :scroll-perc
When I run :scroll-perc 50
Scenario: Scrolling to middle with :scroll-to-perc
When I run :scroll-to-perc 50
Then the page should be scrolled vertically
Scenario: Scrolling to middle with :scroll-perc (float)
When I run :scroll-perc 50.5
Scenario: Scrolling to middle with :scroll-to-perc (float)
When I run :scroll-to-perc 50.5
Then the page should be scrolled vertically
Scenario: Scrolling to middle and to top with :scroll-perc
When I run :scroll-perc 50
Scenario: Scrolling to middle and to top with :scroll-to-perc
When I run :scroll-to-perc 50
And I wait until the scroll position changed
And I run :scroll-perc 0
And I run :scroll-to-perc 0
And I wait until the scroll position changed to 0/0
Then the page should not be scrolled
Scenario: Scrolling to right with :scroll-perc
When I run :scroll-perc --horizontal 100
Scenario: Scrolling to right with :scroll-to-perc
When I run :scroll-to-perc --horizontal 100
Then the page should be scrolled horizontally
Scenario: Scrolling to right and to left with :scroll-perc
When I run :scroll-perc --horizontal 100
Scenario: Scrolling to right and to left with :scroll-to-perc
When I run :scroll-to-perc --horizontal 100
And I wait until the scroll position changed
And I run :scroll-perc --horizontal 0
And I run :scroll-to-perc --horizontal 0
And I wait until the scroll position changed to 0/0
Then the page should not be scrolled
Scenario: Scrolling to middle (horizontally) with :scroll-perc
When I run :scroll-perc --horizontal 50
Scenario: Scrolling to middle (horizontally) with :scroll-to-perc
When I run :scroll-to-perc --horizontal 50
Then the page should be scrolled horizontally
Scenario: Scrolling to middle and to left with :scroll-perc
When I run :scroll-perc --horizontal 50
Scenario: Scrolling to middle and to left with :scroll-to-perc
When I run :scroll-to-perc --horizontal 50
And I wait until the scroll position changed
And I run :scroll-perc --horizontal 0
And I run :scroll-to-perc --horizontal 0
And I wait until the scroll position changed to 0/0
Then the page should not be scrolled
Scenario: :scroll-perc without argument
When I run :scroll-perc
Scenario: :scroll-to-perc without argument
When I run :scroll-to-perc
Then the page should be scrolled vertically
Scenario: :scroll-perc without argument and --horizontal
When I run :scroll-perc --horizontal
Scenario: :scroll-to-perc without argument and --horizontal
When I run :scroll-to-perc --horizontal
Then the page should be scrolled horizontally
Scenario: :scroll-perc with count
When I run :scroll-perc with count 50
Scenario: :scroll-to-perc with count
When I run :scroll-to-perc with count 50
Then the page should be scrolled vertically
@qtwebengine_skip: Causes memory leak...
Scenario: :scroll-perc with a very big value
When I run :scroll-perc 99999999999
Scenario: :scroll-to-perc with a very big value
When I run :scroll-to-perc 99999999999
Then no crash should happen
Scenario: :scroll-perc on a page without scrolling
Scenario: :scroll-to-perc on a page without scrolling
When I open data/hello.txt
And I run :scroll-perc 20
And I run :scroll-to-perc 20
Then the page should not be scrolled
Scenario: :scroll-perc with count and argument
When I run :scroll-perc 0 with count 50
Scenario: :scroll-to-perc with count and argument
When I run :scroll-to-perc 0 with count 50
Then the page should be scrolled vertically
# https://github.com/qutebrowser/qutebrowser/issues/1821
Scenario: :scroll-perc without doctype
Scenario: :scroll-to-perc without doctype
When I open data/scroll/no_doctype.html
And I run :scroll-perc 100
And I run :scroll-to-perc 100
Then the page should be scrolled vertically
## :scroll-page
@ -280,14 +280,14 @@ Feature: Scrolling
Then the page should not be scrolled
Scenario: :scroll-page with --bottom-navigate
When I run :scroll-perc 100
When I run :scroll-to-perc 100
And I wait until the scroll position changed
And I run :scroll-page --bottom-navigate next 0 1
Then data/hello2.txt should be loaded
Scenario: :scroll-page with --bottom-navigate and zoom
When I run :zoom 200
And I run :scroll-perc 100
And I run :scroll-to-perc 100
And I wait until the scroll position changed
And I run :scroll-page --bottom-navigate next 0 1
Then data/hello2.txt should be loaded
@ -317,7 +317,7 @@ Feature: Scrolling
Scenario: Relative scroll position with a position:absolute page
When I open data/scroll/position_absolute.html
And I run :scroll-perc 100
And I run :scroll-to-perc 100
And I wait until the scroll position changed
And I run :scroll-page --bottom-navigate next 0 1
Then data/hello2.txt should be loaded

View File

@ -897,9 +897,9 @@ Feature: Tab management
# :buffer
Scenario: :buffer without args
Scenario: :buffer without args or count
When I run :buffer
Then the error "buffer: The following arguments are required: index" should be shown
Then the error "buffer: Either a count or the argument index must be specified." should be shown
Scenario: :buffer with a matching title
When I open data/title.html
@ -953,7 +953,7 @@ Feature: Tab management
And I run :buffer "99/1"
Then the error "There's no window with id 99!" should be shown
@qtwebengine_flaky
@skip # Too flaky
Scenario: :buffer with matching window index
Given I have a fresh instance
When I open data/title.html

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/>.
import json
import pytest_bdd as bdd
bdd.scenarios('misc.feature')
@ -26,3 +28,10 @@ def pdf_exists(quteproc, tmpdir, filename):
path = tmpdir / filename
data = path.read_binary()
assert data.startswith(b'%PDF')
@bdd.when(bdd.parsers.parse('I set up "{lists}" as block lists'))
def set_up_blocking(quteproc, lists, server):
url = 'http://localhost:{}/data/adblock/'.format(server.port)
urls = [url + item.strip() for item in lists.split(',')]
quteproc.set_setting('content.host_blocking.lists', json.dumps(urls))

View File

@ -458,7 +458,7 @@ class QuteProc(testprocess.Process):
__tracebackhide__ = (lambda e:
e.errisinstance(testprocess.WaitForTimeout))
xfail = self.request.node.get_marker('xfail')
if xfail and xfail.args[0]:
if xfail and (not xfail.args or xfail.args[0]):
kwargs['divisor'] = 10
else:
kwargs['divisor'] = 1
@ -494,7 +494,13 @@ class QuteProc(testprocess.Process):
if skip_texts:
pytest.skip(', '.join(skip_texts))
def _after_start(self):
def before_test(self):
"""Clear settings before every test."""
super().before_test()
self.send_cmd(':config-clear')
self._init_settings()
def _init_settings(self):
"""Adjust some qutebrowser settings after starting."""
settings = [
('messages.timeout', '0'),

View File

@ -47,6 +47,9 @@ class FakeConfig:
'--verbose': False,
}
def __init__(self):
self.webengine = False
def getoption(self, name):
return self.ARGS[name]

View File

@ -242,7 +242,8 @@ class Process(QObject):
self._after_start()
return
raise WaitForTimeout("Timed out while waiting for process start.")
raise WaitForTimeout("Timed out while waiting for process start.\n" +
_render_log(self.captured_log))
def _start(self, args, env):
"""Actually start the process."""

View File

@ -22,23 +22,18 @@
import pytest
@pytest.mark.parametrize(['file_name', 'elem_id', 'source', 'input_text',
'auto_insert'], [
('textarea.html', 'qute-textarea', 'clipboard', 'qutebrowser', 'false'),
('textarea.html', 'qute-textarea', 'keypress', 'superqutebrowser',
'false'),
('input.html', 'qute-input', 'clipboard', 'amazingqutebrowser', 'false'),
('input.html', 'qute-input', 'keypress', 'awesomequtebrowser', 'false'),
('autofocus.html', 'qute-input-autofocus', 'keypress', 'cutebrowser',
'true'),
@pytest.mark.parametrize(['file_name', 'elem_id', 'source', 'input_text'], [
('textarea.html', 'qute-textarea', 'clipboard', 'qutebrowser'),
('textarea.html', 'qute-textarea', 'keypress', 'superqutebrowser'),
('input.html', 'qute-input', 'clipboard', 'amazingqutebrowser'),
('input.html', 'qute-input', 'keypress', 'awesomequtebrowser'),
('autofocus.html', 'qute-input-autofocus', 'keypress', 'cutebrowser'),
])
@pytest.mark.parametrize('zoom', [100, 125, 250])
def test_insert_mode(file_name, elem_id, source, input_text, auto_insert, zoom,
def test_insert_mode(file_name, elem_id, source, input_text, zoom,
quteproc, request):
url_path = 'data/insert_mode_settings/html/{}'.format(file_name)
quteproc.open_path(url_path)
quteproc.set_setting('input.insert_mode.auto_load', auto_insert)
quteproc.send_cmd(':zoom {}'.format(zoom))
quteproc.send_cmd(':click-element --force-event id {}'.format(elem_id))
@ -57,6 +52,24 @@ def test_insert_mode(file_name, elem_id, source, input_text, auto_insert, zoom,
quteproc.send_cmd(':leave-mode')
@pytest.mark.parametrize('auto_load, background, insert_mode', [
(False, False, False), # auto_load disabled
(True, False, True), # enabled and foreground tab
(True, True, False), # background tab
])
def test_auto_load(quteproc, auto_load, background, insert_mode):
quteproc.set_setting('input.insert_mode.auto_load', str(auto_load))
url_path = 'data/insert_mode_settings/html/autofocus.html'
quteproc.open_path(url_path, new_bg_tab=background)
log_message = 'Entering mode KeyMode.insert (reason: *)'
if insert_mode:
quteproc.wait_for(message=log_message)
quteproc.send_cmd(':leave-mode')
else:
quteproc.ensure_not_logged(message=log_message)
def test_auto_leave_insert_mode(quteproc):
url_path = 'data/insert_mode_settings/html/autofocus.html'
quteproc.open_path(url_path)

View File

@ -319,3 +319,18 @@ def test_qute_settings_persistence(short_tmpdir, request, quteproc_new):
quteproc_new.start(args)
assert quteproc_new.get_setting('ignore_case') == 'always'
@pytest.mark.no_xvfb
@pytest.mark.no_ci
def test_force_software_rendering(request, quteproc_new):
"""Make sure we can force software rendering with -s."""
if not request.config.webengine:
pytest.skip("Only runs with QtWebEngine")
args = (_base_args(request.config) +
['--temp-basedir', '-s', 'force_software_rendering', 'true'])
quteproc_new.start(args)
quteproc_new.open_path('chrome://gpu')
message = 'Canvas: Software only, hardware acceleration unavailable'
assert message in quteproc_new.get_content()

View File

@ -336,6 +336,7 @@ class FakeCommand:
deprecated = attr.ib(False)
completion = attr.ib(None)
maxsplit = attr.ib(None)
takes_count = attr.ib(lambda: False)
class FakeTimer(QObject):
@ -417,9 +418,6 @@ class FakeYamlConfig:
self.loaded = False
self._values = {}
def load(self):
self.loaded = True
def __contains__(self, item):
return item in self._values
@ -432,6 +430,12 @@ class FakeYamlConfig:
def __getitem__(self, key):
return self._values[key]
def unset(self, name):
self._values.pop(name, None)
def clear(self):
self._values = []
class StatusBarCommandStub(QLineEdit):

View File

@ -36,8 +36,8 @@ def test_timeout(timer):
func2 = mock.Mock()
timer.timeout.connect(func)
timer.timeout.connect(func2)
assert not func.called
assert not func2.called
func.assert_not_called()
func2.assert_not_called()
timer.timeout.emit()
func.assert_called_once_with()
func2.assert_called_once_with()
@ -49,7 +49,7 @@ def test_disconnect_all(timer):
timer.timeout.connect(func)
timer.timeout.disconnect()
timer.timeout.emit()
assert not func.called
func.assert_not_called()
def test_disconnect_one(timer):
@ -58,7 +58,7 @@ def test_disconnect_one(timer):
timer.timeout.connect(func)
timer.timeout.disconnect(func)
timer.timeout.emit()
assert not func.called
func.assert_not_called()
def test_disconnect_all_invalid(timer):
@ -74,8 +74,8 @@ def test_disconnect_one_invalid(timer):
timer.timeout.connect(func1)
with pytest.raises(TypeError):
timer.timeout.disconnect(func2)
assert not func1.called
assert not func2.called
func1.assert_not_called()
func2.assert_not_called()
timer.timeout.emit()
func1.assert_called_once_with()

View File

@ -284,6 +284,22 @@ def test_import_txt(hist, data_tmpdir, monkeypatch, stubs):
assert (data_tmpdir / 'history.bak').exists()
def test_import_txt_existing_backup(hist, data_tmpdir, monkeypatch, stubs):
monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer)
histfile = data_tmpdir / 'history'
bakfile = data_tmpdir / 'history.bak'
histfile.write('12345 http://example.com/ title')
bakfile.write('12346 http://qutebrowser.org/')
hist.import_txt()
assert list(hist) == [('http://example.com/', 'title', 12345, False)]
assert not histfile.exists()
assert bakfile.read().split('\n') == ['12346 http://qutebrowser.org/',
'12345 http://example.com/ title']
@pytest.mark.parametrize('line', [
'',
'#12345 http://example.com/commented',

View File

@ -26,7 +26,6 @@ import pytest
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
from qutebrowser.browser import signalfilter
from qutebrowser.utils import objreg
class Signaller(QObject):
@ -66,18 +65,11 @@ def objects():
return Objects(signal_filter=signal_filter, signaller=signaller)
@pytest.fixture
def tabbed_browser(stubs, win_registry):
tb = stubs.TabbedBrowserStub()
objreg.register('tabbed-browser', tb, scope='window', window=0)
yield tb
objreg.delete('tabbed-browser', scope='window', window=0)
@pytest.mark.parametrize('index_of, emitted', [(0, True), (1, False)])
def test_filtering(objects, tabbed_browser, index_of, emitted):
tabbed_browser.current_index = 0
tabbed_browser.index_of = index_of
def test_filtering(objects, tabbed_browser_stubs, index_of, emitted):
browser = tabbed_browser_stubs[0]
browser.current_index = 0
browser.index_of = index_of
objects.signaller.signal.emit('foo')
if emitted:
assert objects.signaller.filtered_signal_arg == 'foo'
@ -86,9 +78,10 @@ def test_filtering(objects, tabbed_browser, index_of, emitted):
@pytest.mark.parametrize('index_of, verb', [(0, 'emitting'), (1, 'ignoring')])
def test_logging(caplog, objects, tabbed_browser, index_of, verb):
tabbed_browser.current_index = 0
tabbed_browser.index_of = index_of
def test_logging(caplog, objects, tabbed_browser_stubs, index_of, verb):
browser = tabbed_browser_stubs[0]
browser.current_index = 0
browser.index_of = index_of
with caplog.at_level(logging.DEBUG, logger='signals'):
objects.signaller.signal.emit('foo')
@ -99,9 +92,10 @@ def test_logging(caplog, objects, tabbed_browser, index_of, verb):
@pytest.mark.parametrize('index_of', [0, 1])
def test_no_logging(caplog, objects, tabbed_browser, index_of):
tabbed_browser.current_index = 0
tabbed_browser.index_of = index_of
def test_no_logging(caplog, objects, tabbed_browser_stubs, index_of):
browser = tabbed_browser_stubs[0]
browser.current_index = 0
browser.index_of = index_of
with caplog.at_level(logging.DEBUG, logger='signals'):
objects.signaller.link_hovered.emit('foo')
@ -109,9 +103,10 @@ def test_no_logging(caplog, objects, tabbed_browser, index_of):
assert not caplog.records
def test_runtime_error(objects, tabbed_browser):
def test_runtime_error(objects, tabbed_browser_stubs):
"""Test that there's no crash if indexOf() raises RuntimeError."""
tabbed_browser.current_index = 0
tabbed_browser.index_of = RuntimeError
browser = tabbed_browser_stubs[0]
browser.current_index = 0
browser.index_of = RuntimeError
objects.signaller.signal.emit('foo')
assert objects.signaller.filtered_signal_arg is None

View File

@ -25,7 +25,7 @@ import pytest
from PyQt5.QtCore import QUrl
from qutebrowser.commands import argparser, cmdexc
from qutebrowser.utils import usertypes, objreg
from qutebrowser.utils import usertypes
Enum = usertypes.enum('Enum', ['foo', 'foo_bar'])
@ -37,13 +37,6 @@ class TestArgumentParser:
def parser(self):
return argparser.ArgumentParser('foo')
@pytest.fixture
def tabbed_browser(self, stubs, win_registry):
tb = stubs.TabbedBrowserStub()
objreg.register('tabbed-browser', tb, scope='window', window=0)
yield tb
objreg.delete('tabbed-browser', scope='window', window=0)
def test_name(self, parser):
assert parser.name == 'foo'
@ -60,14 +53,14 @@ class TestArgumentParser:
match="Unrecognized arguments: --foo"):
parser.parse_args(['--foo'])
def test_help(self, parser, tabbed_browser):
def test_help(self, parser, tabbed_browser_stubs):
parser.add_argument('--help', action=argparser.HelpAction, nargs=0)
with pytest.raises(argparser.ArgumentParserExit):
parser.parse_args(['--help'])
expected_url = QUrl('qute://help/commands.html#foo')
assert tabbed_browser.opened_url == expected_url
assert tabbed_browser_stubs[1].opened_url == expected_url
@pytest.mark.parametrize('types, value, expected', [

View File

@ -22,7 +22,6 @@
import pytest
from qutebrowser.commands import runners, cmdexc
from qutebrowser.config import configtypes
class TestCommandParser:
@ -47,7 +46,6 @@ class TestCommandParser:
if not cmdline_test.cmd:
pytest.skip("Empty command")
monkeypatch.setattr(configtypes.Command, 'unvalidated', True)
config_stub.val.aliases = {'alias_name': cmdline_test.cmd}
parser = runners.CommandParser()

View File

@ -120,7 +120,7 @@ def cmdutils_patch(monkeypatch, stubs, miscmodels_patch):
@cmdutils.argument('win_id', win_id=True)
@cmdutils.argument('command', completion=miscmodels_patch.command)
def bind(key, win_id, command=None, *, mode='normal', force=False):
def bind(key, win_id, command=None, *, mode='normal'):
"""docstring."""
pass

View File

@ -103,7 +103,7 @@ def test_delete_cur_item_no_func():
parent = model.index(0, 0)
with pytest.raises(cmdexc.CommandError):
model.delete_cur_item(model.index(0, 0, parent))
assert not callback.called
callback.assert_not_called()
def test_delete_cur_item_no_cat():
@ -114,4 +114,4 @@ def test_delete_cur_item_no_cat():
model.rowsRemoved.connect(callback)
with pytest.raises(qtutils.QtValueError):
model.delete_cur_item(QModelIndex())
assert not callback.called
callback.assert_not_called()

View File

@ -242,7 +242,7 @@ def test_completion_item_del_no_selection(completionview):
completionview.set_model(model)
with pytest.raises(cmdexc.CommandError, match='No item selected!'):
completionview.completion_item_del()
assert not func.called
func.assert_not_called()
def test_resize_no_model(completionview, qtbot):

View File

@ -140,20 +140,24 @@ def test_sorting(max_items, before, after, model_validator, hist, config_stub):
def test_remove_rows(hist, model_validator):
hist.insert({'url': 'foo', 'title': 'Foo'})
hist.insert({'url': 'bar', 'title': 'Bar'})
hist.insert({'url': 'foo', 'title': 'Foo', 'last_atime': 0})
hist.insert({'url': 'bar', 'title': 'Bar', 'last_atime': 0})
cat = histcategory.HistoryCategory()
model_validator.set_model(cat)
cat.set_pattern('')
hist.delete('url', 'foo')
cat.removeRows(0, 1)
model_validator.validate([('bar', 'Bar', '')])
model_validator.validate([('bar', 'Bar', '1970-01-01')])
def test_remove_rows_fetch(hist):
"""removeRows should fetch enough data to make the current index valid."""
# we cannot use model_validator as it will fetch everything up front
hist.insert_batch({'url': [str(i) for i in range(300)]})
hist.insert_batch({
'url': [str(i) for i in range(300)],
'title': [str(i) for i in range(300)],
'last_atime': [0] * 300,
})
cat = histcategory.HistoryCategory()
cat.set_pattern('')

View File

@ -119,6 +119,7 @@ def configdata_stub(monkeypatch, configdata_init):
'normal': collections.OrderedDict([
('<ctrl+q>', 'quit'),
('ZQ', 'quit'),
('I', 'invalid'),
])
},
backends=[],
@ -538,7 +539,8 @@ def test_setting_option_completion(qtmodeltester, config_stub,
"Options": [
('aliases', 'Aliases for commands.', '{"q": "quit"}'),
('bindings.commands', 'Default keybindings',
'{"normal": {"<ctrl+q>": "quit", "ZQ": "quit"}}'),
'{"normal": {"<ctrl+q>": "quit", "ZQ": "quit", '
'"I": "invalid"}}'),
('bindings.default', 'Default keybindings',
'{"normal": {"<ctrl+q>": "quit"}}'),
]
@ -573,6 +575,25 @@ def test_bind_completion(qtmodeltester, cmdutils_stub, config_stub,
})
def test_bind_completion_invalid(cmdutils_stub, config_stub, key_config_stub,
configdata_stub, info):
"""Test command completion with an invalid command bound."""
model = configmodel.bind('I', info=info)
model.set_pattern('')
_check_completions(model, {
"Current": [
('invalid', 'Invalid command!', 'I'),
],
"Commands": [
('open', 'open a url', ''),
('q', "Alias for 'quit'", ''),
('quit', 'quit qutebrowser', 'ZQ, <ctrl+q>'),
('scroll', 'Scroll the current tab in the given direction.', '')
],
})
def test_bind_completion_no_current(qtmodeltester, cmdutils_stub, config_stub,
key_config_stub, configdata_stub, info):
"""Test keybinding completion with no current binding."""

View File

@ -1,7 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2016-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
@ -17,15 +16,14 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
import json
"""Fixtures needed in various config test files."""
import pytest_bdd as bdd
import pytest
bdd.scenarios('adblock.feature')
from qutebrowser.config import config
@bdd.when(bdd.parsers.parse('I set up "{lists}" as block lists'))
def set_up_blocking(quteproc, lists, server):
url = 'http://localhost:{}/data/adblock/'.format(server.port)
urls = [url + item.strip() for item in lists.split(',')]
quteproc.set_setting('content.host_blocking.lists', json.dumps(urls))
@pytest.fixture
def keyconf(config_stub):
config_stub.val.aliases = {}
return config.KeyConfig(config_stub)

View File

@ -18,20 +18,16 @@
"""Tests for qutebrowser.config.config."""
import sys
import copy
import types
import logging
import unittest.mock
import pytest
from PyQt5.QtCore import QObject, QUrl
from PyQt5.QtCore import QObject
from PyQt5.QtGui import QColor
from qutebrowser import qutebrowser
from qutebrowser.commands import cmdexc
from qutebrowser.config import config, configdata, configexc, configfiles
from qutebrowser.utils import objreg, usertypes
from qutebrowser.utils import usertypes
from qutebrowser.misc import objects
@ -42,18 +38,12 @@ def configdata_init():
configdata.init()
@pytest.fixture
def keyconf(config_stub):
config_stub.val.aliases = {}
return config.KeyConfig(config_stub)
class TestChangeFilter:
@pytest.fixture(autouse=True)
def cleanup_globals(self, monkeypatch):
"""Make sure config._change_filters is cleaned up."""
monkeypatch.setattr(config, '_change_filters', [])
"""Make sure config.change_filters is cleaned up."""
monkeypatch.setattr(config, 'change_filters', [])
@pytest.mark.parametrize('option', ['foobar', 'tab', 'tabss', 'tabs.'])
def test_unknown_option(self, option):
@ -65,7 +55,7 @@ class TestChangeFilter:
def test_validate(self, option):
cf = config.change_filter(option)
cf.validate()
assert cf in config._change_filters
assert cf in config.change_filters
@pytest.mark.parametrize('method', [True, False])
@pytest.mark.parametrize('option, changed, matches', [
@ -182,38 +172,24 @@ class TestKeyConfig:
config_stub.val.bindings.commands = {'normal': bindings}
assert keyconf.get_reverse_bindings_for('normal') == expected
def test_bind_invalid_command(self, keyconf):
with pytest.raises(configexc.KeybindingError,
match='Invalid command: foobar'):
keyconf.bind('a', 'foobar', mode='normal')
def test_bind_invalid_mode(self, keyconf):
with pytest.raises(configexc.KeybindingError,
match='completion-item-del: This command is only '
'allowed in command mode, not normal.'):
keyconf.bind('a', 'completion-item-del', mode='normal')
@pytest.mark.parametrize('force', [True, False])
@pytest.mark.parametrize('key', ['a', '<Ctrl-X>', 'b'])
def test_bind_duplicate(self, keyconf, config_stub, force, key):
def test_bind_duplicate(self, keyconf, config_stub, key):
config_stub.val.bindings.default = {'normal': {'a': 'nop',
'<Ctrl+x>': 'nop'}}
config_stub.val.bindings.commands = {'normal': {'b': 'nop'}}
if force:
keyconf.bind(key, 'message-info foo', mode='normal', force=True)
assert keyconf.get_command(key, 'normal') == 'message-info foo'
else:
with pytest.raises(configexc.DuplicateKeyError):
keyconf.bind(key, 'message-info foo', mode='normal')
assert keyconf.get_command(key, 'normal') == 'nop'
keyconf.bind(key, 'message-info foo', mode='normal')
assert keyconf.get_command(key, 'normal') == 'message-info foo'
@pytest.mark.parametrize('mode', ['normal', 'caret'])
def test_bind(self, keyconf, config_stub, qtbot, no_bindings, mode):
@pytest.mark.parametrize('command', [
'message-info foo',
'nop ;; wq', # https://github.com/qutebrowser/qutebrowser/issues/3002
])
def test_bind(self, keyconf, config_stub, qtbot, no_bindings,
mode, command):
config_stub.val.bindings.default = no_bindings
config_stub.val.bindings.commands = no_bindings
command = 'message-info foo'
with qtbot.wait_signal(config_stub.changed):
keyconf.bind('a', command, mode=mode)
@ -221,6 +197,16 @@ class TestKeyConfig:
assert keyconf.get_bindings_for(mode)['a'] == command
assert keyconf.get_command('a', mode) == command
def test_bind_mode_changing(self, keyconf, config_stub, no_bindings):
"""Make sure we can bind to a command which changes the mode.
https://github.com/qutebrowser/qutebrowser/issues/2989
"""
config_stub.val.bindings.default = no_bindings
config_stub.val.bindings.commands = no_bindings
keyconf.bind('a', 'set-cmd-text :nop ;; rl-beginning-of-line',
mode='normal')
@pytest.mark.parametrize('key, normalized', [
('a', 'a'), # default bindings
('b', 'b'), # custom bindings
@ -264,335 +250,18 @@ class TestKeyConfig:
keyconf.unbind('foobar', mode='normal')
class TestSetConfigCommand:
"""Tests for :set."""
@pytest.fixture
def commands(self, config_stub, keyconf):
return config.ConfigCommands(config_stub, keyconf)
@pytest.fixture
def tabbed_browser(self, stubs, win_registry):
tb = stubs.TabbedBrowserStub()
objreg.register('tabbed-browser', tb, scope='window', window=0)
yield tb
objreg.delete('tabbed-browser', scope='window', window=0)
def test_set_no_args(self, commands, tabbed_browser):
"""Run ':set'.
Should open qute://settings."""
commands.set(win_id=0)
assert tabbed_browser.opened_url == QUrl('qute://settings')
def test_get(self, config_stub, commands, message_mock):
"""Run ':set url.auto_search?'.
Should show the value.
"""
config_stub.val.url.auto_search = 'never'
commands.set(win_id=0, option='url.auto_search?')
msg = message_mock.getmsg(usertypes.MessageLevel.info)
assert msg.text == 'url.auto_search = never'
@pytest.mark.parametrize('temp', [True, False])
@pytest.mark.parametrize('option, old_value, inp, new_value', [
('url.auto_search', 'naive', 'dns', 'dns'),
# https://github.com/qutebrowser/qutebrowser/issues/2962
('editor.command', ['gvim', '-f', '{}'], '[emacs, "{}"]',
['emacs', '{}']),
])
def test_set_simple(self, monkeypatch, commands, config_stub,
temp, option, old_value, inp, new_value):
"""Run ':set [-t] option value'.
Should set the setting accordingly.
"""
monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebKit)
assert config_stub.get(option) == old_value
commands.set(0, option, inp, temp=temp)
assert config_stub.get(option) == new_value
if temp:
assert option not in config_stub._yaml
else:
assert config_stub._yaml[option] == new_value
@pytest.mark.parametrize('temp', [True, False])
def test_set_temp_override(self, commands, config_stub, temp):
"""Invoking :set twice.
:set url.auto_search dns
:set -t url.auto_search never
Should set the setting accordingly.
"""
assert config_stub.val.url.auto_search == 'naive'
commands.set(0, 'url.auto_search', 'dns')
commands.set(0, 'url.auto_search', 'never', temp=True)
assert config_stub.val.url.auto_search == 'never'
assert config_stub._yaml['url.auto_search'] == 'dns'
def test_set_print(self, config_stub, commands, message_mock):
"""Run ':set -p url.auto_search never'.
Should set show the value.
"""
assert config_stub.val.url.auto_search == 'naive'
commands.set(0, 'url.auto_search', 'dns', print_=True)
assert config_stub.val.url.auto_search == 'dns'
msg = message_mock.getmsg(usertypes.MessageLevel.info)
assert msg.text == 'url.auto_search = dns'
def test_set_toggle(self, commands, config_stub):
"""Run ':set auto_save.session!'.
Should toggle the value.
"""
assert not config_stub.val.auto_save.session
commands.set(0, 'auto_save.session!')
assert config_stub.val.auto_save.session
assert config_stub._yaml['auto_save.session']
def test_set_toggle_nonbool(self, commands, config_stub):
"""Run ':set url.auto_search!'.
Should show an error
"""
assert config_stub.val.url.auto_search == 'naive'
with pytest.raises(cmdexc.CommandError, match="set: Can't toggle "
"non-bool setting url.auto_search"):
commands.set(0, 'url.auto_search!')
assert config_stub.val.url.auto_search == 'naive'
def test_set_toggle_print(self, commands, config_stub, message_mock):
"""Run ':set -p auto_save.session!'.
Should toggle the value and show the new value.
"""
commands.set(0, 'auto_save.session!', print_=True)
msg = message_mock.getmsg(usertypes.MessageLevel.info)
assert msg.text == 'auto_save.session = true'
def test_set_invalid_option(self, commands):
"""Run ':set foo bar'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError, match="set: No option 'foo'"):
commands.set(0, 'foo', 'bar')
def test_set_invalid_value(self, commands):
"""Run ':set auto_save.session blah'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError,
match="set: Invalid value 'blah' - must be a "
"boolean!"):
commands.set(0, 'auto_save.session', 'blah')
def test_set_wrong_backend(self, commands, monkeypatch):
monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine)
with pytest.raises(cmdexc.CommandError,
match="set: This setting is not available with the "
"QtWebEngine backend!"):
commands.set(0, 'content.cookies.accept', 'all')
@pytest.mark.parametrize('option', ['?', '!', 'url.auto_search'])
def test_empty(self, commands, option):
"""Run ':set ?' / ':set !' / ':set url.auto_search'.
Should show an error.
See https://github.com/qutebrowser/qutebrowser/issues/1109
"""
with pytest.raises(cmdexc.CommandError,
match="set: The following arguments are required: "
"value"):
commands.set(win_id=0, option=option)
@pytest.mark.parametrize('suffix', '?!')
def test_invalid(self, commands, suffix):
"""Run ':set foo?' / ':set foo!'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError, match="set: No option 'foo'"):
commands.set(win_id=0, option='foo' + suffix)
@pytest.mark.parametrize('initial, expected', [
# Normal cycling
('magenta', 'blue'),
# Through the end of the list
('yellow', 'green'),
# Value which is not in the list
('red', 'green'),
])
def test_cycling(self, commands, config_stub, initial, expected):
"""Run ':set' with multiple values."""
opt = 'colors.statusbar.normal.bg'
config_stub.set_obj(opt, initial)
commands.set(0, opt, 'green', 'magenta', 'blue', 'yellow')
assert config_stub.get(opt) == expected
assert config_stub._yaml[opt] == expected
class TestBindConfigCommand:
"""Tests for :bind and :unbind."""
@pytest.fixture
def commands(self, config_stub, keyconf):
return config.ConfigCommands(config_stub, keyconf)
@pytest.fixture
def no_bindings(self):
"""Get a dict with no bindings."""
return {'normal': {}}
@pytest.mark.parametrize('command', ['nop', 'nope'])
def test_bind(self, commands, config_stub, no_bindings, keyconf, command):
"""Simple :bind test (and aliases)."""
config_stub.val.aliases = {'nope': 'nop'}
config_stub.val.bindings.default = no_bindings
config_stub.val.bindings.commands = no_bindings
commands.bind('a', command)
assert keyconf.get_command('a', 'normal') == command
yaml_bindings = config_stub._yaml['bindings.commands']['normal']
assert yaml_bindings['a'] == command
@pytest.mark.parametrize('key, mode, expected', [
# Simple
('a', 'normal', "a is bound to 'message-info a' in normal mode"),
# Alias
('b', 'normal', "b is bound to 'mib' in normal mode"),
# Custom binding
('c', 'normal', "c is bound to 'message-info c' in normal mode"),
# Special key
('<Ctrl-X>', 'normal',
"<ctrl+x> is bound to 'message-info C-x' in normal mode"),
# unbound
('x', 'normal', "x is unbound in normal mode"),
# non-default mode
('x', 'caret', "x is bound to 'nop' in caret mode"),
])
def test_bind_print(self, commands, config_stub, message_mock,
key, mode, expected):
"""Run ':bind key'.
Should print the binding.
"""
config_stub.val.aliases = {'mib': 'message-info b'}
config_stub.val.bindings.default = {
'normal': {'a': 'message-info a',
'b': 'mib',
'<Ctrl+x>': 'message-info C-x'},
'caret': {'x': 'nop'}
}
config_stub.val.bindings.commands = {
'normal': {'c': 'message-info c'}
}
commands.bind(key, mode=mode)
msg = message_mock.getmsg(usertypes.MessageLevel.info)
assert msg.text == expected
@pytest.mark.parametrize('command, mode, expected', [
('foobar', 'normal', "bind: Invalid command: foobar"),
('completion-item-del', 'normal',
"bind: completion-item-del: This command is only allowed in "
"command mode, not normal."),
('nop', 'wrongmode', "bind: Invalid mode wrongmode!"),
])
def test_bind_invalid(self, commands, command, mode, expected):
"""Run ':bind a foobar' / ':bind a completion-item-del'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError, match=expected):
commands.bind('a', command, mode=mode)
@pytest.mark.parametrize('force', [True, False])
@pytest.mark.parametrize('key', ['a', 'b', '<Ctrl-X>'])
def test_bind_duplicate(self, commands, config_stub, keyconf, force, key):
"""Run ':bind' with a key which already has been bound.'.
Also tests for https://github.com/qutebrowser/qutebrowser/issues/1544
"""
config_stub.val.bindings.default = {
'normal': {'a': 'nop', '<Ctrl+x>': 'nop'}
}
config_stub.val.bindings.commands = {
'normal': {'b': 'nop'},
}
if force:
commands.bind(key, 'message-info foo', mode='normal', force=True)
assert keyconf.get_command(key, 'normal') == 'message-info foo'
else:
with pytest.raises(cmdexc.CommandError,
match="bind: Duplicate key .* - use --force to "
"override"):
commands.bind(key, 'message-info foo', mode='normal')
assert keyconf.get_command(key, 'normal') == 'nop'
@pytest.mark.parametrize('key, normalized', [
('a', 'a'), # default bindings
('b', 'b'), # custom bindings
('c', 'c'), # :bind then :unbind
('<Ctrl-X>', '<ctrl+x>') # normalized special binding
])
def test_unbind(self, commands, keyconf, config_stub, key, normalized):
config_stub.val.bindings.default = {
'normal': {'a': 'nop', '<ctrl+x>': 'nop'},
'caret': {'a': 'nop', '<ctrl+x>': 'nop'},
}
config_stub.val.bindings.commands = {
'normal': {'b': 'nop'},
'caret': {'b': 'nop'},
}
if key == 'c':
# Test :bind and :unbind
commands.bind(key, 'nop')
commands.unbind(key)
assert keyconf.get_command(key, 'normal') is None
yaml_bindings = config_stub._yaml['bindings.commands']['normal']
if key in 'bc':
# Custom binding
assert normalized not in yaml_bindings
else:
assert yaml_bindings[normalized] is None
@pytest.mark.parametrize('key, mode, expected', [
('foobar', 'normal',
"unbind: Can't find binding 'foobar' in normal mode"),
('x', 'wrongmode', "unbind: Invalid mode wrongmode!"),
])
def test_unbind_invalid(self, commands, key, mode, expected):
"""Run ':unbind foobar' / ':unbind x wrongmode'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError, match=expected):
commands.unbind(key, mode=mode)
class TestConfig:
@pytest.fixture
def conf(self, stubs):
yaml_config = stubs.FakeYamlConfig()
def conf(self, config_tmpdir):
yaml_config = configfiles.YamlConfig()
return config.Config(yaml_config)
def test_init_save_manager(self, conf, fake_save_manager):
conf.init_save_manager(fake_save_manager)
fake_save_manager.add_saveable.assert_called_once_with(
'yaml-config', unittest.mock.ANY, unittest.mock.ANY)
def test_set_value(self, qtbot, conf, caplog):
opt = conf.get_opt('tabs.show')
with qtbot.wait_signal(conf.changed) as blocker:
@ -610,19 +279,60 @@ class TestConfig:
conf._set_value(opt, 'never')
assert conf._values['tabs.show'] == 'never'
def test_read_yaml(self, conf):
assert not conf._yaml.loaded
conf._yaml['content.plugins'] = True
@pytest.mark.parametrize('save_yaml', [True, False])
def test_unset(self, conf, qtbot, save_yaml):
name = 'tabs.show'
conf.set_obj(name, 'never', save_yaml=True)
assert conf.get(name) == 'never'
conf.read_yaml()
with qtbot.wait_signal(conf.changed):
conf.unset(name, save_yaml=save_yaml)
assert conf._yaml.loaded
assert conf._values['content.plugins'] is True
assert conf.get(name) == 'always'
if save_yaml:
assert name not in conf._yaml
else:
assert conf._yaml[name] == 'never'
def test_read_yaml_invalid(self, conf):
conf._yaml['foo.bar'] = True
def test_unset_never_set(self, conf, qtbot):
name = 'tabs.show'
assert conf.get(name) == 'always'
with qtbot.assert_not_emitted(conf.changed):
conf.unset(name)
assert conf.get(name) == 'always'
def test_unset_unknown(self, conf):
with pytest.raises(configexc.NoOptionError):
conf.read_yaml()
conf.unset('tabs')
@pytest.mark.parametrize('save_yaml', [True, False])
def test_clear(self, conf, qtbot, save_yaml):
name1 = 'tabs.show'
name2 = 'content.plugins'
conf.set_obj(name1, 'never', save_yaml=True)
conf.set_obj(name2, True, save_yaml=True)
assert conf._values[name1] == 'never'
assert conf._values[name2] is True
with qtbot.waitSignals([conf.changed, conf.changed]) as blocker:
conf.clear(save_yaml=save_yaml)
options = {e.args[0] for e in blocker.all_signals_and_args}
assert options == {name1, name2}
if save_yaml:
assert name1 not in conf._yaml
assert name2 not in conf._yaml
else:
assert conf._yaml[name1] == 'never'
assert conf._yaml[name2] is True
def test_read_yaml(self, conf):
conf._yaml['content.plugins'] = True
conf.read_yaml()
assert conf._values['content.plugins'] is True
def test_get_opt_valid(self, conf):
assert conf.get_opt('tabs.show') == configdata.DATA['tabs.show']
@ -722,6 +432,19 @@ class TestConfig:
assert not conf._mutables
assert conf.get_obj(option) == new
def test_get_mutable_twice(self, conf):
"""Get a mutable value twice."""
option = 'content.headers.custom'
obj = conf.get_obj(option, mutable=True)
obj['X-Foo'] = 'fooval'
obj2 = conf.get_obj(option, mutable=True)
obj2['X-Bar'] = 'barval'
conf.update_mutables()
expected = {'X-Foo': 'fooval', 'X-Bar': 'barval'}
assert conf.get_obj(option) == expected
def test_get_obj_unknown_mutable(self, conf):
"""Make sure we don't have unknown mutable types."""
conf._values['aliases'] = set() # This would never happen
@ -873,205 +596,3 @@ def test_set_register_stylesheet(delete, stylesheet_param, update, qtbot,
expected = 'yellow'
assert obj.rendered_stylesheet == expected
@pytest.fixture
def init_patch(qapp, fake_save_manager, monkeypatch, config_tmpdir,
data_tmpdir):
monkeypatch.setattr(configdata, 'DATA', None)
monkeypatch.setattr(configfiles, 'state', None)
monkeypatch.setattr(config, 'instance', None)
monkeypatch.setattr(config, 'key_instance', None)
monkeypatch.setattr(config, '_change_filters', [])
monkeypatch.setattr(config, '_init_errors', [])
# Make sure we get no SSL warning
monkeypatch.setattr(config.earlyinit, 'check_backend_ssl_support',
lambda _backend: None)
yield
try:
objreg.delete('config-commands')
except KeyError:
pass
@pytest.mark.parametrize('load_autoconfig', [True, False]) # noqa
@pytest.mark.parametrize('config_py', [True, 'error', False])
@pytest.mark.parametrize('invalid_yaml', ['42', 'unknown', False])
# pylint: disable=too-many-branches
def test_early_init(init_patch, config_tmpdir, caplog, fake_args,
load_autoconfig, config_py, invalid_yaml):
# Prepare files
autoconfig_file = config_tmpdir / 'autoconfig.yml'
config_py_file = config_tmpdir / 'config.py'
if invalid_yaml == '42':
autoconfig_file.write_text('42', 'utf-8', ensure=True)
elif invalid_yaml == 'unknown':
autoconfig_file.write_text('global:\n colors.foobar: magenta\n',
'utf-8', ensure=True)
else:
assert not invalid_yaml
autoconfig_file.write_text('global:\n colors.hints.fg: magenta\n',
'utf-8', ensure=True)
if config_py:
config_py_lines = ['c.colors.hints.bg = "red"']
if not load_autoconfig:
config_py_lines.append('config.load_autoconfig = False')
if config_py == 'error':
config_py_lines.append('c.foo = 42')
config_py_file.write_text('\n'.join(config_py_lines),
'utf-8', ensure=True)
with caplog.at_level(logging.ERROR):
config.early_init(fake_args)
# Check error messages
expected_errors = []
if config_py == 'error':
expected_errors.append(
"Errors occurred while reading config.py:\n"
" While setting 'foo': No option 'foo'")
if invalid_yaml and (load_autoconfig or not config_py):
error = "Errors occurred while reading autoconfig.yml:\n"
if invalid_yaml == '42':
error += " While loading data: Toplevel object is not a dict"
elif invalid_yaml == 'unknown':
error += " Error: No option 'colors.foobar'"
else:
assert False, invalid_yaml
expected_errors.append(error)
actual_errors = [str(err) for err in config._init_errors]
assert actual_errors == expected_errors
# Make sure things have been init'ed
objreg.get('config-commands')
assert isinstance(config.instance, config.Config)
assert isinstance(config.key_instance, config.KeyConfig)
# Check config values
if config_py and load_autoconfig and not invalid_yaml:
assert config.instance._values == {
'colors.hints.bg': 'red',
'colors.hints.fg': 'magenta',
}
elif config_py:
assert config.instance._values == {'colors.hints.bg': 'red'}
elif invalid_yaml:
assert config.instance._values == {}
else:
assert config.instance._values == {'colors.hints.fg': 'magenta'}
def test_early_init_invalid_change_filter(init_patch, fake_args):
config.change_filter('foobar')
with pytest.raises(configexc.NoOptionError):
config.early_init(fake_args)
@pytest.mark.parametrize('errors', [True, False])
def test_late_init(init_patch, monkeypatch, fake_save_manager, fake_args,
mocker, errors):
config.early_init(fake_args)
if errors:
err = configexc.ConfigErrorDesc("Error text", Exception("Exception"))
errs = configexc.ConfigFileErrors("config.py", [err])
monkeypatch.setattr(config, '_init_errors', [errs])
msgbox_mock = mocker.patch('qutebrowser.config.config.msgbox.msgbox',
autospec=True)
config.late_init(fake_save_manager)
fake_save_manager.add_saveable.assert_any_call(
'state-config', unittest.mock.ANY)
fake_save_manager.add_saveable.assert_any_call(
'yaml-config', unittest.mock.ANY)
if errors:
assert len(msgbox_mock.call_args_list) == 1
_call_posargs, call_kwargs = msgbox_mock.call_args_list[0]
text = call_kwargs['text'].strip()
assert text.startswith('Errors occurred while reading config.py:')
assert '<b>Error text</b>: Exception' in text
else:
assert not msgbox_mock.called
class TestQtArgs:
@pytest.fixture
def parser(self, mocker):
"""Fixture to provide an argparser.
Monkey-patches .exit() of the argparser so it doesn't exit on errors.
"""
parser = qutebrowser.get_argparser()
mocker.patch.object(parser, 'exit', side_effect=Exception)
return parser
@pytest.mark.parametrize('args, expected', [
# No Qt arguments
(['--debug'], [sys.argv[0]]),
# Qt flag
(['--debug', '--qt-flag', 'reverse'], [sys.argv[0], '--reverse']),
# Qt argument with value
(['--qt-arg', 'stylesheet', 'foo'],
[sys.argv[0], '--stylesheet', 'foo']),
# --qt-arg given twice
(['--qt-arg', 'stylesheet', 'foo', '--qt-arg', 'geometry', 'bar'],
[sys.argv[0], '--stylesheet', 'foo', '--geometry', 'bar']),
# --qt-flag given twice
(['--qt-flag', 'foo', '--qt-flag', 'bar'],
[sys.argv[0], '--foo', '--bar']),
])
def test_qt_args(self, config_stub, args, expected, parser):
"""Test commandline with no Qt arguments given."""
parsed = parser.parse_args(args)
assert config.qt_args(parsed) == expected
def test_qt_both(self, config_stub, parser):
"""Test commandline with a Qt argument and flag."""
args = parser.parse_args(['--qt-arg', 'stylesheet', 'foobar',
'--qt-flag', 'reverse'])
qt_args = config.qt_args(args)
assert qt_args[0] == sys.argv[0]
assert '--reverse' in qt_args
assert '--stylesheet' in qt_args
assert 'foobar' in qt_args
def test_with_settings(self, config_stub, parser):
parsed = parser.parse_args(['--qt-flag', 'foo'])
config_stub.val.qt_args = ['bar']
assert config.qt_args(parsed) == [sys.argv[0], '--foo', '--bar']
@pytest.mark.parametrize('arg, confval, can_import, is_new_webkit, used', [
# overridden by commandline arg
('webkit', 'auto', False, False, usertypes.Backend.QtWebKit),
# overridden by config
(None, 'webkit', False, False, usertypes.Backend.QtWebKit),
# WebKit available but too old
(None, 'auto', True, False, usertypes.Backend.QtWebEngine),
# WebKit available and new
(None, 'auto', True, True, usertypes.Backend.QtWebKit),
# WebKit unavailable
(None, 'auto', False, False, usertypes.Backend.QtWebEngine),
])
def test_get_backend(monkeypatch, fake_args, config_stub,
arg, confval, can_import, is_new_webkit, used):
real_import = __import__
def fake_import(name, *args, **kwargs):
if name != 'PyQt5.QtWebKit':
return real_import(name, *args, **kwargs)
if can_import:
return None
raise ImportError
fake_args.backend = arg
config_stub.val.backend = confval
monkeypatch.setattr(config.qtutils, 'is_new_qtwebkit',
lambda: is_new_webkit)
monkeypatch.setattr('builtins.__import__', fake_import)
assert config.get_backend(fake_args) == used

View File

@ -0,0 +1,479 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Tests for qutebrowser.config.configcommands."""
import logging
import unittest.mock
import pytest
from PyQt5.QtCore import QUrl, QProcess
from qutebrowser.config import configcommands
from qutebrowser.commands import cmdexc
from qutebrowser.utils import usertypes
from qutebrowser.misc import objects
@pytest.fixture
def commands(config_stub, keyconf):
return configcommands.ConfigCommands(config_stub, keyconf)
class TestSet:
"""Tests for :set."""
def test_set_no_args(self, commands, tabbed_browser_stubs):
"""Run ':set'.
Should open qute://settings."""
commands.set(win_id=0)
assert tabbed_browser_stubs[0].opened_url == QUrl('qute://settings')
def test_get(self, config_stub, commands, message_mock):
"""Run ':set url.auto_search?'.
Should show the value.
"""
config_stub.val.url.auto_search = 'never'
commands.set(win_id=0, option='url.auto_search?')
msg = message_mock.getmsg(usertypes.MessageLevel.info)
assert msg.text == 'url.auto_search = never'
@pytest.mark.parametrize('temp', [True, False])
@pytest.mark.parametrize('option, old_value, inp, new_value', [
('url.auto_search', 'naive', 'dns', 'dns'),
# https://github.com/qutebrowser/qutebrowser/issues/2962
('editor.command', ['gvim', '-f', '{}'], '[emacs, "{}"]',
['emacs', '{}']),
])
def test_set_simple(self, monkeypatch, commands, config_stub,
temp, option, old_value, inp, new_value):
"""Run ':set [-t] option value'.
Should set the setting accordingly.
"""
monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebKit)
assert config_stub.get(option) == old_value
commands.set(0, option, inp, temp=temp)
assert config_stub.get(option) == new_value
if temp:
assert option not in config_stub._yaml
else:
assert config_stub._yaml[option] == new_value
@pytest.mark.parametrize('temp', [True, False])
def test_set_temp_override(self, commands, config_stub, temp):
"""Invoking :set twice.
:set url.auto_search dns
:set -t url.auto_search never
Should set the setting accordingly.
"""
assert config_stub.val.url.auto_search == 'naive'
commands.set(0, 'url.auto_search', 'dns')
commands.set(0, 'url.auto_search', 'never', temp=True)
assert config_stub.val.url.auto_search == 'never'
assert config_stub._yaml['url.auto_search'] == 'dns'
def test_set_print(self, config_stub, commands, message_mock):
"""Run ':set -p url.auto_search never'.
Should set show the value.
"""
assert config_stub.val.url.auto_search == 'naive'
commands.set(0, 'url.auto_search', 'dns', print_=True)
assert config_stub.val.url.auto_search == 'dns'
msg = message_mock.getmsg(usertypes.MessageLevel.info)
assert msg.text == 'url.auto_search = dns'
def test_set_invalid_option(self, commands):
"""Run ':set foo bar'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError, match="set: No option 'foo'"):
commands.set(0, 'foo', 'bar')
def test_set_invalid_value(self, commands):
"""Run ':set auto_save.session blah'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError,
match="set: Invalid value 'blah' - must be a "
"boolean!"):
commands.set(0, 'auto_save.session', 'blah')
def test_set_wrong_backend(self, commands, monkeypatch):
monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine)
with pytest.raises(cmdexc.CommandError,
match="set: This setting is not available with the "
"QtWebEngine backend!"):
commands.set(0, 'content.cookies.accept', 'all')
@pytest.mark.parametrize('option', ['?', '!', 'url.auto_search'])
def test_empty(self, commands, option):
"""Run ':set ?' / ':set !' / ':set url.auto_search'.
Should show an error.
See https://github.com/qutebrowser/qutebrowser/issues/1109
"""
with pytest.raises(cmdexc.CommandError,
match="set: The following arguments are required: "
"value"):
commands.set(win_id=0, option=option)
def test_invalid(self, commands):
"""Run ':set foo?'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError, match="set: No option 'foo'"):
commands.set(win_id=0, option='foo?')
class TestCycle:
"""Test :config-cycle."""
@pytest.mark.parametrize('initial, expected', [
# Normal cycling
('magenta', 'blue'),
# Through the end of the list
('yellow', 'green'),
# Value which is not in the list
('red', 'green'),
])
def test_cycling(self, commands, config_stub, initial, expected):
"""Run ':set' with multiple values."""
opt = 'colors.statusbar.normal.bg'
config_stub.set_obj(opt, initial)
commands.config_cycle(opt, 'green', 'magenta', 'blue', 'yellow')
assert config_stub.get(opt) == expected
assert config_stub._yaml[opt] == expected
def test_different_representation(self, commands, config_stub):
"""When using a different representation, cycling should work.
For example, we use [foo] which is represented as ["foo"].
"""
opt = 'qt_args'
config_stub.set_obj(opt, ['foo'])
commands.config_cycle(opt, '[foo]', '[bar]')
assert config_stub.get(opt) == ['bar']
commands.config_cycle(opt, '[foo]', '[bar]')
assert config_stub.get(opt) == ['foo']
def test_toggle(self, commands, config_stub):
"""Run ':config-cycle auto_save.session'.
Should toggle the value.
"""
assert not config_stub.val.auto_save.session
commands.config_cycle('auto_save.session')
assert config_stub.val.auto_save.session
assert config_stub._yaml['auto_save.session']
@pytest.mark.parametrize('args', [
['url.auto_search'], ['url.auto_search', 'foo']
])
def test_toggle_nonbool(self, commands, config_stub, args):
"""Run :config-cycle without a bool and 0/1 value.
:config-cycle url.auto_search
:config-cycle url.auto_search foo
Should show an error.
"""
assert config_stub.val.url.auto_search == 'naive'
with pytest.raises(cmdexc.CommandError, match="Need at least "
"two values for non-boolean settings."):
commands.config_cycle(*args)
assert config_stub.val.url.auto_search == 'naive'
def test_set_toggle_print(self, commands, config_stub, message_mock):
"""Run ':config-cycle -p auto_save.session'.
Should toggle the value and show the new value.
"""
commands.config_cycle('auto_save.session', print_=True)
msg = message_mock.getmsg(usertypes.MessageLevel.info)
assert msg.text == 'auto_save.session = true'
class TestUnsetAndClear:
"""Test :config-unset and :config-clear."""
@pytest.mark.parametrize('temp', [True, False])
def test_unset(self, commands, config_stub, temp):
name = 'tabs.show'
config_stub.set_obj(name, 'never', save_yaml=True)
commands.config_unset(name, temp=temp)
assert config_stub.get(name) == 'always'
if temp:
assert config_stub._yaml[name] == 'never'
else:
assert name not in config_stub._yaml
def test_unset_unknown_option(self, commands):
with pytest.raises(cmdexc.CommandError, match="No option 'tabs'"):
commands.config_unset('tabs')
@pytest.mark.parametrize('save', [True, False])
def test_clear(self, commands, config_stub, save):
name = 'tabs.show'
config_stub.set_obj(name, 'never', save_yaml=True)
commands.config_clear(save=save)
assert config_stub.get(name) == 'always'
if save:
assert name not in config_stub._yaml
else:
assert config_stub._yaml[name] == 'never'
class TestSource:
"""Test :config-source."""
pytestmark = pytest.mark.usefixtures('config_tmpdir', 'data_tmpdir')
@pytest.mark.parametrize('use_default_dir', [True, False])
@pytest.mark.parametrize('clear', [True, False])
def test_config_source(self, tmpdir, commands, config_stub, config_tmpdir,
use_default_dir, clear):
assert config_stub.val.content.javascript.enabled
config_stub.val.ignore_case = 'always'
if use_default_dir:
pyfile = config_tmpdir / 'config.py'
arg = None
else:
pyfile = tmpdir / 'sourced.py'
arg = str(pyfile)
pyfile.write_text('c.content.javascript.enabled = False\n',
encoding='utf-8')
commands.config_source(arg, clear=clear)
assert not config_stub.val.content.javascript.enabled
assert config_stub.val.ignore_case == ('smart' if clear else 'always')
def test_errors(self, commands, config_tmpdir):
pyfile = config_tmpdir / 'config.py'
pyfile.write_text('c.foo = 42', encoding='utf-8')
with pytest.raises(cmdexc.CommandError) as excinfo:
commands.config_source()
expected = ("Errors occurred while reading config.py:\n"
" While setting 'foo': No option 'foo'")
assert str(excinfo.value) == expected
class TestEdit:
"""Tests for :config-edit."""
def test_no_source(self, commands, mocker, config_tmpdir):
mock = mocker.patch('qutebrowser.config.configcommands.editor.'
'ExternalEditor._start_editor', autospec=True)
commands.config_edit(no_source=True)
mock.assert_called_once_with(unittest.mock.ANY)
@pytest.fixture
def patch_editor(self, mocker, config_tmpdir, data_tmpdir):
"""Write a config.py file."""
def do_patch(text):
def _write_file(editor_self):
with open(editor_self._filename, 'w', encoding='utf-8') as f:
f.write(text)
editor_self.on_proc_closed(0, QProcess.NormalExit)
return mocker.patch('qutebrowser.config.configcommands.editor.'
'ExternalEditor._start_editor', autospec=True,
side_effect=_write_file)
return do_patch
def test_with_sourcing(self, commands, config_stub, patch_editor):
assert config_stub.val.content.javascript.enabled
mock = patch_editor('c.content.javascript.enabled = False')
commands.config_edit()
mock.assert_called_once_with(unittest.mock.ANY)
assert not config_stub.val.content.javascript.enabled
def test_error(self, commands, config_stub, patch_editor, message_mock,
caplog):
patch_editor('c.foo = 42')
with caplog.at_level(logging.ERROR):
commands.config_edit()
msg = message_mock.getmsg()
expected = ("Errors occurred while reading config.py:\n"
" While setting 'foo': No option 'foo'")
assert msg.text == expected
class TestBind:
"""Tests for :bind and :unbind."""
@pytest.fixture
def no_bindings(self):
"""Get a dict with no bindings."""
return {'normal': {}}
@pytest.mark.parametrize('command', ['nop', 'nope'])
def test_bind(self, commands, config_stub, no_bindings, keyconf, command):
"""Simple :bind test (and aliases)."""
config_stub.val.aliases = {'nope': 'nop'}
config_stub.val.bindings.default = no_bindings
config_stub.val.bindings.commands = no_bindings
commands.bind('a', command)
assert keyconf.get_command('a', 'normal') == command
yaml_bindings = config_stub._yaml['bindings.commands']['normal']
assert yaml_bindings['a'] == command
@pytest.mark.parametrize('key, mode, expected', [
# Simple
('a', 'normal', "a is bound to 'message-info a' in normal mode"),
# Alias
('b', 'normal', "b is bound to 'mib' in normal mode"),
# Custom binding
('c', 'normal', "c is bound to 'message-info c' in normal mode"),
# Special key
('<Ctrl-X>', 'normal',
"<ctrl+x> is bound to 'message-info C-x' in normal mode"),
# unbound
('x', 'normal', "x is unbound in normal mode"),
# non-default mode
('x', 'caret', "x is bound to 'nop' in caret mode"),
])
def test_bind_print(self, commands, config_stub, message_mock,
key, mode, expected):
"""Run ':bind key'.
Should print the binding.
"""
config_stub.val.aliases = {'mib': 'message-info b'}
config_stub.val.bindings.default = {
'normal': {'a': 'message-info a',
'b': 'mib',
'<Ctrl+x>': 'message-info C-x'},
'caret': {'x': 'nop'}
}
config_stub.val.bindings.commands = {
'normal': {'c': 'message-info c'}
}
commands.bind(key, mode=mode)
msg = message_mock.getmsg(usertypes.MessageLevel.info)
assert msg.text == expected
def test_bind_invalid_mode(self, commands):
"""Run ':bind --mode=wrongmode nop'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError,
match='bind: Invalid mode wrongmode!'):
commands.bind('a', 'nop', mode='wrongmode')
@pytest.mark.parametrize('key', ['a', 'b', '<Ctrl-X>'])
def test_bind_duplicate(self, commands, config_stub, keyconf, key):
"""Run ':bind' with a key which already has been bound.'.
Also tests for https://github.com/qutebrowser/qutebrowser/issues/1544
"""
config_stub.val.bindings.default = {
'normal': {'a': 'nop', '<Ctrl+x>': 'nop'}
}
config_stub.val.bindings.commands = {
'normal': {'b': 'nop'},
}
commands.bind(key, 'message-info foo', mode='normal')
assert keyconf.get_command(key, 'normal') == 'message-info foo'
def test_bind_none(self, commands, config_stub):
config_stub.val.bindings.commands = None
commands.bind(',x', 'nop')
def test_unbind_none(self, commands, config_stub):
config_stub.val.bindings.commands = None
commands.unbind('H')
@pytest.mark.parametrize('key, normalized', [
('a', 'a'), # default bindings
('b', 'b'), # custom bindings
('c', 'c'), # :bind then :unbind
('<Ctrl-X>', '<ctrl+x>') # normalized special binding
])
def test_unbind(self, commands, keyconf, config_stub, key, normalized):
config_stub.val.bindings.default = {
'normal': {'a': 'nop', '<ctrl+x>': 'nop'},
'caret': {'a': 'nop', '<ctrl+x>': 'nop'},
}
config_stub.val.bindings.commands = {
'normal': {'b': 'nop'},
'caret': {'b': 'nop'},
}
if key == 'c':
# Test :bind and :unbind
commands.bind(key, 'nop')
commands.unbind(key)
assert keyconf.get_command(key, 'normal') is None
yaml_bindings = config_stub._yaml['bindings.commands']['normal']
if key in 'bc':
# Custom binding
assert normalized not in yaml_bindings
else:
assert yaml_bindings[normalized] is None
@pytest.mark.parametrize('key, mode, expected', [
('foobar', 'normal',
"unbind: Can't find binding 'foobar' in normal mode"),
('x', 'wrongmode', "unbind: Invalid mode wrongmode!"),
])
def test_unbind_invalid(self, commands, key, mode, expected):
"""Run ':unbind foobar' / ':unbind x wrongmode'.
Should show an error.
"""
with pytest.raises(cmdexc.CommandError, match=expected):
commands.unbind(key, mode=mode)

View File

@ -43,10 +43,11 @@ def test_backend_error():
assert str(e) == "This setting is not available with the QtWebKit backend!"
def test_duplicate_key_error():
e = configexc.DuplicateKeyError('asdf')
assert isinstance(e, configexc.KeybindingError)
assert str(e) == "Duplicate key asdf"
def test_desc_with_text():
"""Test ConfigErrorDesc.with_text."""
old = configexc.ConfigErrorDesc("Error text", Exception("Exception text"))
new = old.with_text("additional text")
assert str(new) == 'Error text (additional text): Exception text'
@pytest.fixture

View File

@ -19,15 +19,24 @@
"""Tests for qutebrowser.config.configfiles."""
import os
import sys
import unittest.mock
import pytest
from qutebrowser.config import config, configfiles, configexc
from qutebrowser.config import config, configfiles, configexc, configdata
from qutebrowser.utils import utils
from PyQt5.QtCore import QSettings
@pytest.fixture(autouse=True)
def configdata_init():
"""Initialize configdata if needed."""
if configdata.DATA is None:
configdata.init()
@pytest.mark.parametrize('old_data, insert, new_data', [
(None, False, '[general]\n\n[geometry]\n\n'),
('[general]\nfooled = true', False, '[general]\n\n[geometry]\n\n'),
@ -42,6 +51,7 @@ def test_state_config(fake_save_manager, data_tmpdir,
statefile.write_text(old_data, 'utf-8')
state = configfiles.StateConfig()
state.init_save_manager(fake_save_manager)
if insert:
state['general']['newval'] = '23'
@ -52,141 +62,276 @@ def test_state_config(fake_save_manager, data_tmpdir,
state._save()
assert statefile.read_text('utf-8') == new_data
fake_save_manager.add_saveable('state-config', unittest.mock.ANY)
@pytest.mark.parametrize('old_config', [
None,
'global:\n colors.hints.fg: magenta',
])
@pytest.mark.parametrize('insert', [True, False])
def test_yaml_config(fake_save_manager, config_tmpdir, old_config, insert):
autoconfig = config_tmpdir / 'autoconfig.yml'
if old_config is not None:
autoconfig.write_text(old_config, 'utf-8')
class TestYaml:
yaml = configfiles.YamlConfig()
yaml.load()
pytestmark = pytest.mark.usefixtures('config_tmpdir')
if insert:
yaml['tabs.show'] = 'never'
@pytest.fixture
def yaml(self):
return configfiles.YamlConfig()
yaml._save()
@pytest.mark.parametrize('old_config', [
None,
'global:\n colors.hints.fg: magenta',
])
@pytest.mark.parametrize('insert', [True, False])
def test_yaml_config(self, yaml, config_tmpdir, old_config, insert):
autoconfig = config_tmpdir / 'autoconfig.yml'
if old_config is not None:
autoconfig.write_text(old_config, 'utf-8')
if not insert and old_config is None:
lines = []
else:
text = autoconfig.read_text('utf-8')
lines = text.splitlines()
yaml.load()
if insert:
assert lines[0].startswith('# DO NOT edit this file by hand,')
assert 'config_version: {}'.format(yaml.VERSION) in lines
yaml['tabs.show'] = 'never'
assert 'global:' in lines
yaml._save()
print(lines)
if not insert and old_config is None:
lines = []
else:
text = autoconfig.read_text('utf-8')
lines = text.splitlines()
# WORKAROUND for https://github.com/PyCQA/pylint/issues/574
if 'magenta' in (old_config or ''): # pylint: disable=superfluous-parens
assert ' colors.hints.fg: magenta' in lines
if insert:
assert ' tabs.show: never' in lines
if insert:
assert lines[0].startswith('# DO NOT edit this file by hand,')
assert 'config_version: {}'.format(yaml.VERSION) in lines
assert 'global:' in lines
@pytest.mark.parametrize('old_config', [
None,
'global:\n colors.hints.fg: magenta',
])
@pytest.mark.parametrize('key, value', [
('colors.hints.fg', 'green'),
('colors.hints.bg', None),
('confirm_quit', True),
('confirm_quit', False),
])
def test_yaml_config_changed(fake_save_manager, config_tmpdir, old_config,
key, value):
autoconfig = config_tmpdir / 'autoconfig.yml'
if old_config is not None:
autoconfig.write_text(old_config, 'utf-8')
print(lines)
yaml = configfiles.YamlConfig()
yaml.load()
# WORKAROUND for https://github.com/PyCQA/pylint/issues/574
# pylint: disable=superfluous-parens
if 'magenta' in (old_config or ''):
assert ' colors.hints.fg: magenta' in lines
if insert:
assert ' tabs.show: never' in lines
yaml[key] = value
assert key in yaml
assert yaml[key] == value
def test_init_save_manager(self, yaml, fake_save_manager):
yaml.init_save_manager(fake_save_manager)
fake_save_manager.add_saveable.assert_called_with(
'yaml-config', unittest.mock.ANY, unittest.mock.ANY)
yaml._save()
def test_unknown_key(self, yaml, config_tmpdir):
"""An unknown setting should be deleted."""
autoconfig = config_tmpdir / 'autoconfig.yml'
autoconfig.write_text('global:\n hello: world', encoding='utf-8')
yaml = configfiles.YamlConfig()
yaml.load()
yaml.load()
yaml._save()
assert key in yaml
assert yaml[key] == value
lines = autoconfig.read_text('utf-8').splitlines()
assert ' hello:' not in lines
@pytest.mark.parametrize('old_config', [
None,
'global:\n colors.hints.fg: magenta',
])
@pytest.mark.parametrize('key, value', [
('colors.hints.fg', 'green'),
('colors.hints.bg', None),
('confirm_quit', True),
('confirm_quit', False),
])
def test_changed(self, yaml, qtbot, config_tmpdir, old_config, key, value):
autoconfig = config_tmpdir / 'autoconfig.yml'
if old_config is not None:
autoconfig.write_text(old_config, 'utf-8')
@pytest.mark.parametrize('old_config', [
None,
'global:\n colors.hints.fg: magenta',
])
def test_yaml_config_unchanged(fake_save_manager, config_tmpdir, old_config):
autoconfig = config_tmpdir / 'autoconfig.yml'
mtime = None
if old_config is not None:
autoconfig.write_text(old_config, 'utf-8')
mtime = autoconfig.stat().mtime
yaml = configfiles.YamlConfig()
yaml.load()
yaml._save()
if old_config is None:
assert not autoconfig.exists()
else:
assert autoconfig.stat().mtime == mtime
@pytest.mark.parametrize('line, text, exception', [
('%', 'While parsing', 'while scanning a directive'),
('global: 42', 'While loading data', "'global' object is not a dict"),
('foo: 42', 'While loading data',
"Toplevel object does not contain 'global' key"),
('42', 'While loading data', "Toplevel object is not a dict"),
])
def test_yaml_config_invalid(fake_save_manager, config_tmpdir,
line, text, exception):
autoconfig = config_tmpdir / 'autoconfig.yml'
autoconfig.write_text(line, 'utf-8', ensure=True)
yaml = configfiles.YamlConfig()
with pytest.raises(configexc.ConfigFileErrors) as excinfo:
yaml.load()
assert len(excinfo.value.errors) == 1
error = excinfo.value.errors[0]
assert error.text == text
assert str(error.exception).splitlines()[0] == exception
assert error.traceback is None
with qtbot.wait_signal(yaml.changed):
yaml[key] = value
assert key in yaml
assert yaml[key] == value
def test_yaml_oserror(fake_save_manager, config_tmpdir):
autoconfig = config_tmpdir / 'autoconfig.yml'
autoconfig.ensure()
autoconfig.chmod(0)
if os.access(str(autoconfig), os.R_OK):
# Docker container or similar
pytest.skip("File was still readable")
yaml._save()
yaml = configfiles.YamlConfig()
with pytest.raises(configexc.ConfigFileErrors) as excinfo:
yaml = configfiles.YamlConfig()
yaml.load()
assert len(excinfo.value.errors) == 1
error = excinfo.value.errors[0]
assert error.text == "While reading"
assert isinstance(error.exception, OSError)
assert error.traceback is None
assert key in yaml
assert yaml[key] == value
def test_iter(self, yaml):
yaml['foo'] = 23
yaml['bar'] = 42
assert list(iter(yaml)) == [('bar', 42), ('foo', 23)]
@pytest.mark.parametrize('old_config', [
None,
'global:\n colors.hints.fg: magenta',
])
def test_unchanged(self, yaml, config_tmpdir, old_config):
autoconfig = config_tmpdir / 'autoconfig.yml'
mtime = None
if old_config is not None:
autoconfig.write_text(old_config, 'utf-8')
mtime = autoconfig.stat().mtime
yaml.load()
yaml._save()
if old_config is None:
assert not autoconfig.exists()
else:
assert autoconfig.stat().mtime == mtime
@pytest.mark.parametrize('line, text, exception', [
('%', 'While parsing', 'while scanning a directive'),
('global: 42', 'While loading data', "'global' object is not a dict"),
('foo: 42', 'While loading data',
"Toplevel object does not contain 'global' key"),
('42', 'While loading data', "Toplevel object is not a dict"),
])
def test_invalid(self, yaml, config_tmpdir, line, text, exception):
autoconfig = config_tmpdir / 'autoconfig.yml'
autoconfig.write_text(line, 'utf-8', ensure=True)
with pytest.raises(configexc.ConfigFileErrors) as excinfo:
yaml.load()
assert len(excinfo.value.errors) == 1
error = excinfo.value.errors[0]
assert error.text == text
assert str(error.exception).splitlines()[0] == exception
assert error.traceback is None
def test_oserror(self, yaml, config_tmpdir):
autoconfig = config_tmpdir / 'autoconfig.yml'
autoconfig.ensure()
autoconfig.chmod(0)
if os.access(str(autoconfig), os.R_OK):
# Docker container or similar
pytest.skip("File was still readable")
with pytest.raises(configexc.ConfigFileErrors) as excinfo:
yaml.load()
assert len(excinfo.value.errors) == 1
error = excinfo.value.errors[0]
assert error.text == "While reading"
assert isinstance(error.exception, OSError)
assert error.traceback is None
def test_unset(self, yaml, qtbot, config_tmpdir):
name = 'tabs.show'
yaml[name] = 'never'
with qtbot.wait_signal(yaml.changed):
yaml.unset(name)
assert name not in yaml
def test_unset_never_set(self, yaml, qtbot, config_tmpdir):
with qtbot.assert_not_emitted(yaml.changed):
yaml.unset('tabs.show')
def test_clear(self, yaml, qtbot, config_tmpdir):
name = 'tabs.show'
yaml[name] = 'never'
with qtbot.wait_signal(yaml.changed):
yaml.clear()
assert name not in yaml
class ConfPy:
"""Helper class to get a confpy fixture."""
def __init__(self, tmpdir, filename: str = "config.py"):
self._file = tmpdir / filename
self.filename = str(self._file)
def write(self, *lines):
text = '\n'.join(lines)
self._file.write_text(text, 'utf-8', ensure=True)
def read(self, error=False):
"""Read the config.py via configfiles and check for errors."""
if error:
with pytest.raises(configexc.ConfigFileErrors) as excinfo:
configfiles.read_config_py(self.filename)
errors = excinfo.value.errors
assert len(errors) == 1
return errors[0]
else:
configfiles.read_config_py(self.filename, raising=True)
return None
def write_qbmodule(self):
self.write('import qbmodule',
'qbmodule.run(config)')
class TestConfigPyModules:
pytestmark = pytest.mark.usefixtures('config_stub', 'key_config_stub')
@pytest.fixture
def confpy(self, tmpdir, config_tmpdir, data_tmpdir):
return ConfPy(tmpdir)
@pytest.fixture
def qbmodulepy(self, tmpdir):
return ConfPy(tmpdir, filename="qbmodule.py")
@pytest.fixture(autouse=True)
def restore_sys_path(self):
old_path = sys.path.copy()
yield
sys.path = old_path
def test_bind_in_module(self, confpy, qbmodulepy, tmpdir):
qbmodulepy.write('def run(config):',
' config.bind(",a", "message-info foo", mode="normal")')
confpy.write_qbmodule()
confpy.read()
expected = {'normal': {',a': 'message-info foo'}}
assert config.instance._values['bindings.commands'] == expected
assert "qbmodule" not in sys.modules.keys()
assert tmpdir not in sys.path
def test_restore_sys_on_err(self, confpy, qbmodulepy, tmpdir):
confpy.write_qbmodule()
qbmodulepy.write('def run(config):',
' 1/0')
error = confpy.read(error=True)
assert error.text == "Unhandled exception"
assert isinstance(error.exception, ZeroDivisionError)
assert "qbmodule" not in sys.modules.keys()
assert tmpdir not in sys.path
def test_fail_on_nonexistent_module(self, confpy, qbmodulepy, tmpdir):
qbmodulepy.write('def run(config):',
' pass')
confpy.write('import foobar',
'foobar.run(config)')
error = confpy.read(error=True)
assert error.text == "Unhandled exception"
assert isinstance(error.exception, ImportError)
tblines = error.traceback.strip().splitlines()
assert tblines[0] == "Traceback (most recent call last):"
assert tblines[-1].endswith("Error: No module named 'foobar'")
def test_no_double_if_path_exists(self, confpy, qbmodulepy, tmpdir):
sys.path.insert(0, tmpdir)
confpy.write('import sys',
'if sys.path[0] in sys.path[1:]:',
' raise Exception("Path not expected")')
confpy.read()
assert sys.path.count(tmpdir) == 1
class TestConfigPy:
@ -195,26 +340,23 @@ class TestConfigPy:
pytestmark = pytest.mark.usefixtures('config_stub', 'key_config_stub')
class ConfPy:
"""Helper class to get a confpy fixture."""
def __init__(self, tmpdir):
self._confpy = tmpdir / 'config.py'
self.filename = str(self._confpy)
def write(self, *lines):
text = '\n'.join(lines)
self._confpy.write_text(text, 'utf-8', ensure=True)
def read(self):
"""Read the config.py via configfiles and check for errors."""
api = configfiles.read_config_py(self.filename)
assert not api.errors
@pytest.fixture
def confpy(self, tmpdir):
return self.ConfPy(tmpdir)
def confpy(self, tmpdir, config_tmpdir, data_tmpdir):
return ConfPy(tmpdir)
def test_assertions(self, confpy):
"""Make sure assertions in config.py work for these tests."""
confpy.write('assert False')
with pytest.raises(AssertionError):
confpy.read() # no errors=True so it gets raised
@pytest.mark.parametrize('what', ['configdir', 'datadir'])
def test_getting_dirs(self, confpy, what):
confpy.write('import pathlib',
'directory = config.{}'.format(what),
'assert isinstance(directory, pathlib.Path)',
'assert directory.exists()')
confpy.read()
@pytest.mark.parametrize('line', [
'c.colors.hints.bg = "red"',
@ -231,25 +373,15 @@ class TestConfigPy:
'config.get("colors.hints.fg")',
])
def test_get(self, confpy, set_first, get_line):
"""Test whether getting options works correctly.
We test this by doing the following:
- Set colors.hints.fg to some value (inside the config.py with
set_first, outside of it otherwise).
- In the config.py, read .fg and set .bg to the same value.
- Verify that .bg has been set correctly.
"""
"""Test whether getting options works correctly."""
# pylint: disable=bad-config-option
config.val.colors.hints.fg = 'green'
if set_first:
confpy.write('c.colors.hints.fg = "red"',
'c.colors.hints.bg = {}'.format(get_line))
expected = 'red'
'assert {} == "red"'.format(get_line))
else:
confpy.write('c.colors.hints.bg = {}'.format(get_line))
expected = 'green'
confpy.write('assert {} == "green"'.format(get_line))
confpy.read()
assert config.instance._values['colors.hints.bg'] == expected
@pytest.mark.parametrize('line, mode', [
('config.bind(",a", "message-info foo")', 'normal'),
@ -261,6 +393,29 @@ class TestConfigPy:
expected = {mode: {',a': 'message-info foo'}}
assert config.instance._values['bindings.commands'] == expected
def test_bind_freshly_defined_alias(self, confpy):
"""Make sure we can bind to a new alias.
https://github.com/qutebrowser/qutebrowser/issues/3001
"""
confpy.write("c.aliases['foo'] = 'message-info foo'",
"config.bind(',f', 'foo')")
confpy.read()
def test_bind_duplicate_key(self, confpy):
"""Make sure overriding a keybinding works."""
confpy.write("config.bind('H', 'message-info back')")
confpy.read()
expected = {'normal': {'H': 'message-info back'}}
assert config.instance._values['bindings.commands'] == expected
def test_bind_none(self, confpy):
confpy.write("c.bindings.commands = None",
"config.bind(',x', 'nop')")
confpy.read()
expected = {'normal': {',x': 'nop'}}
assert config.instance._values['bindings.commands'] == expected
@pytest.mark.parametrize('line, key, mode', [
('config.unbind("o")', 'o', 'normal'),
('config.unbind("y", mode="prompt")', 'y', 'prompt'),
@ -278,17 +433,7 @@ class TestConfigPy:
assert config.instance._values['aliases']['foo'] == 'message-info foo'
assert config.instance._values['aliases']['bar'] == 'message-info bar'
def test_reading_default_location(self, config_tmpdir):
(config_tmpdir / 'config.py').write_text(
'c.colors.hints.bg = "red"', 'utf-8')
configfiles.read_config_py()
assert config.instance._values['colors.hints.bg'] == 'red'
def test_reading_missing_default_location(self, config_tmpdir):
assert not (config_tmpdir / 'config.py').exists()
configfiles.read_config_py() # Should not crash
def test_oserror(self, tmpdir):
def test_oserror(self, tmpdir, data_tmpdir, config_tmpdir):
with pytest.raises(configexc.ConfigFileErrors) as excinfo:
configfiles.read_config_py(str(tmpdir / 'foo'))
@ -305,7 +450,7 @@ class TestConfigPy:
assert len(excinfo.value.errors) == 1
error = excinfo.value.errors[0]
assert isinstance(error.exception, (TypeError, ValueError))
assert isinstance(error.exception, ValueError)
assert error.text == "Error while compiling"
exception_text = 'source code string cannot contain null bytes'
assert str(error.exception) == exception_text
@ -330,13 +475,9 @@ class TestConfigPy:
assert " ^" in tblines
def test_unhandled_exception(self, confpy):
confpy.write("config.load_autoconfig = False", "1/0")
api = configfiles.read_config_py(confpy.filename)
confpy.write("1/0")
error = confpy.read(error=True)
assert not api.load_autoconfig
assert len(api.errors) == 1
error = api.errors[0]
assert error.text == "Unhandled exception"
assert isinstance(error.exception, ZeroDivisionError)
@ -348,9 +489,8 @@ class TestConfigPy:
def test_config_val(self, confpy):
"""Using config.val should not work in config.py files."""
confpy.write("config.val.colors.hints.bg = 'red'")
api = configfiles.read_config_py(confpy.filename)
assert len(api.errors) == 1
error = api.errors[0]
error = confpy.read(error=True)
assert error.text == "Unhandled exception"
assert isinstance(error.exception, AttributeError)
message = "'ConfigAPI' object has no attribute 'val'"
@ -358,13 +498,9 @@ class TestConfigPy:
@pytest.mark.parametrize('line', ["c.foo = 42", "config.set('foo', 42)"])
def test_config_error(self, confpy, line):
confpy.write(line, "config.load_autoconfig = False")
api = configfiles.read_config_py(confpy.filename)
confpy.write(line)
error = confpy.read(error=True)
assert not api.load_autoconfig
assert len(api.errors) == 1
error = api.errors[0]
assert error.text == "While setting 'foo'"
assert isinstance(error.exception, configexc.NoOptionError)
assert str(error.exception) == "No option 'foo'"
@ -372,16 +508,20 @@ class TestConfigPy:
def test_multiple_errors(self, confpy):
confpy.write("c.foo = 42", "config.set('foo', 42)", "1/0")
api = configfiles.read_config_py(confpy.filename)
assert len(api.errors) == 3
for error in api.errors[:2]:
with pytest.raises(configexc.ConfigFileErrors) as excinfo:
configfiles.read_config_py(confpy.filename)
errors = excinfo.value.errors
assert len(errors) == 3
for error in errors[:2]:
assert error.text == "While setting 'foo'"
assert isinstance(error.exception, configexc.NoOptionError)
assert str(error.exception) == "No option 'foo'"
assert error.traceback is None
error = api.errors[2]
error = errors[2]
assert error.text == "Unhandled exception"
assert isinstance(error.exception, ZeroDivisionError)
assert error.traceback is not None

View File

@ -0,0 +1,307 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Tests for qutebrowser.config.configinit."""
import os
import sys
import logging
import unittest.mock
import pytest
from qutebrowser import qutebrowser
from qutebrowser.config import (config, configexc, configfiles, configinit,
configdata)
from qutebrowser.utils import objreg, usertypes
@pytest.fixture
def init_patch(qapp, fake_save_manager, monkeypatch, config_tmpdir,
data_tmpdir):
monkeypatch.setattr(configfiles, 'state', None)
monkeypatch.setattr(config, 'instance', None)
monkeypatch.setattr(config, 'key_instance', None)
monkeypatch.setattr(config, 'change_filters', [])
monkeypatch.setattr(configinit, '_init_errors', None)
yield
try:
objreg.delete('config-commands')
except KeyError:
pass
@pytest.fixture
def args(fake_args):
"""Arguments needed for the config to init."""
fake_args.temp_settings = []
return fake_args
@pytest.fixture(autouse=True)
def configdata_init(monkeypatch):
"""Make sure configdata is init'ed and no test re-init's it."""
if not configdata.DATA:
configdata.init()
monkeypatch.setattr(configdata, 'init', lambda: None)
class TestEarlyInit:
@pytest.mark.parametrize('config_py', [True, 'error', False])
def test_config_py(self, init_patch, config_tmpdir, caplog, args,
config_py):
"""Test loading with only a config.py."""
config_py_file = config_tmpdir / 'config.py'
if config_py:
config_py_lines = ['c.colors.hints.bg = "red"']
if config_py == 'error':
config_py_lines.append('c.foo = 42')
config_py_file.write_text('\n'.join(config_py_lines),
'utf-8', ensure=True)
with caplog.at_level(logging.ERROR):
configinit.early_init(args)
# Check error messages
expected_errors = []
if config_py == 'error':
expected_errors.append("While setting 'foo': No option 'foo'")
if configinit._init_errors is None:
actual_errors = []
else:
actual_errors = [str(err)
for err in configinit._init_errors.errors]
assert actual_errors == expected_errors
# Make sure things have been init'ed
objreg.get('config-commands')
assert isinstance(config.instance, config.Config)
assert isinstance(config.key_instance, config.KeyConfig)
# Check config values
if config_py:
assert config.instance._values == {'colors.hints.bg': 'red'}
else:
assert config.instance._values == {}
@pytest.mark.parametrize('load_autoconfig', [True, False])
@pytest.mark.parametrize('config_py', [True, 'error', False])
@pytest.mark.parametrize('invalid_yaml', ['42', 'unknown', 'wrong-type',
False])
def test_autoconfig_yml(self, init_patch, config_tmpdir, caplog, args,
load_autoconfig, config_py, invalid_yaml):
"""Test interaction between config.py and autoconfig.yml."""
# pylint: disable=too-many-locals,too-many-branches
# Prepare files
autoconfig_file = config_tmpdir / 'autoconfig.yml'
config_py_file = config_tmpdir / 'config.py'
yaml_text = {
'42': '42',
'unknown': 'global:\n colors.foobar: magenta\n',
'wrong-type': 'global:\n tabs.position: true\n',
False: 'global:\n colors.hints.fg: magenta\n',
}
autoconfig_file.write_text(yaml_text[invalid_yaml], 'utf-8',
ensure=True)
if config_py:
config_py_lines = ['c.colors.hints.bg = "red"']
if load_autoconfig:
config_py_lines.append('config.load_autoconfig()')
if config_py == 'error':
config_py_lines.append('c.foo = 42')
config_py_file.write_text('\n'.join(config_py_lines),
'utf-8', ensure=True)
with caplog.at_level(logging.ERROR):
configinit.early_init(args)
# Check error messages
expected_errors = []
if load_autoconfig or not config_py:
suffix = ' (autoconfig.yml)' if config_py else ''
if invalid_yaml == '42':
error = ("While loading data{}: Toplevel object is not a dict"
.format(suffix))
expected_errors.append(error)
elif invalid_yaml == 'wrong-type':
error = ("Error{}: Invalid value 'True' - expected a value of "
"type str but got bool.".format(suffix))
expected_errors.append(error)
if config_py == 'error':
expected_errors.append("While setting 'foo': No option 'foo'")
if configinit._init_errors is None:
actual_errors = []
else:
actual_errors = [str(err)
for err in configinit._init_errors.errors]
assert actual_errors == expected_errors
# Check config values
if config_py and load_autoconfig and not invalid_yaml:
assert config.instance._values == {
'colors.hints.bg': 'red',
'colors.hints.fg': 'magenta',
}
elif config_py:
assert config.instance._values == {'colors.hints.bg': 'red'}
elif invalid_yaml:
assert config.instance._values == {}
else:
assert config.instance._values == {'colors.hints.fg': 'magenta'}
def test_invalid_change_filter(self, init_patch, args):
config.change_filter('foobar')
with pytest.raises(configexc.NoOptionError):
configinit.early_init(args)
def test_temp_settings_valid(self, init_patch, args):
args.temp_settings = [('colors.completion.fg', 'magenta')]
configinit.early_init(args)
assert config.instance._values['colors.completion.fg'] == 'magenta'
def test_temp_settings_invalid(self, caplog, init_patch, message_mock,
args):
"""Invalid temp settings should show an error."""
args.temp_settings = [('foo', 'bar')]
with caplog.at_level(logging.ERROR):
configinit.early_init(args)
msg = message_mock.getmsg()
assert msg.level == usertypes.MessageLevel.error
assert msg.text == "set: NoOptionError - No option 'foo'"
assert 'colors.completion.fg' not in config.instance._values
def test_force_software_rendering(self, monkeypatch, init_patch, args):
"""Setting force_software_rendering should set the environment var."""
envvar = 'QT_XCB_FORCE_SOFTWARE_OPENGL'
monkeypatch.setattr(configinit.objects, 'backend',
usertypes.Backend.QtWebEngine)
monkeypatch.delenv(envvar, raising=False)
args.temp_settings = [('force_software_rendering', 'true')]
args.backend = 'webengine'
configinit.early_init(args)
assert os.environ[envvar] == '1'
@pytest.mark.parametrize('errors', [True, False])
def test_late_init(init_patch, monkeypatch, fake_save_manager, args,
mocker, errors):
configinit.early_init(args)
if errors:
err = configexc.ConfigErrorDesc("Error text", Exception("Exception"))
errs = configexc.ConfigFileErrors("config.py", [err])
monkeypatch.setattr(configinit, '_init_errors', errs)
msgbox_mock = mocker.patch('qutebrowser.config.configinit.msgbox.msgbox',
autospec=True)
configinit.late_init(fake_save_manager)
fake_save_manager.add_saveable.assert_any_call(
'state-config', unittest.mock.ANY)
fake_save_manager.add_saveable.assert_any_call(
'yaml-config', unittest.mock.ANY, unittest.mock.ANY)
if errors:
assert len(msgbox_mock.call_args_list) == 1
_call_posargs, call_kwargs = msgbox_mock.call_args_list[0]
text = call_kwargs['text'].strip()
assert text.startswith('Errors occurred while reading config.py:')
assert '<b>Error text</b>: Exception' in text
else:
assert not msgbox_mock.called
class TestQtArgs:
@pytest.fixture
def parser(self, mocker):
"""Fixture to provide an argparser.
Monkey-patches .exit() of the argparser so it doesn't exit on errors.
"""
parser = qutebrowser.get_argparser()
mocker.patch.object(parser, 'exit', side_effect=Exception)
return parser
@pytest.mark.parametrize('args, expected', [
# No Qt arguments
(['--debug'], [sys.argv[0]]),
# Qt flag
(['--debug', '--qt-flag', 'reverse'], [sys.argv[0], '--reverse']),
# Qt argument with value
(['--qt-arg', 'stylesheet', 'foo'],
[sys.argv[0], '--stylesheet', 'foo']),
# --qt-arg given twice
(['--qt-arg', 'stylesheet', 'foo', '--qt-arg', 'geometry', 'bar'],
[sys.argv[0], '--stylesheet', 'foo', '--geometry', 'bar']),
# --qt-flag given twice
(['--qt-flag', 'foo', '--qt-flag', 'bar'],
[sys.argv[0], '--foo', '--bar']),
])
def test_qt_args(self, config_stub, args, expected, parser):
"""Test commandline with no Qt arguments given."""
parsed = parser.parse_args(args)
assert configinit.qt_args(parsed) == expected
def test_qt_both(self, config_stub, parser):
"""Test commandline with a Qt argument and flag."""
args = parser.parse_args(['--qt-arg', 'stylesheet', 'foobar',
'--qt-flag', 'reverse'])
qt_args = configinit.qt_args(args)
assert qt_args[0] == sys.argv[0]
assert '--reverse' in qt_args
assert '--stylesheet' in qt_args
assert 'foobar' in qt_args
def test_with_settings(self, config_stub, parser):
parsed = parser.parse_args(['--qt-flag', 'foo'])
config_stub.val.qt_args = ['bar']
assert configinit.qt_args(parsed) == [sys.argv[0], '--foo', '--bar']
@pytest.mark.parametrize('arg, confval, used', [
# overridden by commandline arg
('webkit', 'webengine', usertypes.Backend.QtWebKit),
# set in config
(None, 'webkit', usertypes.Backend.QtWebKit),
])
def test_get_backend(monkeypatch, args, config_stub,
arg, confval, used):
real_import = __import__
def fake_import(name, *args, **kwargs):
if name != 'PyQt5.QtWebKit':
return real_import(name, *args, **kwargs)
raise ImportError
args.backend = arg
config_stub.val.backend = confval
monkeypatch.setattr('builtins.__import__', fake_import)
assert configinit.get_backend(args) == used

View File

@ -193,7 +193,8 @@ class TestAll:
if member in [configtypes.BaseType, configtypes.MappingType,
configtypes._Numeric]:
pass
elif member is configtypes.List:
elif (member is configtypes.List or
member is configtypes.ListOrValue):
yield functools.partial(member, valtype=configtypes.Int())
yield functools.partial(member, valtype=configtypes.Url())
elif member is configtypes.Dict:
@ -240,6 +241,9 @@ class TestAll:
configtypes.PercOrInt, # ditto
]:
return
if (isinstance(typ, configtypes.ListOrValue) and
isinstance(typ.valtype, configtypes.Int)):
return
assert converted == s
@ -250,7 +254,7 @@ class TestAll:
to_py_expected = configtypes.PaddingValues(None, None, None, None)
elif isinstance(typ, configtypes.Dict):
to_py_expected = {}
elif isinstance(typ, configtypes.List):
elif isinstance(typ, (configtypes.List, configtypes.ListOrValue)):
to_py_expected = []
else:
to_py_expected = None
@ -366,6 +370,10 @@ class TestBaseType:
def test_to_doc(self, klass, value, expected):
assert klass().to_doc(value) == expected
@pytest.mark.parametrize('obj', [42, '', None, 'foo'])
def test_from_obj(self, klass, obj):
assert klass(none_ok=True).from_obj(obj) == obj
class MappingSubclass(configtypes.MappingType):
@ -546,6 +554,14 @@ class TestList:
with pytest.raises(configexc.ValidationError):
klass().from_str(val)
@pytest.mark.parametrize('obj, expected', [
([1], [1]),
([], []),
(None, []),
])
def test_from_obj(self, klass, obj, expected):
assert klass(none_ok_outer=True).from_obj(obj) == expected
@pytest.mark.parametrize('val', [['foo'], ['foo', 'bar']])
def test_to_py_valid(self, klass, val):
assert klass().to_py(val) == val
@ -670,6 +686,108 @@ class TestFlagList:
assert klass().complete() is None
class TestListOrValue:
@pytest.fixture
def klass(self):
return configtypes.ListOrValue
@pytest.fixture
def strtype(self):
return configtypes.String()
@pytest.mark.parametrize('val, expected', [
('["foo"]', ['foo']),
('["foo", "bar"]', ['foo', 'bar']),
('foo', 'foo'),
])
def test_from_str(self, klass, strtype, val, expected):
assert klass(strtype).from_str(val) == expected
def test_from_str_invalid(self, klass):
valtype = configtypes.String(minlen=10)
with pytest.raises(configexc.ValidationError):
klass(valtype).from_str('123')
@pytest.mark.parametrize('val, expected', [
(['foo'], ['foo']),
('foo', ['foo']),
])
def test_to_py_valid(self, klass, strtype, val, expected):
assert klass(strtype).to_py(val) == expected
@pytest.mark.parametrize('val', [[42], ['\U00010000']])
def test_to_py_invalid(self, klass, strtype, val):
with pytest.raises(configexc.ValidationError):
klass(strtype).to_py(val)
@pytest.mark.parametrize('val', [None, ['foo', 'bar'], 'abcd'])
def test_to_py_length(self, strtype, klass, val):
klass(strtype, none_ok=True, length=2).to_py(val)
@pytest.mark.parametrize('obj, expected', [
(['a'], ['a']),
([], []),
(None, []),
])
def test_from_obj(self, klass, obj, expected):
typ = klass(none_ok=True, valtype=configtypes.String())
assert typ.from_obj(obj) == expected
@pytest.mark.parametrize('val', [['a'], ['a', 'b'], ['a', 'b', 'c', 'd']])
def test_wrong_length(self, strtype, klass, val):
with pytest.raises(configexc.ValidationError,
match='Exactly 3 values need to be set!'):
klass(strtype, length=3).to_py(val)
def test_get_name(self, strtype, klass):
assert klass(strtype).get_name() == 'List of String, or String'
def test_get_valid_values(self, klass):
valid_values = configtypes.ValidValues('foo', 'bar', 'baz')
valtype = configtypes.String(valid_values=valid_values)
assert klass(valtype).get_valid_values() == valid_values
def test_to_str(self, strtype, klass):
assert klass(strtype).to_str(["a", True]) == '["a", true]'
@hypothesis.given(val=strategies.lists(strategies.just('foo')))
def test_hypothesis(self, strtype, klass, val):
typ = klass(strtype, none_ok=True)
try:
converted = typ.to_py(val)
except configexc.ValidationError:
pass
else:
expected = converted if converted else []
assert typ.to_py(typ.from_str(typ.to_str(converted))) == expected
@hypothesis.given(val=strategies.lists(strategies.just('foo')))
def test_hypothesis_text(self, strtype, klass, val):
typ = klass(strtype)
text = json.dumps(val)
try:
typ.to_str(typ.from_str(text))
except configexc.ValidationError:
pass
@pytest.mark.parametrize('val, expected', [
# simple list
(['foo', 'bar'], '\n\n- +pass:[foo]+\n- +pass:[bar]+'),
# only one value
(['foo'], '+pass:[foo]+'),
# value without list
('foo', '+pass:[foo]+'),
# empty
([], 'empty'),
(None, 'empty'),
])
def test_to_doc(self, klass, strtype, val, expected):
doc = klass(strtype).to_doc(val)
print(doc)
assert doc == expected
class TestBool:
TESTS = {
@ -718,8 +836,10 @@ class TestBool:
def test_to_str(self, klass, val, expected):
assert klass().to_str(val) == expected
def test_to_doc(self, klass):
assert klass().to_doc(True) == '+pass:[true]+'
@pytest.mark.parametrize('value, expected', [(True, '+pass:[true]+'),
(False, '+pass:[false]+')])
def test_to_doc(self, klass, value, expected):
assert klass().to_doc(value) == expected
class TestBoolAsk:
@ -1072,37 +1192,10 @@ class TestCommand:
monkeypatch.setattr(configtypes, 'cmdutils', cmd_utils)
monkeypatch.setattr('qutebrowser.commands.runners.cmdutils', cmd_utils)
@pytest.fixture(autouse=True)
def patch_aliases(self, config_stub):
"""Patch the aliases setting."""
configtypes.Command.unvalidated = True
config_stub.val.aliases = {'alias': 'cmd1'}
configtypes.Command.unvalidated = False
@pytest.fixture
def klass(self):
return configtypes.Command
@pytest.mark.parametrize('val', ['cmd1', 'cmd2', 'cmd1 foo bar',
'cmd2 baz fish', 'alias foo'])
def test_to_py_valid(self, patch_cmdutils, klass, val):
expected = None if not val else val
assert klass().to_py(val) == expected
@pytest.mark.parametrize('val', ['cmd3', 'cmd3 foo bar', ' '])
def test_to_py_invalid(self, patch_cmdutils, klass, val):
with pytest.raises(configexc.ValidationError):
klass().to_py(val)
def test_cmdline(self, klass, cmdline_test):
"""Test some commandlines from the cmdline_test fixture."""
typ = klass()
if cmdline_test.valid:
typ.to_py(cmdline_test.cmd)
else:
with pytest.raises(configexc.ValidationError):
typ.to_py(cmdline_test.cmd)
def test_complete(self, patch_cmdutils, klass):
"""Test completion."""
items = klass().complete()
@ -1461,6 +1554,16 @@ class TestDict:
valtype=configtypes.Int())
assert typ.from_str('{"answer": 42}') == {"answer": 42}
@pytest.mark.parametrize('obj, expected', [
({'a': 'b'}, {'a': 'b'}),
({}, {}),
(None, {}),
])
def test_from_obj(self, klass, obj, expected):
d = klass(keytype=configtypes.String(), valtype=configtypes.String(),
none_ok=True)
assert d.from_obj(obj) == expected
@pytest.mark.parametrize('keytype, valtype, val', [
(configtypes.String(), configtypes.String(), {'hello': 'world'}),
(configtypes.String(), configtypes.Int(), {'hello': 42}),

View File

@ -31,6 +31,12 @@ BINDINGS = {'prompt': {'<Ctrl-a>': 'message-info ctrla',
'command': {'foo': 'message-info bar',
'<Ctrl+X>': 'message-info ctrlx'},
'normal': {'a': 'message-info a', 'ba': 'message-info ba'}}
MAPPINGS = {
'<Ctrl+a>': 'a',
'<Ctrl+b>': '<Ctrl+a>',
'x': 'a',
'b': 'a',
}
@pytest.fixture
@ -38,3 +44,4 @@ def keyinput_bindings(config_stub, key_config_stub):
"""Register some test bindings."""
config_stub.val.bindings.default = {}
config_stub.val.bindings.commands = dict(BINDINGS)
config_stub.val.bindings.key_mappings = dict(MAPPINGS)

View File

@ -91,8 +91,7 @@ class TestDebugLog:
])
def test_split_count(config_stub, input_key, supports_count, expected):
kp = basekeyparser.BaseKeyParser(0, supports_count=supports_count)
kp._keystring = input_key
assert kp._split_count() == expected
assert kp._split_count(input_key) == expected
@pytest.mark.usefixtures('keyinput_bindings')
@ -165,20 +164,14 @@ class TestSpecialKeys:
keyparser._read_config('prompt')
def test_valid_key(self, fake_keyevent_factory, keyparser):
if utils.is_mac:
modifier = Qt.MetaModifier
else:
modifier = Qt.ControlModifier
modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier
keyparser.handle(fake_keyevent_factory(Qt.Key_A, modifier))
keyparser.handle(fake_keyevent_factory(Qt.Key_X, modifier))
keyparser.execute.assert_called_once_with(
'message-info ctrla', keyparser.Type.special, None)
def test_valid_key_count(self, fake_keyevent_factory, keyparser):
if utils.is_mac:
modifier = Qt.MetaModifier
else:
modifier = Qt.ControlModifier
modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier
keyparser.handle(fake_keyevent_factory(5, text='5'))
keyparser.handle(fake_keyevent_factory(Qt.Key_A, modifier, text='A'))
keyparser.execute.assert_called_once_with(
@ -199,6 +192,22 @@ class TestSpecialKeys:
keyparser.handle(fake_keyevent_factory(Qt.Key_A, Qt.NoModifier))
assert not keyparser.execute.called
def test_mapping(self, config_stub, fake_keyevent_factory, keyparser):
modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier
keyparser.handle(fake_keyevent_factory(Qt.Key_B, modifier))
keyparser.execute.assert_called_once_with(
'message-info ctrla', keyparser.Type.special, None)
def test_binding_and_mapping(self, config_stub, fake_keyevent_factory,
keyparser):
"""with a conflicting binding/mapping, the binding should win."""
modifier = Qt.MetaModifier if utils.is_mac else Qt.ControlModifier
keyparser.handle(fake_keyevent_factory(Qt.Key_A, modifier))
keyparser.execute.assert_called_once_with(
'message-info ctrla', keyparser.Type.special, None)
class TestKeyChain:
@ -230,7 +239,7 @@ class TestKeyChain:
handle_text((Qt.Key_X, 'x'),
# Then start the real chain
(Qt.Key_B, 'b'), (Qt.Key_A, 'a'))
keyparser.execute.assert_called_once_with(
keyparser.execute.assert_called_with(
'message-info ba', keyparser.Type.chain, None)
assert keyparser._keystring == ''
@ -249,6 +258,16 @@ class TestKeyChain:
handle_text((Qt.Key_C, 'c'))
assert keyparser._keystring == ''
def test_mapping(self, config_stub, handle_text, keyparser):
handle_text((Qt.Key_X, 'x'))
keyparser.execute.assert_called_once_with(
'message-info a', keyparser.Type.chain, None)
def test_binding_and_mapping(self, config_stub, handle_text, keyparser):
"""with a conflicting binding/mapping, the binding should win."""
handle_text((Qt.Key_B, 'b'))
assert not keyparser.execute.called
class TestCount:

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