Merge branch 'master' into jay/cache-tabsize

This commit is contained in:
Jay Kamat 2017-10-20 15:24:22 -04:00
commit f6cc9d53b8
No known key found for this signature in database
GPG Key ID: 5D2E399600F4F7B5
59 changed files with 951 additions and 308 deletions

View File

@ -13,6 +13,7 @@ include qutebrowser/utils/testfile
include qutebrowser/git-commit-id
include LICENSE doc/* README.asciidoc
include misc/qutebrowser.desktop
include misc/qutebrowser.appdata.xml
include requirements.txt
include tox.ini
include qutebrowser.py

View File

@ -15,19 +15,74 @@ breaking changes (such as renamed commands) can happen in minor releases.
// `Fixed` for any bug fixes.
// `Security` to invite users to upgrade in case of vulnerabilities.
v1.0.2 (unreleased)
v1.1.0 (unreleased)
-------------------
Fixes
Added
~~~~~
- Fixed workaround for black screens with Nvidia cards
- New `{current_url}` field for `window.title_format` and `tabs.title.format`.
- New `colors.statusbar.passthrough.fg`/`.bg` settings.
- New `completion.delay` and `completion.min_chars` settings to update the
completion less often.
- New `completion.use_best_match` setting to automatically use the best-matching
command in the completion.
- New `:tab-give` and `:tab-take` commands, to give tabs to another window, or
take them from another window.
- New `config.source(...)` method for `config.py` to source another file.
- New `keyhint.radius` option to configure the edge rounding for the key hint
widget.
Fixed
~~~~~
- More consistent sizing for favicons with vertical tabs.
- Using `:home` on pinned tabs is now prevented.
- Fix crash with unknown file types loaded via qute://help
- Scrolling performance improvements
Deprecated
~~~~~~~~~~
- `:tab-detach` has been deprecated, as `:tab-give` without argument can be used
instead.
Removed
~~~~~~~
- The long-deprecated `:prompt-yes`, `:prompt-no`, `:paste-primary` and `:paste`
commands have been removed.
v1.0.3 (unreleased)
-------------------
Fixed
~~~~~
- Handle accessing a locked sqlite database gracefully
v1.0.2
------
Fixed
~~~~~
- Fix workaround for black screens or crashes with Nvidia cards
- Handle a filesystem going read-only gracefully
- Fix crash when setting `fonts.monospace`
- Fix list options not being modifyable via `.append()` in `config.py`
- Mark the content.notifications setting as QtWebKit only correctly
- Fix wrong rendering of keys like `<back>` in the completion
Changed
~~~~~~~
- Nicer error messages and other minor improvements
v1.0.1
------
Fixes
Fixed
~~~~~
- Fixed starting after customizing `fonts.tabs` or `fonts.debug_console`.
@ -65,6 +120,9 @@ Major changes
the entire browsing history. The default for
`completion.web_history_max_items` got changed to `-1` (unlimited). If the
completion is too slow on your machine, try setting it to a few 1000 items.
- Up/Down now navigates through the command history instead of selecting
completion items. Either use Tab to cycle through the completion, or
https://github.com/qutebrowser/qutebrowser/blob/master/doc/help/configuring.asciidoc#migrating-older-configurations[restore the old behavior].
Added
~~~~~

View File

@ -681,7 +681,6 @@ qutebrowser release
* Adjust `__version_info__` in `qutebrowser/__init__.py`.
* Update changelog (remove *(unreleased)*).
* Run tests again.
* Commit.
* Create annotated git tag (`git tag -s "v1.$x.$y" -m "Release v1.$x.$y"`).
@ -691,9 +690,9 @@ qutebrowser release
* Mark the milestone at https://github.com/qutebrowser/qutebrowser/milestones
as closed.
* Linux: Run `python3 scripts/dev/build_release.py --upload v1.$x.$y`.
* Windows: Run `C:\Python36-32\python scripts\dev\build_release.py --asciidoc C:\Python27\python C:\asciidoc-8.6.9\asciidoc.py --upload v1.X.Y` (replace X/Y by hand).
* macOS: Run `python3 scripts/dev/build_release.py --upload v1.X.Y` (replace X/Y by hand).
* Linux: Run `git checkout v1.$x.$y && python3 scripts/dev/build_release.py --upload v1.$x.$y`.
* Windows: Run `git checkout v1.X.Y; C:\Python36-32\python scripts\dev\build_release.py --asciidoc C:\Python27\python C:\asciidoc-8.6.9\asciidoc.py --upload v1.X.Y` (replace X/Y by hand).
* macOS: Run `git checkout v1.X.Y && python3 scripts/dev/build_release.py --upload v1.X.Y` (replace X/Y by hand).
* On server: Run `python3 scripts/dev/download_release.py v1.X.Y` (replace X/Y by hand).
* Update `qutebrowser-git` PKGBUILD if dependencies/install changed.
* Announce to qutebrowser and qutebrowser-announce mailinglist.

View File

@ -83,13 +83,14 @@ It is possible to run or bind multiple commands by separating them with `;;`.
|<<stop,stop>>|Stop loading in the current/[count]th tab.
|<<tab-clone,tab-clone>>|Duplicate the current tab.
|<<tab-close,tab-close>>|Close the current/[count]th tab.
|<<tab-detach,tab-detach>>|Detach the current tab to its own window.
|<<tab-focus,tab-focus>>|Select the tab given as argument/[count].
|<<tab-give,tab-give>>|Give the current tab to a new or existing window if win_id given.
|<<tab-move,tab-move>>|Move the current tab according to the argument and [count].
|<<tab-next,tab-next>>|Switch to the next tab, or switch [count] tabs forward.
|<<tab-only,tab-only>>|Close all tabs except for the current one.
|<<tab-pin,tab-pin>>|Pin/Unpin the current/[count]th tab.
|<<tab-prev,tab-prev>>|Switch to the previous tab, or switch [count] tabs back.
|<<tab-take,tab-take>>|Take a tab from another window.
|<<unbind,unbind>>|Unbind a keychain.
|<<undo,undo>>|Re-open a closed tab.
|<<version,version>>|Show version information.
@ -946,10 +947,6 @@ Close the current/[count]th tab.
==== count
The tab index to close
[[tab-detach]]
=== tab-detach
Detach the current tab to its own window.
[[tab-focus]]
=== tab-focus
Syntax: +:tab-focus ['index']+
@ -967,6 +964,17 @@ If neither count nor index are given, it behaves like tab-next. If both are give
==== count
The tab index to focus, starting with 1.
[[tab-give]]
=== tab-give
Syntax: +:tab-give ['win-id']+
Give the current tab to a new or existing window if win_id given.
If no win_id is given, the tab will get detached into a new window.
==== positional arguments
* +'win-id'+: The window ID of the window to give the current tab to.
[[tab-move]]
=== tab-move
Syntax: +:tab-move ['index']+
@ -1019,6 +1027,16 @@ Switch to the previous tab, or switch [count] tabs back.
==== count
How many tabs to switch back.
[[tab-take]]
=== tab-take
Syntax: +:tab-take 'index'+
Take a tab from another window.
==== positional arguments
* +'index'+: The [win_id/]index of the tab to take. Or a substring in which case the closest match will be taken.
[[unbind]]
=== unbind
Syntax: +:unbind [*--mode* 'mode'] 'key'+

View File

@ -18,8 +18,9 @@ the old defaults.
Other changes in default settings:
- `<Up>` and `<Down>` in the completion now navigate through command history
instead of selecting completion items. You can get back the old behavior by
doing:
instead of selecting completion items. Use `<Tab>`/`<Shift-Tab>` to cycle
through the completion instead.
You can get back the old behavior by doing:
+
----
:bind -f -m command <Up> completion-item-focus prev
@ -237,6 +238,9 @@ 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.
To read config data from a different file with `c` and `config` available, you
can use `config.source('otherfile.py')` in your `config.py`.
Getting the config directory
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -346,15 +350,38 @@ def bind_chained(key, *commands):
bind_chained('<Escape>', 'clear-keychain', 'search')
----
Avoiding flake8 errors
^^^^^^^^^^^^^^^^^^^^^^
Reading colors from Xresources
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If you use an editor with flake8 integration which complains about `c` and `config` being undefined, you can use:
You can use something like this to read colors from an `~/.Xresources` file:
[source,python]
----
c = c # noqa: F821
config = config # noqa: F821
def read_xresources(prefix):
props = {}
x = subprocess.run(['xrdb', '-query'], stdout=subprocess.PIPE)
lines = x.stdout.decode().split('\n')
for line in filter(lambda l : l.startswith(prefix), lines):
prop, _, value = line.partition(':\t')
props[prop] = value
return props
xresources = read_xresources('*')
c.colors.statusbar.normal.bg = xresources['*background']
----
Avoiding flake8 errors
^^^^^^^^^^^^^^^^^^^^^^
If you use an editor with flake8 and pylint integration, it may have some
complaints about invalid names, undefined variables, or missing docstrings.
You can silence those with:
[source,python]
----
# pylint: disable=C0111
c = c # noqa: F821 pylint: disable=E0602,C0103
config = config # noqa: F821 pylint: disable=E0602,C0103
----
For type annotation support (note that those imports aren't guaranteed to be
@ -362,8 +389,9 @@ stable across qutebrowser versions):
[source,python]
----
# pylint: disable=C0111
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
config = config # type: ConfigAPI # noqa: F821 pylint: disable=E0602,C0103
c = c # type: ConfigContainer # noqa: F821 pylint: disable=E0602,C0103
----

View File

@ -70,6 +70,8 @@
|<<colors.statusbar.insert.fg,colors.statusbar.insert.fg>>|Foreground color of the statusbar in insert mode.
|<<colors.statusbar.normal.bg,colors.statusbar.normal.bg>>|Background color of the statusbar.
|<<colors.statusbar.normal.fg,colors.statusbar.normal.fg>>|Foreground color of the statusbar.
|<<colors.statusbar.passthrough.bg,colors.statusbar.passthrough.bg>>|Background color of the statusbar in passthrough mode.
|<<colors.statusbar.passthrough.fg,colors.statusbar.passthrough.fg>>|Foreground color of the statusbar in passthrough mode.
|<<colors.statusbar.private.bg,colors.statusbar.private.bg>>|Background color of the statusbar in private browsing mode.
|<<colors.statusbar.private.fg,colors.statusbar.private.fg>>|Foreground color of the statusbar in private browsing mode.
|<<colors.statusbar.progress.bg,colors.statusbar.progress.bg>>|Background color of the progress bar.
@ -94,13 +96,16 @@
|<<colors.tabs.selected.odd.fg,colors.tabs.selected.odd.fg>>|Foreground color of selected odd tabs.
|<<colors.webpage.bg,colors.webpage.bg>>|Background color for webpages if unset (or empty to use the theme's color)
|<<completion.cmd_history_max_items,completion.cmd_history_max_items>>|How many commands to save in the command history.
|<<completion.delay,completion.delay>>|Delay in ms before updating completions after typing a character.
|<<completion.height,completion.height>>|The height of the completion, in px or as percentage of the window.
|<<completion.min_chars,completion.min_chars>>|Minimum amount of characters needed to update completions.
|<<completion.quick,completion.quick>>|Move on to the next part when there's only one possible completion left.
|<<completion.scrollbar.padding,completion.scrollbar.padding>>|Padding of scrollbar handle in the completion window (in px).
|<<completion.scrollbar.width,completion.scrollbar.width>>|Width of the scrollbar in the completion window (in px).
|<<completion.show,completion.show>>|When to show the autocompletion window.
|<<completion.shrink,completion.shrink>>|Shrink the completion to be smaller than the configured size if there are no scrollbars.
|<<completion.timestamp_format,completion.timestamp_format>>|How to format timestamps (e.g. for the history completion).
|<<completion.use_best_match,completion.use_best_match>>|Whether to execute the best-matching command on a partial match.
|<<completion.web_history_max_items,completion.web_history_max_items>>|How many URLs to show in the web history.
|<<confirm_quit,confirm_quit>>|Whether quitting the application requires a confirmation.
|<<content.cache.appcache,content.cache.appcache>>|Whether support for the HTML 5 web application cache feature is enabled.
@ -204,6 +209,7 @@
|<<input.spatial_navigation,input.spatial_navigation>>|Enable Spatial Navigation.
|<<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).
|<<keyhint.radius,keyhint.radius>>|The rounding radius for the edges of the keyhint dialog.
|<<messages.timeout,messages.timeout>>|Time (in ms) to show messages in the statusbar for.
|<<messages.unfocused,messages.unfocused>>|Show messages in unfocused windows.
|<<new_instance_open_target,new_instance_open_target>>|How to open links in an existing instance if a new one is launched.
@ -1093,6 +1099,22 @@ Type: <<types,QssColor>>
Default: +pass:[white]+
[[colors.statusbar.passthrough.bg]]
=== colors.statusbar.passthrough.bg
Background color of the statusbar in passthrough mode.
Type: <<types,QssColor>>
Default: +pass:[darkblue]+
[[colors.statusbar.passthrough.fg]]
=== colors.statusbar.passthrough.fg
Foreground color of the statusbar in passthrough mode.
Type: <<types,QssColor>>
Default: +pass:[white]+
[[colors.statusbar.private.bg]]
=== colors.statusbar.private.bg
Background color of the statusbar in private browsing mode.
@ -1293,6 +1315,14 @@ Type: <<types,Int>>
Default: +pass:[100]+
[[completion.delay]]
=== completion.delay
Delay in ms before updating completions after typing a character.
Type: <<types,Int>>
Default: +pass:[0]+
[[completion.height]]
=== completion.height
The height of the completion, in px or as percentage of the window.
@ -1301,6 +1331,14 @@ Type: <<types,PercOrInt>>
Default: +pass:[50%]+
[[completion.min_chars]]
=== completion.min_chars
Minimum amount of characters needed to update completions.
Type: <<types,Int>>
Default: +pass:[1]+
[[completion.quick]]
=== completion.quick
Move on to the next part when there's only one possible completion left.
@ -1355,6 +1393,14 @@ Type: <<types,TimestampTemplate>>
Default: +pass:[%Y-%m-%d]+
[[completion.use_best_match]]
=== completion.use_best_match
Whether to execute the best-matching command on a partial match.
Type: <<types,Bool>>
Default: +pass:[false]+
[[completion.web_history_max_items]]
=== completion.web_history_max_items
How many URLs to show in the web history.
@ -2369,6 +2415,14 @@ Type: <<types,Int>>
Default: +pass:[500]+
[[keyhint.radius]]
=== keyhint.radius
The rounding radius for the edges of the keyhint dialog.
Type: <<types,Int>>
Default: +pass:[6]+
[[messages.timeout]]
=== messages.timeout
Time (in ms) to show messages in the statusbar for.
@ -2792,6 +2846,7 @@ The following placeholders are defined:
* `{host}`: The host of the current web page.
* `{backend}`: Either ''webkit'' or ''webengine''
* `{private}` : Indicates when private mode is enabled.
* `{current_url}` : The url of the current web page.
Type: <<types,FormatString>>
@ -2928,6 +2983,7 @@ The following placeholders are defined:
* `{host}`: The host of the current web page.
* `{backend}`: Either ''webkit'' or ''webengine''
* `{private}` : Indicates when private mode is enabled.
* `{current_url}` : The url of the current web page.
Type: <<types,FormatString>>

View File

@ -41,21 +41,17 @@ Debian Stretch / Ubuntu 17.04 and newer
Those versions come with QtWebEngine in the repositories. This makes it possible
to install qutebrowser via the Debian package.
Install the dependencies via apt-get:
----
# apt install python-tox python3-{lxml,pyqt5,sip,jinja2,pygments,yaml,attr} python3-pyqt5.qt{webengine,quick,opengl,sql} libqt5sql5-sqlite
----
Get the qutebrowser package from the
https://github.com/qutebrowser/qutebrowser/releases[release page] and download
the https://qutebrowser.org/python3-pypeg2_2.15.2-1_all.deb[PyPEG2 package].
(If you are using debian testing you can just use the python3-pypeg2 package from the repos)
Install the packages:
----
# dpkg -i python3-pypeg2_*_all.deb
# dpkg -i qutebrowser_*_all.deb
# apt install ./python3-pypeg2_*_all.deb
# apt install ./qutebrowser_*_all.deb
----
Some additional hints:
@ -66,7 +62,7 @@ Some additional hints:
`:help` command:
+
----
# apt-get install --no-install-recommends asciidoc source-highlight
# apt install --no-install-recommends asciidoc source-highlight
$ python3 scripts/asciidoc2html.py
----
@ -76,7 +72,7 @@ $ python3 scripts/asciidoc2html.py
- If video or sound don't work with QtWebKit, try installing the gstreamer plugins:
+
----
# apt-get install gstreamer1.0-plugins-{bad,base,good,ugly}
# apt install gstreamer1.0-plugins-{bad,base,good,ugly}
----
On Fedora
@ -221,6 +217,10 @@ https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce[qutebrows
mailinglist] to get notified on new releases). You can install a newer version
without uninstalling the older one.
The binary release ships with a QtWebEngine built without proprietary codec
support. To get support for e.g. h264/h265 videos, you'll need to build
QtWebEngine from source yourself with support for that enabled.
https://chocolatey.org/packages/qutebrowser[Chocolatey package]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -261,6 +261,10 @@ Note that you'll need to upgrade to new versions manually (subscribe to the
https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce[qutebrowser-announce
mailinglist] to get notified on new releases).
The binary release ships with a QtWebEngine built without proprietary codec
support. To get support for e.g. h264/h265 videos, you'll need to build
QtWebEngine from source yourself with support for that enabled.
This binary is also available through the
https://caskroom.github.io/[Homebrew Cask] package manager:
@ -355,6 +359,18 @@ also typically means you'll be using an older release of QtWebEngine.
On Windows, run `tox -e 'mkvenv-win' instead, however make sure that ONLY
Python3 is in your PATH before running tox.
Building the docs
~~~~~~~~~~~~~~~~~
To build the documentation, install `asciidoc` (note that LaTeX which comes as
optional/recommended dependency with some distributions is not required).
Then, run:
----
$ python3 scripts/asciidoc2html.py
----
Creating a wrapper script
~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright 2017 suve <veg@svgames.pl> -->
<component type="desktop">
<id>org.qutebrowser.qutebrowser</id>
<metadata_license>CC-BY-SA-3.0</metadata_license>
<project_license>GPL-3.0</project_license>
<name>qutebrowser</name>
<summary>A keyboard-driven web browser</summary>
<description>
<p>
qutebrowser is a keyboard-focused browser with a minimal GUI.
It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl,
and is based on Python and PyQt5.
</p>
</description>
<categories>
<category>Network</category>
<category>WebBrowser</category>
</categories>
<provides>
<binary>qutebrowser</binary>
</provides>
<launchable type="desktop-id">qutebrowser.desktop</launchable>
<screenshots>
<screenshot type="default">
<image>https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/main.png</image>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/downloads.png</image>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/completion.png</image>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/qutebrowser/qutebrowser/master/doc/img/hints.png</image>
</screenshot>
</screenshots>
<url type="homepage">https://www.qutebrowser.org</url>
<url type="faq">https://qutebrowser.org/doc/faq.html</url>
<url type="help">https://qutebrowser.org/doc/help/</url>
<url type="bugtracker">https://github.com/qutebrowser/qutebrowser/issues/</url>
<url type="donation">https://github.com/qutebrowser/qutebrowser#donating</url>
</component>

View File

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

View File

@ -11,7 +11,7 @@ fields==5.0.0
Flask==0.12.2
glob2==0.6
hunter==2.0.1
hypothesis==3.32.0
hypothesis==3.33.0
itsdangerous==0.24
# Jinja2==2.9.6
Mako==1.0.7

View File

@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2017 Florian Bruhin (The Compiler)"
__license__ = "GPL"
__maintainer__ = __author__
__email__ = "mail@qutebrowser.org"
__version_info__ = (1, 0, 1)
__version_info__ = (1, 0, 2)
__version__ = '.'.join(str(e) for e in __version_info__)
__description__ = "A keyboard-driven, vim-like browser based on PyQt5."

View File

@ -353,6 +353,10 @@ class CommandDispatcher:
Return:
A list of URLs that can be opened.
"""
if isinstance(url, QUrl):
yield url
return
force_search = False
urllist = [u for u in url.split('\n') if u.strip()]
if (len(urllist) > 1 and not urlutils.is_url(urllist[0]) and
@ -514,14 +518,54 @@ class CommandDispatcher:
return newtab
@cmdutils.register(instance='command-dispatcher', scope='window')
def tab_detach(self):
"""Detach the current tab to its own window."""
@cmdutils.argument('index', completion=miscmodels.buffer)
def tab_take(self, index):
"""Take a tab from another window.
Args:
index: The [win_id/]index of the tab to take. Or a substring
in which case the closest match will be taken.
"""
tabbed_browser, tab = self._resolve_buffer_index(index)
if tabbed_browser is self._tabbed_browser:
raise cmdexc.CommandError("Can't take a tab from the same window")
self._open(tab.url(), tab=True)
tabbed_browser.close_tab(tab, add_undo=False)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('win_id', completion=miscmodels.window)
def tab_give(self, win_id: int = None):
"""Give the current tab to a new or existing window if win_id given.
If no win_id is given, the tab will get detached into a new window.
Args:
win_id: The window ID of the window to give the current tab to.
"""
if win_id == self._win_id:
raise cmdexc.CommandError("Can't give a tab to the same window")
if win_id is None:
if self._count() < 2:
raise cmdexc.CommandError("Cannot detach one tab.")
url = self._current_url()
self._open(url, window=True)
cur_widget = self._current_widget()
self._tabbed_browser.close_tab(cur_widget, add_undo=False)
raise cmdexc.CommandError("Cannot detach from a window with "
"only one tab")
tabbed_browser = self._new_tabbed_browser(
private=self._tabbed_browser.private)
else:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
tabbed_browser.tabopen(self._current_url())
self._tabbed_browser.close_tab(self._current_widget(), add_undo=False)
@cmdutils.register(instance='command-dispatcher', hide=True,
scope='window', deprecated='Use :tab-give instead!')
def tab_detach(self):
"""Deprecated way to detach a tab."""
self.tab_give()
def _back_forward(self, tab, bg, window, count, forward):
"""Helper function for :back/:forward."""
@ -972,63 +1016,13 @@ class CommandDispatcher:
else:
raise cmdexc.CommandError("Last tab")
@cmdutils.register(instance='command-dispatcher', scope='window',
deprecated="Use :open {clipboard}")
def paste(self, sel=False, tab=False, bg=False, window=False):
"""Open a page from the clipboard.
If the pasted text contains newlines, each line gets opened in its own
tab.
def _resolve_buffer_index(self, index):
"""Resolve a buffer index to the tabbedbrowser and tab.
Args:
sel: Use the primary selection instead of the clipboard.
tab: Open in a new tab.
bg: Open in a background tab.
window: Open in new window.
"""
force_search = False
if not utils.supports_selection():
sel = False
try:
text = utils.get_clipboard(selection=sel)
except utils.ClipboardError as e:
raise cmdexc.CommandError(e)
text_urls = [u for u in text.split('\n') if u.strip()]
if (len(text_urls) > 1 and not urlutils.is_url(text_urls[0]) and
urlutils.get_path_if_valid(
text_urls[0], check_exists=True) is None):
force_search = True
text_urls = [text]
for i, text_url in enumerate(text_urls):
if not window and i > 0:
tab = False
bg = True
try:
url = urlutils.fuzzy_url(text_url, force_search=force_search)
except urlutils.InvalidUrlError as e:
raise cmdexc.CommandError(e)
self._open(url, tab, bg, window)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('index', completion=miscmodels.buffer)
@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 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
index: The [win_id/]index of the tab to be selected. Or a substring
in which case the closest match will be focused.
count: The tab index to focus, starting with 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:
@ -1066,10 +1060,35 @@ class CommandDispatcher:
raise cmdexc.CommandError(
"There's no tab with index {}!".format(idx))
window = objreg.window_registry[win_id]
return (tabbed_browser, tabbed_browser.widget(idx-1))
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('index', completion=miscmodels.buffer)
@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 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.
"""
if count is None and index is None:
raise cmdexc.CommandError("buffer: Either a count or the argument "
"index must be specified.")
if count is not None:
index = str(count)
tabbed_browser, tab = self._resolve_buffer_index(index)
window = tabbed_browser.window()
window.activateWindow()
window.raise_()
tabbed_browser.setCurrentIndex(idx-1)
tabbed_browser.setCurrentWidget(tab)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('index', choices=['last'])
@ -1195,7 +1214,7 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window')
def home(self):
"""Open main startpage in current tab."""
self._current_widget().openurl(config.val.url.start_pages[0])
self.openurl(config.val.url.start_pages[0])
def _run_userscript(self, cmd, *args, verbose=False):
"""Run a userscript given as argument.
@ -1624,14 +1643,6 @@ class CommandDispatcher:
except webelem.Error as e:
raise cmdexc.CommandError(str(e))
@cmdutils.register(instance='command-dispatcher',
deprecated="Use :insert-text {primary}",
modes=[KeyMode.insert], hide=True, scope='window',
backend=usertypes.Backend.QtWebKit)
def paste_primary(self):
"""Paste the primary selection at cursor position."""
self.insert_text(utils.get_clipboard(selection=True, fallback=True))
@cmdutils.register(instance='command-dispatcher', maxsplit=0,
scope='window')
def insert_text(self, text):

View File

@ -29,8 +29,9 @@ import os
import time
import urllib.parse
import textwrap
import pkg_resources
import mimetypes
import pkg_resources
from PyQt5.QtCore import QUrlQuery, QUrl
import qutebrowser
@ -323,8 +324,10 @@ def qute_help(url):
"scripts/asciidoc2html.py.")
path = 'html/doc/{}'.format(urlpath)
if urlpath.endswith('.png'):
return 'image/png', utils.read_file(path, binary=True)
if not urlpath.endswith('.html'):
mimetype, _encoding = mimetypes.guess_type(urlpath)
assert mimetype is not None, url
return mimetype, utils.read_file(path, binary=True)
try:
data = utils.read_file(path)

View File

@ -36,7 +36,8 @@ from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile,
from qutebrowser.browser import shared
from qutebrowser.browser.webengine import spell
from qutebrowser.config import config, websettings
from qutebrowser.utils import utils, standarddir, javascript, qtutils, message
from qutebrowser.utils import (utils, standarddir, javascript, qtutils,
message, log)
# The default QWebEngineProfile
default_profile = None
@ -145,6 +146,7 @@ class DictionaryLanguageSetter(DefaultProfileSetter):
raise ValueError("'settings' may not be set with "
"DictionaryLanguageSetter!")
filenames = [self._find_installed(code) for code in value]
log.config.debug("Found dicts: {}".format(filenames))
super()._set([f for f in filenames if f], settings)

View File

@ -24,7 +24,7 @@ import functools
import html as html_utils
import sip
from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QPoint, QUrl, QTimer
from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QPoint, QPointF, QUrl, QTimer
from PyQt5.QtGui import QKeyEvent
from PyQt5.QtNetwork import QAuthenticator
from PyQt5.QtWidgets import QApplication
@ -293,6 +293,7 @@ class WebEngineScroller(browsertab.AbstractScroller):
def __init__(self, tab, parent=None):
super().__init__(tab, parent)
self._args = objreg.get('args')
self._pos_perc = (0, 0)
self._pos_px = QPoint()
self._at_bottom = False
@ -307,39 +308,31 @@ class WebEngineScroller(browsertab.AbstractScroller):
for _ in range(min(count, 5000)):
self._tab.key_press(key, modifier)
@pyqtSlot()
def _update_pos(self):
@pyqtSlot(QPointF)
def _update_pos(self, pos):
"""Update the scroll position attributes when it changed."""
def update_pos_cb(jsret):
"""Callback after getting scroll position via JS."""
if jsret is None:
# This can happen when the callback would get called after
# shutting down a tab
return
log.webview.vdebug(jsret)
assert isinstance(jsret, dict), jsret
self._pos_px = QPoint(jsret['px']['x'], jsret['px']['y'])
self._pos_px = pos.toPoint()
contents_size = self._widget.page().contentsSize()
dx = jsret['scroll']['width'] - jsret['inner']['width']
if dx == 0:
scrollable_x = contents_size.width() - self._widget.width()
if scrollable_x == 0:
perc_x = 0
else:
perc_x = min(100, round(100 / dx * jsret['px']['x']))
perc_x = min(100, round(100 / scrollable_x * pos.x()))
dy = jsret['scroll']['height'] - jsret['inner']['height']
if dy == 0:
scrollable_y = contents_size.height() - self._widget.height()
if scrollable_y == 0:
perc_y = 0
else:
perc_y = min(100, round(100 / dy * jsret['px']['y']))
perc_y = min(100, round(100 / scrollable_y * pos.y()))
self._at_bottom = math.ceil(jsret['px']['y']) >= dy
self._at_bottom = math.ceil(pos.y()) >= scrollable_y
if (self._pos_perc != (perc_x, perc_y) or
'no-scroll-filtering' in self._args.debug_flags):
self._pos_perc = perc_x, perc_y
self.perc_changed.emit(*self._pos_perc)
js_code = javascript.assemble('scroll', 'pos')
self._tab.run_js_async(js_code, update_pos_cb)
def pos_px(self):
return self._pos_px

View File

@ -422,12 +422,13 @@ class WebKitScroller(browsertab.AbstractScroller):
else:
for val, orientation in [(x, Qt.Horizontal), (y, Qt.Vertical)]:
if val is not None:
val = qtutils.check_overflow(val, 'int', fatal=False)
frame = self._widget.page().mainFrame()
m = frame.scrollBarMaximum(orientation)
if m == 0:
maximum = frame.scrollBarMaximum(orientation)
if maximum == 0:
continue
frame.setScrollBarValue(orientation, int(m * val / 100))
pos = int(maximum * val / 100)
pos = qtutils.check_overflow(pos, 'int', fatal=False)
frame.setScrollBarValue(orientation, pos)
def _key_press(self, key, count=1, getter_name=None, direction=None):
frame = self._widget.page().mainFrame()

View File

@ -214,12 +214,12 @@ class CommandParser:
Return:
cmdstr modified to the matching completion or unmodified
"""
matches = []
for valid_command in cmdutils.cmd_dict:
if valid_command.find(cmdstr) == 0:
matches.append(valid_command)
matches = [cmd for cmd in sorted(cmdutils.cmd_dict, key=len)
if cmdstr in cmd]
if len(matches) == 1:
cmdstr = matches[0]
elif len(matches) > 1 and config.val.completion.use_best_match:
cmdstr = matches[0]
return cmdstr
def _split_args(self, cmd, argstr, keep):

View File

@ -196,14 +196,25 @@ class Completer(QObject):
For performance reasons we don't want to block here, instead we do this
in the background.
We delay the update only if we've already input some text and ignore
updates if the text is shorter than completion.min_chars (unless we're
hitting backspace in which case updates won't be ignored).
"""
if (self._cmd.cursorPosition() == self._last_cursor_pos and
_cmd, _sep, rest = self._cmd.text().partition(' ')
input_length = len(rest)
if (0 < input_length < config.val.completion.min_chars and
self._cmd.cursorPosition() > self._last_cursor_pos):
log.completion.debug("Ignoring update because the length of "
"the text is less than completion.min_chars.")
elif (self._cmd.cursorPosition() == self._last_cursor_pos and
self._cmd.text() == self._last_text):
log.completion.debug("Ignoring update because there were no "
"changes.")
else:
log.completion.debug("Scheduling completion update.")
self._timer.start()
start_delay = config.val.completion.delay if self._last_text else 0
self._timer.start(start_delay)
self._last_cursor_pos = self._cmd.cursorPosition()
self._last_text = self._cmd.text()

View File

@ -202,7 +202,8 @@ class CompletionItemDelegate(QStyledItemDelegate):
if index.column() in columns_to_filter and pattern:
repl = r'<span class="highlight">\g<0></span>'
text = re.sub(re.escape(pattern).replace(r'\ ', r'|'),
repl, self._opt.text, flags=re.IGNORECASE)
repl, html.escape(self._opt.text),
flags=re.IGNORECASE)
self._doc.setHtml(text)
else:
self._doc.setPlainText(self._opt.text)

View File

@ -122,3 +122,22 @@ def buffer(*, info=None): # pylint: disable=unused-argument
model.add_category(cat)
return model
def window(*, info=None): # pylint: disable=unused-argument
"""A model to complete on all open windows."""
model = completionmodel.CompletionModel(column_widths=(6, 30, 64))
windows = []
for win_id in objreg.window_registry:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
tab_titles = (tab.title() for tab in tabbed_browser.widgets())
windows.append(("{}".format(win_id),
objreg.window_registry[win_id].windowTitle(),
", ".join(tab_titles)))
model.add_category(listcategory.ListCategory("Windows", windows))
return model

View File

@ -75,6 +75,10 @@ class ConfigCommands:
tabbed_browser.openurl(QUrl('qute://settings'), newtab=False)
return
if option.endswith('!'):
raise cmdexc.CommandError("Toggling values was moved to the "
":config-cycle command")
if option.endswith('?') and option != '?':
self._print_value(option[:-1])
return

View File

@ -678,6 +678,25 @@ completion.web_history_max_items:
0: no history / -1: unlimited
completion.delay:
default: 0
type:
name: Int
minval: 0
desc: Delay in ms before updating completions after typing a character.
completion.min_chars:
default: 1
type:
name: Int
minval: 1
desc: Minimum amount of characters needed to update completions.
completion.use_best_match:
type: Bool
default: false
desc: Whether to execute the best-matching command on a partial match.
## downloads
downloads.location.directory:
@ -959,6 +978,13 @@ keyhint.blacklist:
Globs are supported, so `;*` will blacklist all keychains starting with `;`.
Use `*` to disable keyhints.
keyhint.radius:
type:
name: Int
minval: 0
default: 6
desc: The rounding radius for the edges of the keyhint dialog.
# emacs: '
keyhint.delay:
@ -1224,6 +1250,7 @@ tabs.title.format:
- scroll_pos
- host
- private
- current_url
none_ok: true
desc: |
The format to use for the tab title.
@ -1239,6 +1266,7 @@ tabs.title.format:
* `{host}`: The host of the current web page.
* `{backend}`: Either ''webkit'' or ''webengine''
* `{private}` : Indicates when private mode is enabled.
* `{current_url}` : The url of the current web page.
tabs.title.format_pinned:
default: '{index}'
@ -1254,6 +1282,7 @@ tabs.title.format_pinned:
- scroll_pos
- host
- private
- current_url
none_ok: true
desc: The format to use for the tab title for pinned tabs. The same placeholders
like for `tabs.title.format` are defined.
@ -1371,6 +1400,7 @@ window.title_format:
- host
- backend
- private
- current_url
default: '{perc}{title}{title_sep}qutebrowser'
desc: |
The format to use for the window title.
@ -1385,6 +1415,7 @@ window.title_format:
* `{host}`: The host of the current web page.
* `{backend}`: Either ''webkit'' or ''webengine''
* `{private}` : Indicates when private mode is enabled.
* `{current_url}` : The url of the current web page.
## zoom
@ -1469,16 +1500,6 @@ colors.completion.category.border.bottom:
type: QssColor
desc: Bottom border color of the completion widget category headers.
colors.statusbar.insert.fg:
default: white
type: QssColor
desc: Foreground color of the statusbar in insert mode.
colors.statusbar.insert.bg:
default: darkgreen
type: QssColor
desc: Background color of the statusbar in insert mode.
colors.completion.item.selected.fg:
default: black
type: QtColor
@ -1668,6 +1689,26 @@ colors.statusbar.normal.bg:
type: QssColor
desc: Background color of the statusbar.
colors.statusbar.insert.fg:
default: white
type: QssColor
desc: Foreground color of the statusbar in insert mode.
colors.statusbar.insert.bg:
default: darkgreen
type: QssColor
desc: Background color of the statusbar in insert mode.
colors.statusbar.passthrough.fg:
default: white
type: QssColor
desc: Foreground color of the statusbar in passthrough mode.
colors.statusbar.passthrough.bg:
default: darkblue
type: QssColor
desc: Background color of the statusbar in passthrough mode.
colors.statusbar.private.fg:
default: white
type: QssColor

View File

@ -259,6 +259,16 @@ class ConfigAPI:
with self._handle_error('unbinding', key):
self._keyconfig.unbind(key, mode=mode)
def source(self, filename):
"""Read the given config file from disk."""
if not os.path.isabs(filename):
filename = str(self.configdir / filename)
try:
read_config_py(filename)
except configexc.ConfigFileErrors as e:
self.errors += e.errors
class ConfigPyWriter:

View File

@ -104,7 +104,9 @@ def _update_monospace_fonts():
continue
elif not isinstance(opt.typ, configtypes.Font):
continue
elif not config.instance.get_obj(name).endswith(' monospace'):
value = config.instance.get_obj(name)
if value is None or not value.endswith(' monospace'):
continue
config.instance.changed.emit(name)

View File

@ -71,32 +71,5 @@ window._qutebrowser.scroll = (function() {
window.scrollBy(dx, dy);
};
funcs.pos = function() {
var pos = {
"px": {"x": window.scrollX, "y": window.scrollY},
"scroll": {
"width": Math.max(
document.body.scrollWidth,
document.body.offsetWidth,
document.documentElement.scrollWidth,
document.documentElement.offsetWidth
),
"height": Math.max(
document.body.scrollHeight,
document.body.offsetHeight,
document.documentElement.scrollHeight,
document.documentElement.offsetHeight
),
},
"inner": {
"width": window.innerWidth,
"height": window.innerHeight,
},
};
// console.log(JSON.stringify(pos));
return pos;
};
return funcs;
})();

View File

@ -388,20 +388,6 @@ class PromptContainer(QWidget):
message.global_bridge.prompt_done.emit(self._prompt.KEY_MODE)
question.done()
@cmdutils.register(instance='prompt-container', hide=True, scope='window',
modes=[usertypes.KeyMode.yesno],
deprecated='Use :prompt-accept yes instead!')
def prompt_yes(self):
"""Answer yes to a yes/no prompt."""
self.prompt_accept('yes')
@cmdutils.register(instance='prompt-container', hide=True, scope='window',
modes=[usertypes.KeyMode.yesno],
deprecated='Use :prompt-accept no instead!')
def prompt_no(self):
"""Answer no to a yes/no prompt."""
self.prompt_accept('no')
@cmdutils.register(instance='prompt-container', hide=True, scope='window',
modes=[usertypes.KeyMode.prompt], maxsplit=0)
def prompt_open_download(self, cmdline: str = None):

View File

@ -43,6 +43,7 @@ class ColorFlags:
command: If we're currently in command mode.
mode: The current caret mode (CaretMode.off/.on/.selection).
private: Whether this window is in private browsing mode.
passthrough: If we're currently in passthrough-mode.
"""
CaretMode = usertypes.enum('CaretMode', ['off', 'on', 'selection'])
@ -51,6 +52,7 @@ class ColorFlags:
command = attr.ib(False)
caret = attr.ib(CaretMode.off)
private = attr.ib(False)
passthrough = attr.ib(False)
def to_stringlist(self):
"""Get a string list of set flags used in the stylesheet.
@ -66,6 +68,8 @@ class ColorFlags:
strings.append('command')
if self.private:
strings.append('private')
if self.passthrough:
strings.append('passthrough')
if self.private and self.command:
strings.append('private-command')
@ -88,6 +92,7 @@ def _generate_stylesheet():
('prompt', 'prompts'),
('insert', 'statusbar.insert'),
('command', 'statusbar.command'),
('passthrough', 'statusbar.passthrough'),
('private-command', 'statusbar.command.private'),
]
stylesheet = """
@ -244,6 +249,9 @@ class StatusBar(QWidget):
if mode == usertypes.KeyMode.insert:
log.statusbar.debug("Setting insert flag to {}".format(val))
self._color_flags.insert = val
if mode == usertypes.KeyMode.passthrough:
log.statusbar.debug("Setting passthrough flag to {}".format(val))
self._color_flags.passthrough = val
if mode == usertypes.KeyMode.command:
log.statusbar.debug("Setting command flag to {}".format(val))
self._color_flags.command = val
@ -307,7 +315,8 @@ class StatusBar(QWidget):
usertypes.KeyMode.command,
usertypes.KeyMode.caret,
usertypes.KeyMode.prompt,
usertypes.KeyMode.yesno]:
usertypes.KeyMode.yesno,
usertypes.KeyMode.passthrough]:
self.set_mode_active(mode, True)
@pyqtSlot(usertypes.KeyMode, usertypes.KeyMode)
@ -324,7 +333,8 @@ class StatusBar(QWidget):
usertypes.KeyMode.command,
usertypes.KeyMode.caret,
usertypes.KeyMode.prompt,
usertypes.KeyMode.yesno]:
usertypes.KeyMode.yesno,
usertypes.KeyMode.passthrough]:
self.set_mode_active(old_mode, False)
@pyqtSlot(browsertab.AbstractTab)

View File

@ -173,8 +173,18 @@ class TabbedBrowser(tabwidget.TabWidget):
widgets.append(widget)
return widgets
def _update_window_title(self):
"""Change the window title to match the current tab."""
def _update_window_title(self, field=None):
"""Change the window title to match the current tab.
Args:
idx: The tab index to update.
field: A field name which was updated. If given, the title
is only set if the given field is in the template.
"""
title_format = config.val.window.title_format
if field is not None and ('{' + field + '}') not in title_format:
return
idx = self.currentIndex()
if idx == -1:
# (e.g. last tab removed)
@ -183,7 +193,6 @@ class TabbedBrowser(tabwidget.TabWidget):
fields = self.get_tab_fields(idx)
fields['id'] = self._win_id
title_format = config.val.window.title_format
title = title_format.format(**fields)
self.window().setWindowTitle(title)
@ -696,8 +705,8 @@ class TabbedBrowser(tabwidget.TabWidget):
log.webview.debug("Not updating scroll position because index is "
"-1")
return
self._update_window_title()
self._update_tab_title(idx)
self._update_window_title('scroll_pos')
self._update_tab_title(idx, 'scroll_pos')
def _on_renderer_process_terminated(self, tab, status, code):
"""Show an error when a renderer process terminated."""

View File

@ -121,21 +121,29 @@ class TabWidget(QTabWidget):
"""Get the tab title user data."""
return self.tabBar().page_title(idx)
def _update_tab_title(self, idx):
"""Update the tab text for the given tab."""
def _update_tab_title(self, idx, field=None):
"""Update the tab text for the given tab.
Args:
idx: The tab index to update.
field: A field name which was updated. If given, the title
is only set if the given field is in the template.
"""
tab = self.widget(idx)
if tab.data.pinned:
fmt = config.val.tabs.title.format_pinned
else:
fmt = config.val.tabs.title.format
if (field is not None and
(fmt is None or ('{' + field + '}') not in fmt)):
return
fields = self.get_tab_fields(idx)
fields['title'] = fields['title'].replace('&', '&&')
fields['index'] = idx + 1
fmt = config.val.tabs.title.format
fmt_pinned = config.val.tabs.title.format_pinned
if tab.data.pinned:
title = '' if fmt_pinned is None else fmt_pinned.format(**fields)
else:
title = '' if fmt is None else fmt.format(**fields)
self.tabBar().setTabText(idx, title)
def get_tab_fields(self, idx):
@ -164,6 +172,11 @@ class TabWidget(QTabWidget):
except qtutils.QtValueError:
fields['host'] = ''
try:
fields['current_url'] = self.tab_url(idx).url()
except qtutils.QtValueError:
fields['current_url'] = ''
y = tab.scroller.pos_perc()[1]
if y is None:
scroll_pos = '???'
@ -659,7 +672,7 @@ class TabBarStyle(QCommonStyle):
icon_state = (QIcon.On if opt.state & QStyle.State_Selected
else QIcon.Off)
icon = opt.icon.pixmap(opt.iconSize, icon_mode, icon_state)
p.drawPixmap(layouts.icon.x(), layouts.icon.y(), icon)
self._style.drawItemPixmap(p, layouts.icon, Qt.AlignCenter, icon)
def drawControl(self, element, opt, p, widget=None):
"""Override drawControl to draw odd tabs in a different color.
@ -826,8 +839,7 @@ class TabBarStyle(QCommonStyle):
else QIcon.Off)
# reserve space for favicon when tab bar is vertical (issue #1968)
position = config.val.tabs.position
if (opt.icon.isNull() and
position in [QTabWidget.East, QTabWidget.West] and
if (position in [QTabWidget.East, QTabWidget.West] and
config.val.tabs.favicons.show):
tab_icon_size = icon_size
else:
@ -835,6 +847,7 @@ class TabBarStyle(QCommonStyle):
tab_icon_size = QSize(
min(actual_size.width(), icon_size.width()),
min(actual_size.height(), icon_size.height()))
icon_top = text_rect.center().y() + 1 - tab_icon_size.height() / 2
icon_rect = QRect(QPoint(text_rect.left(), icon_top), tab_icon_size)
icon_rect = self._style.visualRect(opt.direction, opt.rect, icon_rect)

View File

@ -49,7 +49,7 @@ def check_python_version():
# pylint: disable=bad-builtin
version_str = '.'.join(map(str, sys.version_info[:3]))
text = ("At least Python 3.5 is required to run qutebrowser, but " +
version_str + " is installed!\n")
"it's running with " + version_str + ".\n")
if Tk and '--no-err-windows' not in sys.argv: # pragma: no cover
root = Tk()
root.withdraw()

View File

@ -54,9 +54,9 @@ class KeyHintView(QLabel):
background-color: {{ conf.colors.keyhint.bg }};
padding: 6px;
{% if conf.statusbar.position == 'top' %}
border-bottom-right-radius: 6px;
border-bottom-right-radius: {{ conf.keyhint.radius }}px;
{% else %}
border-top-right-radius: 6px;
border-top-right-radius: {{ conf.keyhint.radius }}px;
{% endif %}
}
"""

View File

@ -65,13 +65,12 @@ class SqliteError(SqlError):
log.sql.debug("error code: {}".format(error.nativeErrorCode()))
# https://sqlite.org/rescode.html
environmental_errors = [
# SQLITE_LOCKED,
# https://github.com/qutebrowser/qutebrowser/issues/2930
'9',
# SQLITE_FULL,
# https://github.com/qutebrowser/qutebrowser/issues/3004
'13',
environmental_errors = [
'5', # SQLITE_BUSY ("database is locked")
'8', # SQLITE_READONLY
'13', # SQLITE_FULL
]
self.environmental = error.nativeErrorCode() in environmental_errors

View File

@ -159,7 +159,8 @@ def debug_flag_error(flag):
debug-exit: Turn on debugging of late exit.
pdb-postmortem: Drop into pdb on exceptions.
"""
valid_flags = ['debug-exit', 'pdb-postmortem', 'no-sql-history']
valid_flags = ['debug-exit', 'pdb-postmortem', 'no-sql-history',
'no-scroll-filtering']
if flag in valid_flags:
return flag

View File

@ -20,6 +20,7 @@
"""Utilities to get and initialize data/config paths."""
import os
import sys
import shutil
import os.path
import contextlib
@ -106,6 +107,10 @@ def _init_data(args):
if utils.is_windows:
app_data_path = _writable_location(QStandardPaths.AppDataLocation)
path = os.path.join(app_data_path, 'data')
elif sys.platform.startswith('haiku'):
# HaikuOS returns an empty value for AppDataLocation
config_path = _writable_location(QStandardPaths.ConfigLocation)
path = os.path.join(config_path, 'data')
else:
path = _writable_location(typ)
_create(path)

View File

@ -882,7 +882,7 @@ def yaml_load(f):
end = datetime.datetime.now()
delta = (end - start).total_seconds()
deadline = 3 if 'CI' in os.environ else 1
deadline = 3 if 'CI' in os.environ else 2
if delta > deadline: # pragma: no cover
log.misc.warning(
"YAML load took unusually long, please report this at "

View File

@ -150,13 +150,14 @@ def _git_str_subprocess(gitpath):
if not os.path.isdir(os.path.join(gitpath, ".git")):
return None
try:
cid = subprocess.check_output(
['git', 'describe', '--tags', '--dirty', '--always'],
# https://stackoverflow.com/questions/21017300/21017394#21017394
commit_hash = subprocess.check_output(
['git', 'describe', '--match=NeVeRmAtCh', '--always', '--dirty'],
cwd=gitpath).decode('UTF-8').strip()
date = subprocess.check_output(
['git', 'show', '-s', '--format=%ci', 'HEAD'],
cwd=gitpath).decode('UTF-8').strip()
return '{} ({})'.format(cid, date)
return '{} ({})'.format(commit_hash, date)
except (subprocess.CalledProcessError, OSError):
return None

View File

@ -20,8 +20,7 @@
"""Custom astroid checker for config calls."""
import sys
import os
import os.path
import pathlib
import yaml
import astroid
@ -30,6 +29,7 @@ from pylint.checkers import utils
OPTIONS = None
FAILED_LOAD = False
class ConfigChecker(checkers.BaseChecker):
@ -44,6 +44,7 @@ class ConfigChecker(checkers.BaseChecker):
None),
}
priority = -1
printed_warning = False
@utils.check_messages('bad-config-option')
def visit_attribute(self, node):
@ -58,6 +59,13 @@ class ConfigChecker(checkers.BaseChecker):
def _check_config(self, node, name):
"""Check that we're accessing proper config options."""
if FAILED_LOAD:
if not ConfigChecker.printed_warning:
print("[WARN] Could not find configdata.yml. Please run "
"pylint from qutebrowser root.", file=sys.stderr)
print("Skipping some checks...", file=sys.stderr)
ConfigChecker.printed_warning = True
return
if name not in OPTIONS:
self.add_message('bad-config-option', node=node, args=name)
@ -66,6 +74,11 @@ def register(linter):
"""Register this checker."""
linter.register_checker(ConfigChecker(linter))
global OPTIONS
yaml_file = os.path.join('qutebrowser', 'config', 'configdata.yml')
with open(yaml_file, 'r', encoding='utf-8') as f:
global FAILED_LOAD
yaml_file = pathlib.Path('qutebrowser') / 'config' / 'configdata.yml'
if not yaml_file.exists():
OPTIONS = None
FAILED_LOAD = True
return
with yaml_file.open(mode='r', encoding='utf-8') as f:
OPTIONS = list(yaml.load(f))

View File

@ -0,0 +1,68 @@
#!/usr/bin/env python3
# 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/>.
"""Show various QStandardPath paths."""
import os
import sys
from PyQt5.QtCore import (QT_VERSION_STR, PYQT_VERSION_STR, qVersion,
QStandardPaths, QCoreApplication)
def print_header():
"""Show system information."""
print("Python {}".format(sys.version))
print("os.name: {}".format(os.name))
print("sys.platform: {}".format(sys.platform))
print()
print("Qt {}, compiled {}".format(qVersion(), QT_VERSION_STR))
print("PyQt {}".format(PYQT_VERSION_STR))
print()
def print_paths():
for name, obj in vars(QStandardPaths).items():
if isinstance(obj, QStandardPaths.StandardLocation):
location = QStandardPaths.writableLocation(obj)
print("{:25} {}".format(name, location))
def main():
print_header()
print("No QApplication")
print("===============")
print()
print_paths()
app = QCoreApplication(sys.argv)
app.setApplicationName("qapp_name")
print()
print("With QApplication")
print("=================")
print()
print_paths()
if __name__ == '__main__':
main()

View File

@ -50,13 +50,14 @@ def _git_str():
if not os.path.isdir(os.path.join(BASEDIR, ".git")):
return None
try:
cid = subprocess.check_output(
['git', 'describe', '--tags', '--dirty', '--always'],
# https://stackoverflow.com/questions/21017300/21017394#21017394
commit_hash = subprocess.check_output(
['git', 'describe', '--match=NeVeRmAtCh', '--always', '--dirty'],
cwd=BASEDIR).decode('UTF-8').strip()
date = subprocess.check_output(
['git', 'show', '-s', '--format=%ci', 'HEAD'],
cwd=BASEDIR).decode('UTF-8').strip()
return '{} ({})'.format(cid, date)
return '{} ({})'.format(commit_hash, date)
except (subprocess.CalledProcessError, OSError):
return None

View File

@ -99,6 +99,8 @@ Feature: Page history
Then the page should contain the plaintext "3.txt"
Then the page should contain the plaintext "4.txt"
# Hangs a lot on AppVeyor
@posix
Scenario: Listing history with qute:history redirect
When I open data/numbers/3.txt
And I open data/numbers/4.txt

View File

@ -74,12 +74,12 @@ Feature: Invoking a new process
# issue #1060
Scenario: Using target_window = first-opened after tab-detach
Scenario: Using target_window = first-opened after tab-give
When I set new_instance_open_target to tab
And I set new_instance_open_target_window to first-opened
And I open data/title.html
And I open data/search.html in a new tab
And I run :tab-detach
And I run :tab-give
And I wait until data/search.html is loaded
And I open data/hello.txt as a URL
Then the session should look like:

View File

@ -387,22 +387,6 @@ Feature: Prompts
Then the javascript message "confirm reply: true" should be logged
And the error "No default value was set for this question!" should be shown
Scenario: Javascript confirm with deprecated :prompt-yes command
When I open data/prompt/jsconfirm.html
And I run :click-element id button
And I wait for a prompt
And I run :prompt-yes
Then the javascript message "confirm reply: true" should be logged
And the warning "prompt-yes is deprecated - Use :prompt-accept yes instead!" should be shown
Scenario: Javascript confirm with deprecated :prompt-no command
When I open data/prompt/jsconfirm.html
And I run :click-element id button
And I wait for a prompt
And I run :prompt-no
Then the javascript message "confirm reply: false" should be logged
And the warning "prompt-no is deprecated - Use :prompt-accept no instead!" should be shown
# Other
@qtwebengine_skip

View File

@ -638,28 +638,6 @@ Feature: Tab management
And I run :tab-clone
Then no crash should happen
# :tab-detach
Scenario: Detaching a tab
When I open data/numbers/1.txt
And I open data/numbers/2.txt in a new tab
And I run :tab-detach
And I wait until data/numbers/2.txt is loaded
Then the session should look like:
windows:
- tabs:
- history:
- url: about:blank
- url: http://localhost:*/data/numbers/1.txt
- tabs:
- history:
- url: http://localhost:*/data/numbers/2.txt
Scenario: Detach tab from window with only one tab
When I open data/hello.txt
And I run :tab-detach
Then the error "Cannot detach one tab." should be shown
# :undo
Scenario: Undo without any closed tabs
@ -1010,6 +988,76 @@ Feature: Tab management
And I run :buffer "1/2/3"
Then the error "No matching tab for: 1/2/3" should be shown
# :tab-take
@xfail_norun # Needs qutewm
Scenario: Take a tab from another window
Given I have a fresh instance
When I open data/numbers/1.txt
And I open data/numbers/2.txt in a new window
And I run :tab-take 0/1
Then the session should look like:
windows:
- tabs:
- history:
- url: about:blank
- tabs:
- history:
- url: http://localhost:*/data/numbers/2.txt
- history:
- url: http://localhost:*/data/numbers/1.txt
Scenario: Take a tab from the same window
Given I have a fresh instance
When I open data/numbers/1.txt
And I run :tab-take 0/1
Then the error "Can't take a tab from the same window" should be shown
# :tab-give
@xfail_norun # Needs qutewm
Scenario: Give a tab to another window
Given I have a fresh instance
When I open data/numbers/1.txt
And I open data/numbers/2.txt in a new window
And I run :tab-give 0
Then the session should look like:
windows:
- tabs:
- history:
- url: http://localhost:*/data/numbers/1.txt
- history:
- url: http://localhost:*/data/numbers/2.txt
- tabs:
- history:
- url: about:blank
Scenario: Give a tab to the same window
Given I have a fresh instance
When I open data/numbers/1.txt
And I run :tab-give 0
Then the error "Can't give a tab to the same window" should be shown
Scenario: Give a tab to a new window
When I open data/numbers/1.txt
And I open data/numbers/2.txt in a new tab
And I run :tab-give
And I wait until data/numbers/2.txt is loaded
Then the session should look like:
windows:
- tabs:
- history:
- url: about:blank
- url: http://localhost:*/data/numbers/1.txt
- tabs:
- history:
- url: http://localhost:*/data/numbers/2.txt
Scenario: Give a tab from window with only one tab
When I open data/hello.txt
And I run :tab-give
Then the error "Cannot detach from a window with only one tab" should be shown
# Other
Scenario: Using :tab-next after closing last tab (#1448)
@ -1149,6 +1197,14 @@ Feature: Tab management
And the following tabs should be open:
- data/numbers/1.txt (active) (pinned)
Scenario: :tab-pin open url
When I open data/numbers/1.txt
And I run :tab-pin
And I run :home
Then the message "Tab is pinned!" should be shown
And the following tabs should be open:
- data/numbers/1.txt (active) (pinned)
Scenario: Cloning a pinned tab
When I open data/numbers/1.txt
And I run :tab-pin

View File

@ -17,10 +17,19 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
import pytest
import pytest_bdd as bdd
bdd.scenarios('marks.feature')
@pytest.fixture(autouse=True)
def turn_on_scroll_logging(quteproc):
"""Make sure all scrolling changes are logged."""
quteproc.send_cmd(":debug-pyeval -q objreg.get('args')."
"debug_flags.append('no-scroll-filtering')")
@bdd.then(bdd.parsers.parse("the page should be scrolled to {x} {y}"))
def check_y(request, quteproc, x, y):
data = quteproc.get_session()

View File

@ -17,5 +17,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 pytest
import pytest_bdd as bdd
bdd.scenarios('scroll.feature')
@pytest.fixture(autouse=True)
def turn_on_scroll_logging(quteproc):
"""Make sure all scrolling changes are logged."""
quteproc.send_cmd(":debug-pyeval -q objreg.get('args')."
"debug_flags.append('no-scroll-filtering')")

View File

@ -105,6 +105,12 @@ def is_ignored_lowlevel_message(message):
elif (message.startswith('QNetworkProxyFactory: factory 0x') and
message.endswith(' has returned an empty result set')):
return True
elif message == ' Error: No such file or directory':
# Qt 5.10 with debug Chromium
# [1016/155149.941048:WARNING:stack_trace_posix.cc(625)] Failed to open
# file: /home/florian/#14687139 (deleted)
# Error: No such file or directory
return True
return False
@ -170,6 +176,12 @@ def is_ignored_chromium_message(line):
# WebFrame LEAKED 1 TIMES
'WebFrame LEAKED 1 TIMES',
# Qt 5.10 with debug Chromium
# [1016/155149.941048:WARNING:stack_trace_posix.cc(625)] Failed to open
# file: /home/florian/#14687139 (deleted)
# Error: No such file or directory
'Failed to open file: * (deleted)',
# macOS on Travis
# [5140:5379:0911/063441.239771:ERROR:mach_port_broker.mm(175)]
# Unknown process 5176 is sending Mach IPC messages!

View File

@ -60,6 +60,9 @@ class WinRegistryHelper:
registry = attr.ib()
def windowTitle(self):
return 'window title - qutebrowser'
def __init__(self):
self._ids = []

View File

@ -131,10 +131,11 @@ class FakeUrl:
"""QUrl stub which provides .path(), isValid() and host()."""
def __init__(self, path=None, valid=True, host=None):
def __init__(self, path=None, valid=True, host=None, url=None):
self.path = mock.Mock(return_value=path)
self.isValid = mock.Mock(returl_value=valid)
self.host = mock.Mock(returl_value=host)
self.url = mock.Mock(return_value=url)
class FakeNetworkReply:
@ -377,7 +378,9 @@ class FakeTimer(QObject):
def isSingleShot(self):
return self._singleshot
def start(self):
def start(self, interval=None):
if interval:
self._interval = interval
self._started = True
def stop(self):
@ -396,7 +399,7 @@ class InstaTimer(QObject):
timeout = pyqtSignal()
def start(self):
def start(self, interval=None):
self.timeout.emit()
def setSingleShot(self, yes):
@ -520,6 +523,9 @@ class TabbedBrowserStub(QObject):
def count(self):
return len(self.tabs)
def widgets(self):
return self.tabs
def widget(self, i):
return self.tabs[i]

View File

@ -146,3 +146,26 @@ class TestHistoryHandler:
url = QUrl("qute://history/data?start_time={}".format(now))
_mimetype, data = benchmark(qutescheme.qute_history, url)
assert len(json.loads(data)) > 1
class TestHelpHandler:
"""Tests for qute://help."""
@pytest.fixture
def data_patcher(self, monkeypatch):
def _patch(path, data):
def _read_file(name, binary=False):
assert path == name
if binary:
return data
return data.decode('utf-8')
monkeypatch.setattr(qutescheme.utils, 'read_file', _read_file)
return _patch
def test_unknown_file_type(self, data_patcher):
data_patcher('html/doc/foo.bin', b'\xff')
mimetype, data = qutescheme.qute_help(QUrl('qute://help/foo.bin'))
assert mimetype == 'application/octet-stream'
assert data == b'\xff'

View File

@ -31,6 +31,8 @@ QWebElement = pytest.importorskip('PyQt5.QtWebKit').QWebElement
from qutebrowser.browser import webelem
from qutebrowser.browser.webkit import webkitelem
from qutebrowser.misc import objects
from qutebrowser.utils import usertypes
def get_webelem(geometry=None, frame=None, *, null=False, style=None,
@ -715,8 +717,10 @@ class TestRectOnView:
@pytest.mark.parametrize('js_rect', [None, {}])
@pytest.mark.parametrize('zoom_text_only', [True, False])
def test_zoomed(self, stubs, config_stub, js_rect, zoom_text_only):
def test_zoomed(self, stubs, config_stub, js_rect, monkeypatch,
zoom_text_only):
"""Make sure the coordinates are adjusted when zoomed."""
monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebKit)
config_stub.val.zoom.text_only = zoom_text_only
geometry = QRect(10, 10, 4, 4)
frame = stubs.FakeWebFrame(QRect(0, 0, 100, 100), zoom=0.5)

View File

@ -21,7 +21,7 @@
import pytest
from qutebrowser.commands import runners, cmdexc
from qutebrowser.commands import runners, cmdexc, cmdutils
class TestCommandParser:
@ -66,11 +66,47 @@ class TestCommandParser:
with pytest.raises(cmdexc.NoSuchCommandError):
parser.parse_all(command)
def test_partial_parsing(self):
class TestCompletions:
"""Tests for completions.use_best_match."""
@pytest.fixture(autouse=True)
def cmdutils_stub(self, monkeypatch, stubs):
"""Patch the cmdutils module to provide fake commands."""
monkeypatch.setattr(cmdutils, 'cmd_dict', {
'one': stubs.FakeCommand(name='one'),
'two': stubs.FakeCommand(name='two'),
'two-foo': stubs.FakeCommand(name='two-foo'),
})
def test_partial_parsing(self, config_stub):
"""Test partial parsing with a runner where it's enabled.
The same with it being disabled is tested by test_parse_all.
"""
parser = runners.CommandParser(partial_match=True)
result = parser.parse('message-i')
assert result.cmd.name == 'message-info'
result = parser.parse('on')
assert result.cmd.name == 'one'
def test_dont_use_best_match(self, config_stub):
"""Test multiple completion options with use_best_match set to false.
Should raise NoSuchCommandError
"""
config_stub.val.completion.use_best_match = False
parser = runners.CommandParser(partial_match=True)
with pytest.raises(cmdexc.NoSuchCommandError):
parser.parse('tw')
def test_use_best_match(self, config_stub):
"""Test multiple completion options with use_best_match set to true.
The resulting command should be the best match
"""
config_stub.val.completion.use_best_match = True
parser = runners.CommandParser(partial_match=True)
result = parser.parse('tw')
assert result.cmd.name == 'two'

View File

@ -528,6 +528,30 @@ def test_tab_completion_delete(qtmodeltester, fake_web_tab, app_stub,
QUrl('https://duckduckgo.com')]
def test_window_completion(qtmodeltester, fake_web_tab, tabbed_browser_stubs):
tabbed_browser_stubs[0].tabs = [
fake_web_tab(QUrl('https://github.com'), 'GitHub', 0),
fake_web_tab(QUrl('https://wikipedia.org'), 'Wikipedia', 1),
fake_web_tab(QUrl('https://duckduckgo.com'), 'DuckDuckGo', 2)
]
tabbed_browser_stubs[1].tabs = [
fake_web_tab(QUrl('https://wiki.archlinux.org'), 'ArchWiki', 0)
]
model = miscmodels.window()
model.set_pattern('')
qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model)
_check_completions(model, {
'Windows': [
('0', 'window title - qutebrowser',
'GitHub, Wikipedia, DuckDuckGo'),
('1', 'window title - qutebrowser', 'ArchWiki')
]
})
def test_setting_option_completion(qtmodeltester, config_stub,
configdata_stub, info):
model = configmodel.option(info=info)

View File

@ -133,9 +133,9 @@ class TestSet:
"QtWebEngine backend!"):
commands.set(0, 'content.cookies.accept', 'all')
@pytest.mark.parametrize('option', ['?', '!', 'url.auto_search'])
@pytest.mark.parametrize('option', ['?', 'url.auto_search'])
def test_empty(self, commands, option):
"""Run ':set ?' / ':set !' / ':set url.auto_search'.
"""Run ':set ?' / ':set url.auto_search'.
Should show an error.
See https://github.com/qutebrowser/qutebrowser/issues/1109
@ -145,6 +145,16 @@ class TestSet:
"value"):
commands.set(win_id=0, option=option)
def test_toggle(self, commands):
"""Try toggling a value.
Should show an nicer error.
"""
with pytest.raises(cmdexc.CommandError,
match="Toggling values was moved to the "
":config-cycle command"):
commands.set(win_id=0, option='javascript.enabled!')
def test_invalid(self, commands):
"""Run ':set foo?'.

View File

@ -580,6 +580,52 @@ class TestConfigPy:
assert isinstance(error.exception, ZeroDivisionError)
assert error.traceback is not None
@pytest.mark.parametrize('location', ['abs', 'rel'])
def test_source(self, tmpdir, confpy, location):
if location == 'abs':
subfile = tmpdir / 'subfile.py'
arg = str(subfile)
else:
subfile = tmpdir / 'config' / 'subfile.py'
arg = 'subfile.py'
subfile.write_text("c.content.javascript.enabled = False",
encoding='utf-8')
confpy.write("config.source({!r})".format(arg))
confpy.read()
assert not config.instance._values['content.javascript.enabled']
def test_source_errors(self, tmpdir, confpy):
subfile = tmpdir / 'config' / 'subfile.py'
subfile.write_text("c.foo = 42", encoding='utf-8')
confpy.write("config.source('subfile.py')")
error = confpy.read(error=True)
assert error.text == "While setting 'foo'"
assert isinstance(error.exception, configexc.NoOptionError)
def test_source_multiple_errors(self, tmpdir, confpy):
subfile = tmpdir / 'config' / 'subfile.py'
subfile.write_text("c.foo = 42", encoding='utf-8')
confpy.write("config.source('subfile.py')", "c.bar = 23")
with pytest.raises(configexc.ConfigFileErrors) as excinfo:
configfiles.read_config_py(confpy.filename)
errors = excinfo.value.errors
assert len(errors) == 2
for error in errors:
assert isinstance(error.exception, configexc.NoOptionError)
def test_source_not_found(self, confpy):
confpy.write("config.source('doesnotexist.py')")
error = confpy.read(error=True)
assert error.text == "Error while reading doesnotexist.py"
assert isinstance(error.exception, FileNotFoundError)
class TestConfigPyWriter:

View File

@ -258,6 +258,15 @@ class TestEarlyInit:
# Font subclass, but doesn't end with "monospace"
assert 'fonts.web.family.standard' not in changed_options
def test_setting_monospace_fonts_family(self, init_patch, args):
"""Make sure setting fonts.monospace after a family works.
See https://github.com/qutebrowser/qutebrowser/issues/3130
"""
configinit.early_init(args)
config.instance.set_str('fonts.web.family.standard', '')
config.instance.set_str('fonts.monospace', 'Terminus')
def test_force_software_rendering(self, monkeypatch, config_stub):
"""Setting force_software_rendering should set the environment var."""
envvar = 'QT_XCB_FORCE_SOFTWARE_OPENGL'

View File

@ -28,8 +28,8 @@ import pytest
from qutebrowser.misc import checkpyver
TEXT = (r"At least Python 3.5 is required to run qutebrowser, but "
r"\d+\.\d+\.\d+ is installed!\n")
TEXT = (r"At least Python 3.5 is required to run qutebrowser, but it's "
r"running with \d+\.\d+\.\d+.\n")
@pytest.mark.not_frozen

View File

@ -39,7 +39,7 @@ def test_sqlerror():
class TestSqliteError:
@pytest.mark.parametrize('error_code, environmental', [
('9', True), # SQLITE_LOCKED
('5', True), # SQLITE_BUSY
('19', False), # SQLITE_CONSTRAINT
])
def test_environmental(self, error_code, environmental):

View File

@ -103,6 +103,20 @@ def test_fake_windows(tmpdir, monkeypatch, what):
assert func() == str(tmpdir / APPNAME / what)
def test_fake_haiku(tmpdir, monkeypatch):
"""Test getting data dir on HaikuOS."""
locations = {
QStandardPaths.DataLocation: '',
QStandardPaths.ConfigLocation: str(tmpdir / 'config' / APPNAME),
}
monkeypatch.setattr(standarddir.QStandardPaths, 'writableLocation',
locations.get)
monkeypatch.setattr(standarddir.sys, 'platform', 'haiku1')
standarddir._init_data(args=None)
assert standarddir.data() == str(tmpdir / 'config' / APPNAME / 'data')
class TestWritableLocation:
"""Tests for _writable_location."""

View File

@ -356,7 +356,7 @@ class TestGitStrSubprocess:
def test_real_git(self, git_repo):
"""Test with a real git repository."""
ret = version._git_str_subprocess(str(git_repo))
assert ret == 'foobar (1970-01-01 01:00:00 +0100)'
assert ret == '6e4b65a (1970-01-01 01:00:00 +0100)'
def test_missing_dir(self, tmpdir):
"""Test with a directory which doesn't exist."""