Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Jacob Sword 2017-04-07 21:21:01 -04:00
commit dadbf7657f
104 changed files with 1403 additions and 578 deletions

View File

@ -7,11 +7,14 @@ environment:
PYTHONUNBUFFERED: 1
matrix:
- TESTENV: py34
- TESTENV: py36-pyqt58
PYTHON: C:\Python36\python.exe
- TESTENV: unittests-frozen
- TESTENV: pylint
install:
- C:\Python27\python -u scripts\dev\ci\appveyor_install.py
- set PATH=%PATH%;C:\Python36
test_script:
- C:\Python34\Scripts\tox -e %TESTENV%

View File

@ -38,7 +38,8 @@ disable=no-self-use,
suppressed-message,
too-many-return-statements,
duplicate-code,
wrong-import-position
wrong-import-position,
no-else-return
[BASIC]
function-rgx=[a-z_][a-z0-9_]{2,50}$

View File

@ -21,18 +21,41 @@ Added
~~~~~
- New `:clear-messages` command to clear shown messages.
- New `ui -> keyhint-delay` setting to configure the delay until
the keyhint overlay pops up.
- New `-s` option for `:open` to force a HTTPS scheme.
- `:debug-log-filter` now accepts `none` as an argument to clear any log
filters.
Changed
~~~~~~~
- When using QtWebEngine, the underlying Chromium version is now shown in the
version info.
- Improved `qute:history` page with lazy loading
- Messages are now hidden when clicked
- Paths like `C:` are now treated as absolute paths on Windows for downloads,
and invalid paths are handled properly.
- PAC on QtWebKit now supports SOCKS5 as type.
- Comments in the config file are now before the individual
options instead of being before sections.
- The HTTP cache is disabled with QtWebKit on Qt 5.8 now as it leads to frequent
crashes due to a Qt bug.
Fixed
~~~~~
- Added a workaround for a black screen with QtWebEngine with some setups
(requires PyOpenGL to be installed)
(the workaround requires PyOpenGL to be installed, but it's optional)
- Crash when trying to retry downloads with QtWebEngine
- Crash when cloning page without history
- Continuing a search after clearing it
- Crash when downloading a download resulting in a HTTP error
- Crash when pressing ctrl-c while a config error is shown
- Crash when the key config isn't writable
- Crash when unbinding an unbound key in the key config
- Crash when using `:debug-log-filter` when `--filter` wasn't given on startup.
- Various rare crashes
v0.10.1
-------

View File

@ -124,6 +124,34 @@ When using quickmark, you can give them all names, like
`:open foodrecipes`, you will see a list of all the food recipe sites,
without having to remember the exact website title or address.
How do I use spell checking?::
Qutebrowser's support for spell checking is somewhat limited at the moment
(see https://github.com/qutebrowser/qutebrowser/issues/700[#700]), but it
can be done.
+
For QtWebKit:
. Install https://github.com/QupZilla/qtwebkit-plugins[qtwebkit-plugins].
. Note: with QtWebKit reloaded you may experience some issues. See
https://github.com/QupZilla/qtwebkit-plugins/issues/10[#10].
. The dictionary to use is taken from the `DICTIONARY` environment variable.
The default is `en_US`. For example to use Dutch spell check set `DICTIONARY`
to `nl_NL`; you can't use multiple dictionaries or change them at runtime at
the moment.
(also see the README file for `qtwebkit-plugins`).
. Remember to install the hunspell dictionaries if you don't have them already
(most distros should have packages for this).
+
For QtWebEngine:
. Not yet supported unfortunately :-( +
Adding it shouldn't be too hard though, since QtWebEngine 5.8 added an API for
this (see
https://github.com/qutebrowser/qutebrowser/issues/700#issuecomment-290780706[this
comment for a basic example]), so what are you waiting for and why aren't you
hacking qutebrowser yet?
== Troubleshooting
Configuration not saved after modifying config.::

View File

@ -135,12 +135,42 @@ If video or sound don't seem to work, try installing the gstreamer plugins:
On Gentoo
---------
qutebrowser is available in the main repository and can be installed with:
A version of qutebrowser is available in the main repository and can be installed with:
----
# emerge -av qutebrowser
----
However it is suggested to install the Live version (-9999) to take advantage
of the newest features introduced.
First of all you need to edit your package.accept_keywords file to accept the live
version:
----
# nano /etc/portage/package.accept_keywords
----
And add the following line to it:
=www-client/qutebrowser-9999 **
Save the file and then install qutebrowser via
----
# emerge -av qutebrowser
----
Or rebuild your system if you already installed it.
To update to the last Live version, remember to do
----
# emerge -uDNav @live-rebuild @world
----
To include qutebrowser among the updates.
Make sure you have `python3_4` in your `PYTHON_TARGETS`
(`/etc/portage/make.conf`) and rebuild your system (`emerge -uDNav @world`) if
necessary.

View File

@ -71,7 +71,8 @@ https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[mailinglist] at
mailto:qutebrowser@lists.qutebrowser.org[].
There's also a https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce[announce-only mailinglist]
at mailto:qutebrowser-announce@lists.qutebrowser.org[].
at mailto:qutebrowser-announce@lists.qutebrowser.org[] (the announcements also
get sent to the general qutebrowser@ list).
Contributions / Bugs
--------------------
@ -152,10 +153,11 @@ Contributors, sorted by the number of commits in descending order:
* Lamar Pavel
* Marshall Lochbaum
* Bruno Oliveira
* Martin Tournoij
* Alexander Cogneau
* Imran Sobir
* Felix Van der Jeugt
* Daniel Karbach
* Martin Tournoij
* Kevin Velghe
* Raphael Pierzina
* Joel Torstensson
@ -165,7 +167,6 @@ Contributors, sorted by the number of commits in descending order:
* Corentin Julé
* meles5
* Philipp Hansch
* Imran Sobir
* Panagiotis Ktistakis
* Artur Shaik
* Nathan Isom
@ -177,6 +178,7 @@ Contributors, sorted by the number of commits in descending order:
* Maciej Wołczyk
* Spreadyy
* Alexey "Averrin" Nabrodov
* pkill9
* nanjekyejoannah
* avk
* ZDarian
@ -205,7 +207,6 @@ Contributors, sorted by the number of commits in descending order:
* David Vogt
* Claire Cavanaugh
* rikn00
* pkill9
* kanikaa1234
* haitaka
* Nick Ginther
@ -239,6 +240,7 @@ Contributors, sorted by the number of commits in descending order:
* adam
* Samir Benmendil
* Regina Hug
* Penaz
* Mathias Fussenegger
* Marcelo Santos
* Joel Bradshaw
@ -253,6 +255,7 @@ Contributors, sorted by the number of commits in descending order:
* haxwithaxe
* evan
* dylan araps
* caveman
* addictedtoflames
* Xitian9
* Vasilij Schneidermann
@ -284,6 +287,7 @@ Contributors, sorted by the number of commits in descending order:
* Arseniy Seroka
* Andy Balaam
* Andreas Fischer
* Amos Bird
* Akselmo
// QUTE_AUTHORS_END

View File

@ -544,7 +544,7 @@ For `increment` and `decrement`, the number to change the URL by. For `up`, the
[[open]]
=== open
Syntax: +:open [*--implicit*] [*--bg*] [*--tab*] [*--window*] ['url']+
Syntax: +:open [*--implicit*] [*--bg*] [*--tab*] [*--window*] [*--secure*] ['url']+
Open a URL in the current/[count]th tab.
@ -559,6 +559,7 @@ If the URL contains newlines, each line gets opened in its own tab.
* +*-b*+, +*--bg*+: Open in a new background tab.
* +*-t*+, +*--tab*+: Open in a new tab.
* +*-w*+, +*--window*+: Open in a new window.
* +*-s*+, +*--secure*+: Force HTTPS.
==== count
The tab index to open the URL in.
@ -1598,7 +1599,8 @@ Syntax: +:debug-log-filter 'filters'+
Change the log filter for console logging.
==== positional arguments
* +'filters'+: A comma separated list of logger names.
* +'filters'+: A comma separated list of logger names. Can also be "none" to clear any existing filters.
[[debug-log-level]]
=== debug-log-level

View File

@ -36,6 +36,7 @@
[options="header",width="75%",cols="25%,75%"]
|==============
|Setting|Description
|<<ui-history-session-interval,history-session-interval>>|The maximum time in minutes between two history items for them to be considered being from the same session. Use -1 to disable separation.
|<<ui-zoom-levels,zoom-levels>>|The available zoom levels, separated by commas.
|<<ui-default-zoom,default-zoom>>|The default zoom level.
|<<ui-downloads-position,downloads-position>>|Where to show the downloaded files.
@ -56,6 +57,7 @@
|<<ui-modal-js-dialog,modal-js-dialog>>|Use standard JavaScript modal dialog for alert() and confirm()
|<<ui-hide-wayland-decoration,hide-wayland-decoration>>|Hide the window decoration when using wayland (requires restart)
|<<ui-keyhint-blacklist,keyhint-blacklist>>|Keychains that shouldn't be shown in the keyhint dialog
|<<ui-keyhint-delay,keyhint-delay>>|Time from pressing a key to seeing the keyhint dialog (ms)
|<<ui-prompt-radius,prompt-radius>>|The rounding radius for the edges of prompts.
|<<ui-prompt-filebrowser,prompt-filebrowser>>|Show a filebrowser in upload/download prompts.
|==============
@ -536,6 +538,12 @@ Default: +pass:[path,query]+
== ui
General options related to the user interface.
[[ui-history-session-interval]]
=== history-session-interval
The maximum time in minutes between two history items for them to be considered being from the same session. Use -1 to disable separation.
Default: +pass:[30]+
[[ui-zoom-levels]]
=== zoom-levels
The available zoom levels, separated by commas.
@ -732,6 +740,12 @@ Globs are supported, so ';*' will blacklist all keychainsstarting with ';'. Use
Default: empty
[[ui-keyhint-delay]]
=== keyhint-delay
Time from pressing a key to seeing the keyhint dialog (ms)
Default: +pass:[500]+
[[ui-prompt-radius]]
=== prompt-radius
The rounding radius for the edges of prompts.

View File

@ -3,6 +3,6 @@
appdirs==1.4.3
packaging==16.8
pyparsing==2.2.0
setuptools==34.3.2
setuptools==34.3.3
six==1.10.0
wheel==0.29.0

View File

@ -2,10 +2,13 @@
-e git+https://github.com/PyCQA/astroid.git#egg=astroid
editdistance==0.3.1
github3.py==0.9.6
isort==4.2.5
lazy-object-proxy==1.2.2
mccabe==0.6.1
-e git+https://github.com/PyCQA/pylint.git#egg=pylint
./scripts/dev/pylint_checkers
requests==2.13.0
uritemplate==3.0.0
uritemplate.py==3.0.2
wrapt==1.10.10

View File

@ -2,6 +2,7 @@
-e git+https://github.com/PyCQA/pylint.git#egg=pylint
./scripts/dev/pylint_checkers
requests
github3.py
# remove @commit-id for scm installs
#@ replace: @.*# #

View File

@ -1,4 +1,4 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.8.1.1
sip==4.19.1
PyQt5==5.8.2
sip==4.19.2

View File

@ -1,15 +1,15 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
beautifulsoup4==4.5.3
cheroot==5.3.0
cheroot==5.4.0
click==6.7
coverage==4.3.4
decorator==4.0.11
EasyProcess==0.2.3
Flask==0.12
Flask==0.12.1
glob2==0.5
httpbin==0.5.0
hypothesis==3.6.1
hypothesis==3.7.0
itsdangerous==0.24
# Jinja2==2.9.5
Mako==1.0.6
@ -24,7 +24,7 @@ pytest-catchlog==1.2.2
pytest-cov==2.4.0
pytest-faulthandler==1.3.1
pytest-instafail==0.3.0
pytest-mock==1.5.0
pytest-mock==1.6.0
pytest-qt==2.1.0
pytest-repeat==0.4.1
pytest-rerunfailures==2.1.0

View File

@ -2,5 +2,5 @@
pluggy==0.4.0
py==1.4.33
tox==2.6.0
tox==2.7.0
virtualenv==15.1.0

View File

@ -9,7 +9,7 @@ directly ask me via IRC (nickname thorsten\`) in #qutebrowser on freenode.
$blink!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!$reset
WARNING: the passwords are stored in qutebrowser's
debug log reachable via the url qute:log
debug log reachable via the url qute://log
$blink!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!$reset
Usage: run as a userscript form qutebrowser, e.g.:

View File

@ -24,11 +24,12 @@ markers =
js_prompt: Tests needing to display a javascript prompt
this: Used to mark tests during development
no_invalid_lines: Don't fail on unparseable lines in end2end tests
issue2478: Tests which are broken on Windows with QtWebEngine, https://github.com/qutebrowser/qutebrowser/issues/2478
qt_log_level_fail = WARNING
qt_log_ignore =
^SpellCheck: .*
^SetProcessDpiAwareness failed: .*
^QWindowsWindow::setGeometryDp: Unable to set geometry .*
^QWindowsWindow::setGeometry(Dp)?: Unable to set geometry .*
^QProcess: Destroyed while process .* is still running\.
^"Method "GetAll" with signature "s" on interface "org\.freedesktop\.DBus\.Properties" doesn't exist
^"Method \\"GetAll\\" with signature \\"s\\" on interface \\"org\.freedesktop\.DBus\.Properties\\" doesn't exist\\n"
@ -51,4 +52,5 @@ qt_log_ignore =
^Image of format '' blocked because it is not considered safe. If you are sure it is safe to do so, you can white-list the format by setting the environment variable QTWEBKIT_IMAGEFORMAT_WHITELIST=
^QPainter::end: Painter ended with \d+ saved states
^QSslSocket: cannot resolve SSLv[23]_(client|server)_method
^QQuickWidget::invalidateRenderControl could not make context current
xfail_strict = true

View File

@ -170,12 +170,15 @@ def _init_icon():
for size in [16, 24, 32, 48, 64, 96, 128, 256, 512]:
filename = ':/icons/qutebrowser-{}x{}.png'.format(size, size)
pixmap = QPixmap(filename)
qtutils.ensure_not_null(pixmap)
fallback_icon.addPixmap(pixmap)
qtutils.ensure_not_null(fallback_icon)
if pixmap.isNull():
log.init.warning("Failed to load {}".format(filename))
else:
fallback_icon.addPixmap(pixmap)
icon = QIcon.fromTheme('qutebrowser', fallback_icon)
qtutils.ensure_not_null(icon)
qApp.setWindowIcon(icon)
if icon.isNull():
log.init.warning("Failed to load icon")
else:
qApp.setWindowIcon(icon)
def _process_args(args):
@ -301,7 +304,7 @@ def _open_startpage(win_id=None):
window_ids = [win_id]
else:
window_ids = objreg.window_registry
for cur_win_id in window_ids:
for cur_win_id in list(window_ids): # Copying as the dict could change
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=cur_win_id)
if tabbed_browser.count() == 0:
@ -340,8 +343,9 @@ def _open_quickstart(args):
def _save_version():
"""Save the current version to the state config."""
state_config = objreg.get('state-config')
state_config['general']['version'] = qutebrowser.__version__
state_config = objreg.get('state-config', None)
if state_config is not None:
state_config['general']['version'] = qutebrowser.__version__
def on_focus_changed(_old, new):
@ -647,14 +651,14 @@ class Quitter:
self._shutting_down = True
log.destroy.debug("Shutting down with status {}, session {}...".format(
status, session))
session_manager = objreg.get('session-manager')
if session is not None:
session_manager.save(session, last_window=last_window,
load_next_time=True)
elif config.get('general', 'save-session'):
session_manager.save(sessions.default, last_window=last_window,
load_next_time=True)
session_manager = objreg.get('session-manager', None)
if session_manager is not None:
if session is not None:
session_manager.save(session, last_window=last_window,
load_next_time=True)
elif config.get('general', 'save-session'):
session_manager.save(sessions.default, last_window=last_window,
load_next_time=True)
if prompt.prompt_queue.shutdown():
# If shutdown was called while we were asking a question, we're in
@ -671,7 +675,7 @@ class Quitter:
# event loop, so we can shut down immediately.
self._shutdown(status, restart=restart)
def _shutdown(self, status, restart):
def _shutdown(self, status, restart): # noqa
"""Second stage of shutdown."""
log.destroy.debug("Stage 2 of shutting down...")
if qApp is None:
@ -680,7 +684,9 @@ class Quitter:
# Remove eventfilter
try:
log.destroy.debug("Removing eventfilter...")
qApp.removeEventFilter(objreg.get('event-filter'))
event_filter = objreg.get('event-filter', None)
if event_filter is not None:
qApp.removeEventFilter(event_filter)
except AttributeError:
pass
# Close all windows
@ -722,7 +728,9 @@ class Quitter:
# Now we can hopefully quit without segfaults
log.destroy.debug("Deferring QApplication::exit...")
objreg.get('signal-handler').deactivate()
objreg.get('session-manager').delete_autosave()
session_manager = objreg.get('session-manager', None)
if session_manager is not None:
session_manager.delete_autosave()
# We use a singleshot timer to exit here to minimize the likelihood of
# segfaults.
QTimer.singleShot(0, functools.partial(qApp.exit, status))

View File

@ -236,7 +236,7 @@ class CommandDispatcher:
@cmdutils.argument('url', completion=usertypes.Completion.url)
@cmdutils.argument('count', count=True)
def openurl(self, url=None, implicit=False,
bg=False, tab=False, window=False, count=None):
bg=False, tab=False, window=False, count=None, secure=False):
"""Open a URL in the current/[count]th tab.
If the URL contains newlines, each line gets opened in its own tab.
@ -249,6 +249,7 @@ class CommandDispatcher:
implicit: If opening a new tab, treat the tab as implicit (like
clicking on a link).
count: The tab index to open the URL in, or None.
secure: Force HTTPS.
"""
if url is None:
urls = [config.get('general', 'default-page')]
@ -256,6 +257,8 @@ class CommandDispatcher:
urls = self._parse_url_input(url)
for i, cur_url in enumerate(urls):
if secure:
cur_url.setScheme('https')
if not window and i > 0:
tab = False
bg = True
@ -1334,6 +1337,9 @@ class CommandDispatcher:
scope='window', window=self._win_id)
target = None
if dest is not None:
dest = downloads.transform_path(dest)
if dest is None:
raise cmdexc.CommandError("Invalid target filename")
target = downloads.FileDownloadTarget(dest)
tab = self._current_widget()
@ -1536,10 +1542,7 @@ class CommandDispatcher:
backend=usertypes.Backend.QtWebKit)
def paste_primary(self):
"""Paste the primary selection at cursor position."""
try:
self.insert_text(utils.get_clipboard(selection=True))
except utils.SelectionUnsupportedError:
self.insert_text(utils.get_clipboard())
self.insert_text(utils.get_clipboard(selection=True, fallback=True))
@cmdutils.register(instance='command-dispatcher', maxsplit=0,
scope='window')
@ -1650,21 +1653,22 @@ class CommandDispatcher:
tab = self._current_widget()
tab.search.clear()
if not text:
return
options = {
'ignore_case': config.get('general', 'ignore-case'),
'reverse': reverse,
}
self._tabbed_browser.search_text = text
self._tabbed_browser.search_options = dict(options)
if text:
cb = functools.partial(self._search_cb, tab=tab,
old_scroll_pos=tab.scroller.pos_px(),
options=options, text=text, prev=False)
else:
cb = None
cb = functools.partial(self._search_cb, tab=tab,
old_scroll_pos=tab.scroller.pos_px(),
options=options, text=text, prev=False)
options['result_cb'] = cb
tab.search.search(text, **options)
@cmdutils.register(instance='command-dispatcher', hide=True,

View File

@ -19,11 +19,13 @@
"""Shared QtWebKit/QtWebEngine code for downloads."""
import re
import sys
import html
import os.path
import collections
import functools
import pathlib
import tempfile
import sip
@ -161,6 +163,25 @@ def get_filename_question(*, suggested_filename, url, parent=None):
return q
def transform_path(path):
r"""Do platform-specific transformations, like changing E: to E:\.
Returns None if the path is invalid on the current platform.
"""
if sys.platform != "win32":
return path
path = utils.expand_windows_drive(path)
# Drive dependent working directories are not supported, e.g.
# E:filename is invalid
if re.match(r'[A-Z]:[^\\]', path, re.IGNORECASE):
return None
# Paths like COM1, ...
# See https://github.com/qutebrowser/qutebrowser/issues/82
if pathlib.Path(path).is_reserved():
return None
return path
class NoFilenameError(Exception):
"""Raised when we can't find out a filename in DownloadTarget."""
@ -507,6 +528,14 @@ class AbstractDownloadItem(QObject):
"""Retry a failed download."""
raise NotImplementedError
@pyqtSlot()
def try_retry(self):
"""Try to retry a download and show an error if it's unsupported."""
try:
self.retry()
except UnsupportedOperationError as e:
message.error(str(e))
def _get_open_filename(self):
"""Get the filename to open a download.
@ -968,7 +997,7 @@ class DownloadModel(QAbstractListModel):
raise cmdexc.CommandError("No failed downloads!")
else:
download = to_retry[0]
download.retry()
download.try_retry()
def can_clear(self):
"""Check if there are finished downloads to clear."""

View File

@ -134,7 +134,7 @@ class DownloadView(QListView):
if item.successful:
actions.append(("Open", item.open_file))
else:
actions.append(("Retry", item.retry))
actions.append(("Retry", item.try_retry))
actions.append(("Remove", item.remove))
else:
actions.append(("Cancel", item.cancel))

View File

@ -155,7 +155,7 @@ class PACResolver:
raise ParseProxyError("Invalid number of parameters for PROXY")
host, port = PACResolver._parse_proxy_host(config[1])
return QNetworkProxy(QNetworkProxy.HttpProxy, host, port)
elif config[0] == "SOCKS":
elif config[0] in ["SOCKS", "SOCKS5"]:
if len(config) != 2:
raise ParseProxyError("Invalid number of parameters for SOCKS")
host, port = PACResolver._parse_proxy_host(config[1])

View File

@ -110,6 +110,9 @@ class DownloadItem(downloads.AbstractDownloadItem):
def _do_die(self):
"""Abort the download and emit an error."""
self._read_timer.stop()
if self._reply is None:
log.downloads.debug("Reply gone while dying")
return
self._reply.downloadProgress.disconnect()
self._reply.finished.disconnect()
self._reply.error.disconnect()
@ -270,7 +273,7 @@ class DownloadItem(downloads.AbstractDownloadItem):
if self.fileobj is None or self._reply is None:
# No filename has been set yet (so we don't empty the buffer) or we
# got a readyRead after the reply was finished (which happens on
# qute:log for example).
# qute://log for example).
return
if not self._reply.isOpen():
raise OSError("Reply is closed!")

View File

@ -17,23 +17,26 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Backend-independent qute:* code.
"""Backend-independent qute://* code.
Module attributes:
pyeval_output: The output of the last :pyeval command.
_HANDLERS: The handlers registered via decorators.
"""
import json
import os
import sys
import time
import datetime
import urllib.parse
import datetime
from PyQt5.QtCore import QUrlQuery
from PyQt5.QtCore import QUrlQuery, QUrl
import qutebrowser
from qutebrowser.config import config
from qutebrowser.utils import (version, utils, jinja, log, message, docutils,
objreg)
objreg, usertypes, qtutils)
from qutebrowser.misc import objects
@ -75,12 +78,25 @@ class QuteSchemeError(Exception):
super().__init__(errorstring)
class add_handler: # pylint: disable=invalid-name
class Redirect(Exception):
"""Decorator to register a qute:* URL handler.
"""Exception to signal a redirect should happen.
Attributes:
_name: The 'foo' part of qute:foo
url: The URL to redirect to, as a QUrl.
"""
def __init__(self, url):
super().__init__(url.toDisplayString())
self.url = url
class add_handler: # pylint: disable=invalid-name
"""Decorator to register a qute://* URL handler.
Attributes:
_name: The 'foo' part of qute://foo
backend: Limit which backends the handler can run with.
"""
@ -103,7 +119,7 @@ class add_handler: # pylint: disable=invalid-name
def wrong_backend_handler(self, url):
"""Show an error page about using the invalid backend."""
html = jinja.render('error.html',
title="Error while opening qute:url",
title="Error while opening qute://url",
url=url.toDisplayString(),
error='{} is not available with this '
'backend'.format(url.toDisplayString()),
@ -125,13 +141,17 @@ def data_for_url(url):
# A url like "qute:foo" is split as "scheme:path", not "scheme:host".
log.misc.debug("url: {}, path: {}, host {}".format(
url.toDisplayString(), path, host))
if path and not host:
new_url = QUrl()
new_url.setScheme('qute')
new_url.setHost(path)
raise Redirect(new_url)
try:
handler = _HANDLERS[path]
handler = _HANDLERS[host]
except KeyError:
try:
handler = _HANDLERS[host]
except KeyError:
raise NoHandlerFound(url)
raise NoHandlerFound(url)
try:
mimetype, data = handler(url)
except OSError as e:
@ -150,7 +170,7 @@ def data_for_url(url):
@add_handler('bookmarks')
def qute_bookmarks(_url):
"""Handler for qute:bookmarks. Display all quickmarks / bookmarks."""
"""Handler for qute://bookmarks. Display all quickmarks / bookmarks."""
bookmarks = sorted(objreg.get('bookmark-manager').marks.items(),
key=lambda x: x[1]) # Sort by title
quickmarks = sorted(objreg.get('quickmark-manager').marks.items(),
@ -163,90 +183,164 @@ def qute_bookmarks(_url):
return 'text/html', html
@add_handler('history') # noqa
def qute_history(url):
"""Handler for qute:history. Display history."""
# Get current date from query parameter, if not given choose today.
curr_date = datetime.date.today()
try:
query_date = QUrlQuery(url).queryItemValue("date")
if query_date:
curr_date = datetime.datetime.strptime(query_date, "%Y-%m-%d")
curr_date = curr_date.date()
except ValueError:
log.misc.debug("Invalid date passed to qute:history: " + query_date)
def history_data(start_time): # noqa
"""Return history data
one_day = datetime.timedelta(days=1)
next_date = curr_date + one_day
prev_date = curr_date - one_day
Arguments:
start_time -- select history starting from this timestamp.
"""
def history_iter(start_time, reverse=False):
"""Iterate through the history and get items we're interested.
def history_iter(reverse):
"""Iterate through the history and get items we're interested in."""
curr_timestamp = time.mktime(curr_date.timetuple())
Arguments:
reverse -- whether to reverse the history_dict before iterating.
"""
history = objreg.get('web-history').history_dict.values()
if reverse:
history = reversed(history)
# when history_dict is not reversed, we need to keep track of last item
# so that we can yield its atime
last_item = None
# end is 24hrs earlier than start
end_time = start_time - 24*60*60
for item in history:
# If we can't apply the reverse performance trick below,
# at least continue as early as possible with old items.
# This gets us down from 550ms to 123ms with 500k old items on my
# machine.
if item.atime < curr_timestamp and not reverse:
continue
# Convert timestamp
try:
item_atime = datetime.datetime.fromtimestamp(item.atime)
except (ValueError, OSError, OverflowError):
log.misc.debug("Invalid timestamp {}.".format(item.atime))
continue
if reverse and item_atime.date() < curr_date:
# If we could reverse the history in-place, and this entry is
# older than today, only older entries will follow, so we can
# abort here.
return
# Skip items not on curr_date
# Skip redirects
# Skip qute:// links
is_internal = item.url.scheme() == 'qute'
is_not_today = item_atime.date() != curr_date
if item.redirect or is_internal or is_not_today:
if item.redirect or item.url.scheme() == 'qute':
continue
# Skip items out of time window
item_newer = item.atime > start_time
item_older = item.atime <= end_time
if reverse:
# history_dict is reversed, we are going back in history.
# so:
# abort if item is older than start_time+24hr
# skip if item is newer than start
if item_older:
yield {"next": int(item.atime)}
return
if item_newer:
continue
else:
# history_dict isn't reversed, we are going forward in history.
# so:
# abort if item is newer than start_time
# skip if item is older than start_time+24hrs
if item_older:
last_item = item
continue
if item_newer:
yield {"next": int(last_item.atime if last_item else -1)}
return
# Use item's url as title if there's no title.
item_url = item.url.toDisplayString()
item_title = item.title if item.title else item_url
display_atime = item_atime.strftime("%X")
item_time = int(item.atime * 1000)
yield (item_url, item_title, display_atime)
yield {"url": item_url, "title": item_title, "time": item_time}
# if we reached here, we had reached the end of history
yield {"next": int(last_item.atime if last_item else -1)}
if sys.hexversion >= 0x03050000:
# On Python >= 3.5 we can reverse the ordereddict in-place and thus
# apply an additional performance improvement in history_iter.
# On my machine, this gets us down from 550ms to 72us with 500k old
# items.
history = list(history_iter(reverse=True))
history = history_iter(start_time, reverse=True)
else:
# On Python 3.4, we can't do that, so we'd need to copy the entire
# history to a list. There, filter first and then reverse it here.
history = reversed(list(history_iter(reverse=False)))
history = reversed(list(history_iter(start_time, reverse=False)))
html = jinja.render('history.html',
title='History',
history=history,
curr_date=curr_date,
next_date=next_date,
prev_date=prev_date,
today=datetime.date.today())
return 'text/html', html
return list(history)
@add_handler('history')
def qute_history(url):
"""Handler for qute://history. Display and serve history."""
if url.path() == '/data':
# Use start_time in query or current time.
try:
start_time = QUrlQuery(url).queryItemValue("start_time")
start_time = float(start_time) if start_time else time.time()
except ValueError as e:
raise QuteSchemeError("Query parameter start_time is invalid", e)
return 'text/html', json.dumps(history_data(start_time))
else:
try:
from PyQt5.QtWebKit import qWebKitVersion
is_webkit_ng = qtutils.is_qtwebkit_ng(qWebKitVersion())
except ImportError: # pragma: no cover
is_webkit_ng = False
if (
config.get('content', 'allow-javascript') and
(objects.backend == usertypes.Backend.QtWebEngine or is_webkit_ng)
):
return 'text/html', jinja.render(
'history.html',
title='History',
session_interval=config.get('ui', 'history-session-interval')
)
else:
# Get current date from query parameter, if not given choose today.
curr_date = datetime.date.today()
try:
query_date = QUrlQuery(url).queryItemValue("date")
if query_date:
curr_date = datetime.datetime.strptime(query_date,
"%Y-%m-%d").date()
except ValueError:
log.misc.debug("Invalid date passed to qute:history: " +
query_date)
one_day = datetime.timedelta(days=1)
next_date = curr_date + one_day
prev_date = curr_date - one_day
# start_time is the last second of curr_date
start_time = time.mktime(next_date.timetuple()) - 1
history = [
(i["url"], i["title"],
datetime.datetime.fromtimestamp(i["time"]/1000))
for i in history_data(start_time) if "next" not in i
]
return 'text/html', jinja.render(
'history_nojs.html',
title='History',
history=history,
curr_date=curr_date,
next_date=next_date,
prev_date=prev_date,
today=datetime.date.today(),
)
@add_handler('javascript')
def qute_javascript(url):
"""Handler for qute://javascript.
Return content of file given as query parameter.
"""
path = url.path()
if path:
path = "javascript" + os.sep.join(path.split('/'))
return 'text/html', utils.read_file(path, binary=False)
else:
raise QuteSchemeError("No file specified", ValueError())
@add_handler('pyeval')
def qute_pyeval(_url):
"""Handler for qute:pyeval."""
"""Handler for qute://pyeval."""
html = jinja.render('pre.html', title='pyeval', content=pyeval_output)
return 'text/html', html
@ -254,7 +348,7 @@ def qute_pyeval(_url):
@add_handler('version')
@add_handler('verizon')
def qute_version(_url):
"""Handler for qute:version."""
"""Handler for qute://version."""
html = jinja.render('version.html', title='Version info',
version=version.version(),
copyright=qutebrowser.__copyright__)
@ -263,7 +357,7 @@ def qute_version(_url):
@add_handler('plainlog')
def qute_plainlog(url):
"""Handler for qute:plainlog.
"""Handler for qute://plainlog.
An optional query parameter specifies the minimum log level to print.
For example, qute://log?level=warning prints warnings and errors.
@ -283,7 +377,7 @@ def qute_plainlog(url):
@add_handler('log')
def qute_log(url):
"""Handler for qute:log.
"""Handler for qute://log.
An optional query parameter specifies the minimum log level to print.
For example, qute://log?level=warning prints warnings and errors.
@ -304,13 +398,13 @@ def qute_log(url):
@add_handler('gpl')
def qute_gpl(_url):
"""Handler for qute:gpl. Return HTML content as string."""
"""Handler for qute://gpl. Return HTML content as string."""
return 'text/html', utils.read_file('html/COPYING.html')
@add_handler('help')
def qute_help(url):
"""Handler for qute:help."""
"""Handler for qute://help."""
try:
utils.read_file('html/doc/index.html')
except OSError:

View File

@ -78,24 +78,24 @@ class DownloadItem(downloads.AbstractDownloadItem):
self.stats.finish()
elif state == QWebEngineDownloadItem.DownloadInterrupted:
self.successful = False
self.done = True
# https://bugreports.qt.io/browse/QTBUG-56839
self.error.emit("Download failed")
self.stats.finish()
self._die("Download failed")
else:
raise ValueError("_on_state_changed was called with unknown state "
"{}".format(state_name))
def _do_die(self):
self._qt_item.downloadProgress.disconnect()
self._qt_item.cancel()
if self._qt_item.state() != QWebEngineDownloadItem.DownloadInterrupted:
self._qt_item.cancel()
def _do_cancel(self):
self._qt_item.cancel()
def retry(self):
# https://bugreports.qt.io/browse/QTBUG-56840
raise downloads.UnsupportedOperationError
raise downloads.UnsupportedOperationError(
"Retrying downloads is unsupported with QtWebEngine")
def _get_open_filename(self):
return self._filename
@ -104,6 +104,7 @@ class DownloadItem(downloads.AbstractDownloadItem):
raise downloads.UnsupportedOperationError
def _set_tempfile(self, fileobj):
fileobj.close()
self._set_filename(fileobj.name, force_overwrite=True,
remember_directory=False)

View File

@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""QtWebEngine specific qute:* handlers and glue code."""
"""QtWebEngine specific qute://* handlers and glue code."""
from PyQt5.QtCore import QBuffer, QIODevice
# pylint: disable=no-name-in-module,import-error,useless-suppression
@ -26,15 +26,15 @@ from PyQt5.QtWebEngineCore import (QWebEngineUrlSchemeHandler,
# pylint: enable=no-name-in-module,import-error,useless-suppression
from qutebrowser.browser import qutescheme
from qutebrowser.utils import log
from qutebrowser.utils import log, qtutils
class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
"""Handle qute:* requests on QtWebEngine."""
"""Handle qute://* requests on QtWebEngine."""
def install(self, profile):
"""Install the handler for qute: URLs on the given profile."""
"""Install the handler for qute:// URLs on the given profile."""
profile.installUrlSchemeHandler(b'qute', self)
def requestStarted(self, job):
@ -58,12 +58,15 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
job.fail(QWebEngineUrlRequestJob.UrlNotFound)
except qutescheme.QuteSchemeOSError:
# FIXME:qtwebengine how do we show a better error here?
log.misc.exception("OSError while handling qute:* URL")
log.misc.exception("OSError while handling qute://* URL")
job.fail(QWebEngineUrlRequestJob.UrlNotFound)
except qutescheme.QuteSchemeError:
# FIXME:qtwebengine how do we show a better error here?
log.misc.exception("Error while handling qute:* URL")
log.misc.exception("Error while handling qute://* URL")
job.fail(QWebEngineUrlRequestJob.RequestFailed)
except qutescheme.Redirect as e:
qtutils.ensure_valid(e.url)
job.redirect(e.url)
else:
log.misc.debug("Returning {} data".format(mimetype))

View File

@ -55,7 +55,7 @@ def init():
app = QApplication.instance()
profile = QWebEngineProfile.defaultProfile()
log.init.debug("Initializing qute:* handler...")
log.init.debug("Initializing qute://* handler...")
_qute_scheme_handler = webenginequtescheme.QuteSchemeHandler(parent=app)
_qute_scheme_handler.install(profile)
@ -369,8 +369,13 @@ class WebEngineHistory(browsertab.AbstractHistory):
return self._history.canGoForward()
def serialize(self):
# WORKAROUND for https://github.com/qutebrowser/qutebrowser/issues/2289
# FIXME:qtwebengine can we get rid of this with Qt 5.8.1?
# WORKAROUND (remove this when we bump the requirements to 5.9)
# https://bugreports.qt.io/browse/QTBUG-59599
if self._history.count() == 0:
raise browsertab.WebTabError("Can't serialize page without "
"history!")
# WORKAROUND (FIXME: remove this when we bump the requirements to 5.9?)
# https://github.com/qutebrowser/qutebrowser/issues/2289
scheme = self._history.currentItem().url().scheme()
if scheme in ['view-source', 'chrome']:
raise browsertab.WebTabError("Can't serialize special URL!")

View File

@ -25,7 +25,7 @@ from PyQt5.QtCore import pyqtSlot
from PyQt5.QtNetwork import QNetworkDiskCache, QNetworkCacheMetaData
from qutebrowser.config import config
from qutebrowser.utils import utils, objreg
from qutebrowser.utils import utils, objreg, qtutils
class DiskCache(QNetworkDiskCache):
@ -53,6 +53,9 @@ class DiskCache(QNetworkDiskCache):
size = config.get('storage', 'cache-size')
if size is None:
size = 1024 * 1024 * 50 # default from QNetworkDiskCachePrivate
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-59909
if qtutils.version_check('5.7.1'): # pragma: no cover
size = 0
self.setMaximumCacheSize(size)
def _maybe_activate(self):

View File

@ -19,6 +19,10 @@
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
#
# For some reason, a segfault will be triggered if the unnecessary lambdas in
# this file aren't there.
# pylint: disable=unnecessary-lambda
"""Special network replies.."""
@ -114,9 +118,6 @@ class ErrorNetworkReply(QNetworkReply):
# the device to avoid getting a warning.
self.setOpenMode(QIODevice.ReadOnly)
self.setError(error, errorstring)
# For some reason, a segfault will be triggered if these lambdas aren't
# there.
# pylint: disable=unnecessary-lambda
QTimer.singleShot(0, lambda: self.error.emit(error))
QTimer.singleShot(0, lambda: self.finished.emit())
@ -137,3 +138,16 @@ class ErrorNetworkReply(QNetworkReply):
def isRunning(self):
return False
class RedirectNetworkReply(QNetworkReply):
"""A reply which redirects to the given URL."""
def __init__(self, new_url, parent=None):
super().__init__(parent)
self.setAttribute(QNetworkRequest.RedirectionTargetAttribute, new_url)
QTimer.singleShot(0, lambda: self.finished.emit())
def readData(self, _maxlen):
return bytes()

View File

@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""QtWebKit specific qute:* handlers and glue code."""
"""QtWebKit specific qute://* handlers and glue code."""
import mimetypes
import functools
@ -28,13 +28,13 @@ from PyQt5.QtNetwork import QNetworkReply
from qutebrowser.browser import pdfjs, qutescheme
from qutebrowser.browser.webkit.network import schemehandler, networkreply
from qutebrowser.utils import jinja, log, message, objreg, usertypes
from qutebrowser.utils import jinja, log, message, objreg, usertypes, qtutils
from qutebrowser.config import configexc, configdata
class QuteSchemeHandler(schemehandler.SchemeHandler):
"""Scheme handler for qute: URLs."""
"""Scheme handler for qute:// URLs."""
def createRequest(self, _op, request, _outgoing_data):
"""Create a new request.
@ -62,6 +62,9 @@ class QuteSchemeHandler(schemehandler.SchemeHandler):
except qutescheme.QuteSchemeError as e:
return networkreply.ErrorNetworkReply(request, e.errorstring,
e.error, self.parent())
except qutescheme.Redirect as e:
qtutils.ensure_valid(e.url)
return networkreply.RedirectNetworkReply(e.url, self.parent())
return networkreply.FixedDataNetworkReply(request, data, mimetype,
self.parent())
@ -69,15 +72,15 @@ class QuteSchemeHandler(schemehandler.SchemeHandler):
class JSBridge(QObject):
"""Javascript-bridge for special qute:... pages."""
"""Javascript-bridge for special qute://... pages."""
@pyqtSlot(str, str, str)
def set(self, sectname, optname, value):
"""Slot to set a setting from qute:settings."""
"""Slot to set a setting from qute://settings."""
# https://github.com/qutebrowser/qutebrowser/issues/727
if ((sectname, optname) == ('content', 'allow-javascript') and
value == 'false'):
message.error("Refusing to disable javascript via qute:settings "
message.error("Refusing to disable javascript via qute://settings "
"as it needs javascript support.")
return
try:
@ -88,7 +91,7 @@ class JSBridge(QObject):
@qutescheme.add_handler('settings', backend=usertypes.Backend.QtWebKit)
def qute_settings(_url):
"""Handler for qute:settings. View/change qute configuration."""
"""Handler for qute://settings. View/change qute configuration."""
config_getter = functools.partial(objreg.get('config').get, raw=True)
html = jinja.render('settings.html', title='settings', config=configdata,
confget=config_getter)

View File

@ -23,6 +23,7 @@ import sys
import functools
import xml.etree.ElementTree
import sip
from PyQt5.QtCore import (pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer, QSizeF,
QSize)
from PyQt5.QtGui import QKeyEvent
@ -707,6 +708,9 @@ class WebKitTab(browsertab.AbstractTab):
@pyqtSlot()
def _on_webkit_icon_changed(self):
"""Emit iconChanged with a QIcon like QWebEngineView does."""
if sip.isdeleted(self._widget):
log.webview.debug("Got _on_webkit_icon_changed for deleted view!")
return
self.icon_changed.emit(self._widget.icon())
@pyqtSlot(QWebFrame)

View File

@ -140,7 +140,7 @@ class WebView(QWebView):
@pyqtSlot()
def add_js_bridge(self):
"""Add the javascript bridge for qute:... pages."""
"""Add the javascript bridge for qute://... pages."""
frame = self.sender()
if not isinstance(frame, QWebFrame):
log.webview.error("Got non-QWebFrame {!r} in "

View File

@ -133,7 +133,8 @@ class CommandRunner(QObject):
Yields:
ParseResult tuples.
"""
if not text.strip():
text = text.strip().lstrip(':').strip()
if not text:
raise cmdexc.NoSuchCommandError("No command given")
if aliases:

View File

@ -186,7 +186,8 @@ def _init_key_config(parent):
key_config = keyconf.KeyConfigParser(standarddir.config(), 'keys.conf',
args.relaxed_config,
parent=parent)
except (keyconf.KeyConfigError, UnicodeDecodeError) as e:
except (keyconf.KeyConfigError, cmdexc.CommandError,
UnicodeDecodeError) as e:
log.init.exception(e)
errstr = "Error while reading key config:\n"
if e.lineno is not None:
@ -471,10 +472,9 @@ class ConfigManager(QObject):
"""Get the whole config as a string."""
lines = configdata.FIRST_COMMENT.strip('\n').splitlines()
for sectname, sect in self.sections.items():
lines.append('\n[{}]'.format(sectname))
lines += self._str_section_desc(sectname)
lines += self._str_option_desc(sectname, sect)
lines += self._str_items(sect)
lines += ['\n'] + self._str_section_desc(sectname)
lines.append('[{}]'.format(sectname))
lines += self._str_items(sectname, sect)
return '\n'.join(lines) + '\n'
def _str_section_desc(self, sectname):
@ -489,42 +489,7 @@ class ConfigManager(QObject):
lines += wrapper.wrap(secline)
return lines
def _str_option_desc(self, sectname, sect):
"""Get the option description strings for sect/sectname."""
wrapper = textwrapper.TextWrapper(initial_indent='#' + ' ' * 5,
subsequent_indent='#' + ' ' * 5)
lines = []
if not getattr(sect, 'descriptions', None):
return lines
for optname, option in sect.items():
lines.append('#')
typestr = ' ({})'.format(option.typ.get_name())
lines.append("# {}{}:".format(optname, typestr))
try:
desc = self.sections[sectname].descriptions[optname]
except KeyError:
log.config.exception("No description for {}.{}!".format(
sectname, optname))
continue
for descline in desc.splitlines():
lines += wrapper.wrap(descline)
valid_values = option.typ.get_valid_values()
if valid_values is not None:
if valid_values.descriptions:
for val in valid_values:
desc = valid_values.descriptions[val]
lines += wrapper.wrap(" {}: {}".format(val, desc))
else:
lines += wrapper.wrap("Valid values: {}".format(', '.join(
valid_values)))
lines += wrapper.wrap("Default: {}".format(
option.values['default']))
return lines
def _str_items(self, sect):
def _str_items(self, sectname, sect):
"""Get the option items as string for sect."""
lines = []
for optname, option in sect.items():
@ -535,9 +500,43 @@ class ConfigManager(QObject):
# configparser can't handle = in keys :(
optname = optname.replace('=', '<eq>')
keyval = '{} = {}'.format(optname, value)
lines += self._str_option_desc(sectname, sect, optname, option)
lines.append(keyval)
return lines
def _str_option_desc(self, sectname, sect, optname, option):
"""Get the option description strings for a single option."""
wrapper = textwrapper.TextWrapper(initial_indent='#' + ' ' * 5,
subsequent_indent='#' + ' ' * 5)
lines = []
if not getattr(sect, 'descriptions', None):
return lines
lines.append('')
typestr = ' ({})'.format(option.typ.get_name())
lines.append("# {}{}:".format(optname, typestr))
try:
desc = self.sections[sectname].descriptions[optname]
except KeyError:
log.config.exception("No description for {}.{}!".format(
sectname, optname))
return []
for descline in desc.splitlines():
lines += wrapper.wrap(descline)
valid_values = option.typ.get_valid_values()
if valid_values is not None:
if valid_values.descriptions:
for val in valid_values:
desc = valid_values.descriptions[val]
lines += wrapper.wrap(" {}: {}".format(val, desc))
else:
lines += wrapper.wrap("Valid values: {}".format(', '.join(
valid_values)))
lines += wrapper.wrap("Default: {}".format(
option.values['default']))
return lines
def _get_real_sectname(self, cp, sectname):
"""Get an old or new section name based on a configparser.
@ -806,7 +805,7 @@ class ConfigManager(QObject):
if section_ is None and option is None:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
tabbed_browser.openurl(QUrl('qute:settings'), newtab=False)
tabbed_browser.openurl(QUrl('qute://settings'), newtab=False)
return
if option.endswith('?') and option != '?':

View File

@ -292,6 +292,12 @@ def data(readonly=False):
)),
('ui', sect.KeyValue(
('history-session-interval',
SettingValue(typ.Int(), '30'),
"The maximum time in minutes between two history items for them "
"to be considered being from the same session. Use -1 to "
"disable separation."),
('zoom-levels',
SettingValue(typ.List(typ.Perc(minval=0)),
'25%,33%,50%,67%,75%,90%,100%,110%,125%,150%,175%,'
@ -400,6 +406,10 @@ def data(readonly=False):
"Globs are supported, so ';*' will blacklist all keychains"
"starting with ';'. Use '*' to disable keyhints"),
('keyhint-delay',
SettingValue(typ.Int(minval=0), '500'),
"Time from pressing a key to seeing the keyhint dialog (ms)"),
('prompt-radius',
SettingValue(typ.Int(minval=0), '8'),
"The rounding radius for the edges of prompts."),
@ -1679,7 +1689,7 @@ KEY_DATA = collections.OrderedDict([
('home', ['<Ctrl-h>']),
('stop', ['<Ctrl-s>']),
('print', ['<Ctrl-Alt-p>']),
('open qute:settings', ['Ss']),
('open qute://settings', ['Ss']),
('follow-selected', RETURN_KEYS),
('follow-selected -t', ['<Ctrl-Return>', '<Ctrl-Enter>']),
('repeat-command', ['.']),

View File

@ -142,9 +142,14 @@ class KeyConfigParser(QObject):
def save(self):
"""Save the key config file."""
log.destroy.debug("Saving key config to {}".format(self._configfile))
with qtutils.savefile_open(self._configfile, encoding='utf-8') as f:
data = str(self)
f.write(data)
try:
with qtutils.savefile_open(self._configfile,
encoding='utf-8') as f:
data = str(self)
f.write(data)
except OSError as e:
message.error("Could not save key config: {}".format(e))
@cmdutils.register(instance='key-config', maxsplit=1, no_cmd_split=True,
no_replace_variables=True)
@ -252,6 +257,7 @@ class KeyConfigParser(QObject):
"""
# {'sectname': {'keychain1': 'command', 'keychain2': 'command'}, ...}
bindings_to_add = collections.OrderedDict()
mark_dirty = False
for sectname, sect in configdata.KEY_DATA.items():
sectname = self._normalize_sectname(sectname)
@ -261,6 +267,7 @@ class KeyConfigParser(QObject):
if not only_new or self._is_new(sectname, command, e):
assert e not in bindings_to_add[sectname]
bindings_to_add[sectname][e] = command
mark_dirty = True
for sectname, sect in bindings_to_add.items():
if not sect:
@ -271,7 +278,7 @@ class KeyConfigParser(QObject):
self._add_binding(sectname, keychain, command)
self.changed.emit(sectname)
if bindings_to_add:
if mark_dirty:
self._mark_config_dirty()
def _is_new(self, sectname, command, keychain):
@ -315,7 +322,7 @@ class KeyConfigParser(QObject):
else:
line = line.strip()
self._read_command(line)
except KeyConfigError as e:
except (KeyConfigError, cmdexc.CommandError) as e:
if relaxed:
continue
else:

View File

@ -16,43 +16,62 @@ td.time {
white-space: nowrap;
}
table {
margin-bottom: 30px;
}
.date {
color: #888;
font-size: 14pt;
padding-left: 25px;
}
.pagination-link {
display: inline-block;
margin-bottom: 10px;
margin-top: 10px;
padding-right: 10px;
}
.pagination-link > a {
color: #333;
color: #555;
font-size: 12pt;
padding-bottom: 15px;
font-weight: bold;
text-align: left;
}
{% endblock %}
#load {
color: #555;
font-weight: bold;
text-decoration: none;
}
#eof {
color: #aaa;
margin-bottom: 30px;
text-align: center;
width: 100%;
}
.session-separator {
color: #aaa;
height: 40px;
text-align: center;
}
{% endblock %}
{% block content %}
<h1>Browsing history</h1>
<div id="hist-container"></div>
<span id="eof" style="display: none">end</span>
<a href="#" id="load" style="display: none">Show more</a>
<script type="text/javascript" src="qute://javascript/history.js"></script>
<script type="text/javascript">
window.SESSION_INTERVAL = {{session_interval}} * 60 * 1000;
<h1>Browsing history <span class="date">{{curr_date.strftime("%a, %d %B %Y")}}</span></h1>
window.onload = function() {
var loadLink = document.getElementById('load');
loadLink.style.display = null;
loadLink.addEventListener('click', function(ev) {
ev.preventDefault();
window.loadHistory();
});
<table>
<tbody>
{% for url, title, time in history %}
<tr>
<td class="title"><a href="{{url}}">{{title}}</a></td>
<td class="time">{{time}}</td>
</tr>
{% endfor %}
</tbody>
</table>
<span class="pagination-link"><a href="qute://history/?date={{prev_date.strftime("%Y-%m-%d")}}" rel="prev">Previous</a></span>
{% if today >= next_date %}
<span class="pagination-link"><a href="qute://history/?date={{next_date.strftime("%Y-%m-%d")}}" rel="next">Next</a></span>
{% endif %}
window.onscroll = function(ev) {
if ((window.innerHeight + window.scrollY) >= document.body.scrollHeight) {
window.loadHistory();
}
};
window.loadHistory();
};
</script>
{% endblock %}

View File

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

View File

@ -0,0 +1,190 @@
/**
* Copyright 2017 Imran Sobir
*
* 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/>.
*/
"use strict";
window.loadHistory = (function() {
// Date of last seen item.
var lastItemDate = null;
// The time to load next.
var nextTime = null;
// The URL to fetch data from.
var DATA_URL = "qute://history/data";
// Various fixed elements
var EOF_MESSAGE = document.getElementById("eof");
var LOAD_LINK = document.getElementById("load");
var HIST_CONTAINER = document.getElementById("hist-container");
/**
* Finds or creates the session table>tbody to which item with given date
* should be added.
*
* @param {Date} date - the date of the item being added.
* @returns {Element} the element to which new rows should be added.
*/
function getSessionNode(date) {
// Find/create table
var tableId = ["hist", date.getDate(), date.getMonth(),
date.getYear()].join("-");
var table = document.getElementById(tableId);
if (table === null) {
table = document.createElement("table");
table.id = tableId;
// Caption contains human-readable date
var caption = document.createElement("caption");
caption.className = "date";
var options = {
"weekday": "long",
"year": "numeric",
"month": "long",
"day": "numeric",
};
caption.innerHTML = date.toLocaleDateString("en-US", options);
table.appendChild(caption);
// Add table to page
HIST_CONTAINER.appendChild(table);
}
// Find/create tbody
var tbody = table.lastChild;
if (tbody.tagName !== "TBODY") {
tbody = document.createElement("tbody");
table.appendChild(tbody);
}
// Create session-separator and new tbody if necessary
if (tbody.lastChild !== null && lastItemDate !== null &&
window.SESSION_INTERVAL > 0) {
var interval = lastItemDate.getTime() - date.getTime();
if (interval > window.SESSION_INTERVAL) {
// Add session-separator
var sessionSeparator = document.createElement("td");
sessionSeparator.className = "session-separator";
sessionSeparator.colSpan = 2;
sessionSeparator.innerHTML = "&#167;";
table.appendChild(document.createElement("tr"));
table.lastChild.appendChild(sessionSeparator);
// Create new tbody
tbody = document.createElement("tbody");
table.appendChild(tbody);
}
}
return tbody;
}
/**
* Given a history item, create and return <tr> for it.
*
* @param {string} itemUrl - The url for this item.
* @param {string} itemTitle - The title for this item.
* @param {string} itemTime - The formatted time for this item.
* @returns {Element} the completed tr.
*/
function makeHistoryRow(itemUrl, itemTitle, itemTime) {
var row = document.createElement("tr");
var title = document.createElement("td");
title.className = "title";
var link = document.createElement("a");
link.href = itemUrl;
link.innerHTML = itemTitle;
title.appendChild(link);
var time = document.createElement("td");
time.className = "time";
time.innerHTML = itemTime;
row.appendChild(title);
row.appendChild(time);
return row;
}
/**
* Get JSON from given URL.
*
* @param {string} url - the url to fetch data from.
* @param {function} callback - the function to callback with data.
* @returns {void}
*/
function getJSON(url, callback) {
var xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.responseType = "json";
xhr.onload = function() {
var status = xhr.status;
callback(status, xhr.response);
};
xhr.send();
}
/**
* Receive history data from qute://history/data.
*
* @param {Number} status - The status of the query.
* @param {Array} history - History data.
* @returns {void}
*/
function receiveHistory(status, history) {
if (history === null) {
return;
}
for (var i = 0, len = history.length - 1; i < len; i++) {
var item = history[i];
var currentItemDate = new Date(item.time);
getSessionNode(currentItemDate).appendChild(makeHistoryRow(
item.url, item.title, currentItemDate.toLocaleTimeString()
));
lastItemDate = currentItemDate;
}
var next = history[history.length - 1].next;
if (next === -1) {
// Reached end of history
window.onscroll = null;
EOF_MESSAGE.style.display = "block";
LOAD_LINK.style.display = "none";
} else {
nextTime = next;
}
}
/**
* Load new history.
* @return {void}
*/
function loadHistory() {
if (nextTime === null) {
getJSON(DATA_URL, receiveHistory);
} else {
var url = DATA_URL.concat("?start_time=", nextTime.toString());
getJSON(url, receiveHistory);
}
}
return loadHistory;
})();

View File

@ -184,8 +184,6 @@ class MainWindow(QWidget):
self._keyhint = keyhintwidget.KeyHintView(self.win_id, self)
self._add_overlay(self._keyhint, self._keyhint.update_geometry)
self._messageview = messageview.MessageView(parent=self)
self._add_overlay(self._messageview, self._messageview.update_geometry)
self._prompt_container = prompt.PromptContainer(self.win_id, self)
self._add_overlay(self._prompt_container,
@ -195,6 +193,9 @@ class MainWindow(QWidget):
scope='window', window=self.win_id)
self._prompt_container.hide()
self._messageview = messageview.MessageView(parent=self)
self._add_overlay(self._messageview, self._messageview.update_geometry)
if geometry is not None:
self._load_geometry(geometry)
elif self.win_id == 0:

View File

@ -130,3 +130,8 @@ class MessageView(QWidget):
self._last_text = text
self.show()
self.update_geometry.emit()
def mousePressEvent(self, e):
"""Clear messages when they are clicked on."""
if e.button() in [Qt.LeftButton, Qt.MiddleButton, Qt.RightButton]:
self.clear_messages()

View File

@ -437,13 +437,13 @@ class LineEdit(QLineEdit):
"""Override keyPressEvent to paste primary selection on Shift + Ins."""
if e.key() == Qt.Key_Insert and e.modifiers() == Qt.ShiftModifier:
try:
text = utils.get_clipboard(selection=True)
text = utils.get_clipboard(selection=True, fallback=True)
except utils.ClipboardError: # pragma: no cover
pass
e.ignore()
else:
e.accept()
self.insert(text)
return
return
super().keyPressEvent(e)
def __repr__(self):
@ -644,6 +644,10 @@ class FilenamePrompt(_BasePrompt):
def accept(self, value=None):
text = value if value is not None else self._lineedit.text()
text = downloads.transform_path(text)
if text is None:
message.error("Invalid filename")
return False
self.question.answer = text
return True
@ -694,9 +698,11 @@ class DownloadFilenamePrompt(FilenamePrompt):
self._file_model.setFilter(QDir.AllDirs | QDir.Drives | QDir.NoDot)
def accept(self, value=None):
text = value if value is not None else self._lineedit.text()
self.question.answer = downloads.FileDownloadTarget(text)
return True
done = super().accept(value)
answer = self.question.answer
if answer is not None:
self.question.answer = downloads.FileDownloadTarget(answer)
return done
def download_open(self, cmdline):
self.question.answer = downloads.OpenFileDownloadTarget(cmdline)

View File

@ -146,10 +146,14 @@ class TabbedBrowser(tabwidget.TabWidget):
We don't implement this as generator so we can delete tabs while
iterating over the list.
"""
w = []
widgets = []
for i in range(self.count()):
w.append(self.widget(i))
return w
widget = self.widget(i)
if widget is None:
log.webview.debug("Got None-widget in tabbedbrowser!")
else:
widgets.append(widget)
return widgets
@config.change_filter('ui', 'window-title-format')
def update_window_title(self):

View File

@ -118,6 +118,9 @@ class TabWidget(QTabWidget):
def get_tab_fields(self, idx):
"""Get the tab field data."""
tab = self.widget(idx)
if tab is None:
log.misc.debug("Got None-tab in get_tab_fields!")
page_title = self.page_title(idx)
fields = {}

View File

@ -171,14 +171,14 @@ class CrashHandler(QObject):
"""
try:
pages = self._recover_pages(forgiving=True)
except Exception:
log.destroy.exception("Error while recovering pages")
except Exception as e:
log.destroy.exception("Error while recovering pages: {}".format(e))
pages = []
try:
cmd_history = objreg.get('command-history')[-5:]
except Exception:
log.destroy.exception("Error while getting history: {}")
except Exception as e:
log.destroy.exception("Error while getting history: {}".format(e))
cmd_history = []
try:

View File

@ -27,7 +27,7 @@ import getpass
import binascii
import hashlib
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, Qt, QTimer
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, Qt
from PyQt5.QtNetwork import QLocalSocket, QLocalServer, QAbstractSocket
import qutebrowser
@ -182,6 +182,7 @@ class IPCServer(QObject):
self._server.newConnection.connect(self.handle_connection)
self._socket = None
self._old_socket = None
self._socketopts_ok = os.name == 'nt'
if self._socketopts_ok: # pragma: no cover
# If we use setSocketOptions on Unix with Qt < 5.4, we get a
@ -278,15 +279,8 @@ class IPCServer(QObject):
log.ipc.debug("Client disconnected from socket 0x{:x}.".format(
id(self._socket)))
self._timer.stop()
if self._socket is None:
log.ipc.debug("In on_disconnected with None socket!")
else:
# For some reason Qt can still get delayed canReadNotifications
# internally, so if we call deleteLater() right away and then call
# QApplication::processEvents() somewhere in the code, we can get a
# segfault.
QTimer.singleShot(500, self._socket.deleteLater)
self._socket = None
self._old_socket = self._socket
self._socket = None
# Maybe another connection is waiting.
self.handle_connection()
@ -349,17 +343,23 @@ class IPCServer(QObject):
@pyqtSlot()
def on_ready_read(self):
"""Read json data from the client."""
if self._socket is None:
if self._socket is None: # pragma: no cover
# This happens when doing a connection while another one is already
# active for some reason.
log.ipc.warning("In on_ready_read with None socket!")
return
if self._old_socket is None:
log.ipc.warning("In on_ready_read with None socket and "
"old_socket!")
return
log.ipc.debug("In on_ready_read with None socket!")
socket = self._old_socket
else:
socket = self._socket
self._timer.stop()
while self._socket is not None and self._socket.canReadLine():
data = bytes(self._socket.readLine())
while socket is not None and socket.canReadLine():
data = bytes(socket.readLine())
self.got_raw.emit(data)
log.ipc.debug("Read from socket 0x{:x}: {!r}".format(
id(self._socket), data))
id(socket), data))
self._handle_data(data)
self._timer.start()

View File

@ -67,7 +67,6 @@ class KeyHintView(QLabel):
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum)
self.hide()
self._show_timer = usertypes.Timer(self, 'keyhint_show')
self._show_timer.setInterval(500)
self._show_timer.timeout.connect(self.show)
style.set_register_stylesheet(self)
@ -108,6 +107,7 @@ class KeyHintView(QLabel):
return
# delay so a quickly typed keychain doesn't display hints
self._show_timer.setInterval(config.get('ui', 'keyhint-delay'))
self._show_timer.start()
suffix_color = html.escape(config.get('colors', 'keyhint.fg.suffix'))

View File

@ -46,13 +46,13 @@ class MinimalLineEditMixin:
"""Override keyPressEvent to paste primary selection on Shift + Ins."""
if e.key() == Qt.Key_Insert and e.modifiers() == Qt.ShiftModifier:
try:
text = utils.get_clipboard(selection=True)
text = utils.get_clipboard(selection=True, fallback=True)
except utils.ClipboardError:
pass
e.ignore()
else:
e.accept()
self.insert(text)
return
return
super().keyPressEvent(e)
def __repr__(self):

View File

@ -228,7 +228,7 @@ def debug_pyeval(s, quiet=False):
else:
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window='last-focused')
tabbed_browser.openurl(QUrl('qute:pyeval'), newtab=True)
tabbed_browser.openurl(QUrl('qute://pyeval'), newtab=True)
@cmdutils.register(debug=True)
@ -293,15 +293,24 @@ def debug_log_filter(filters: str):
"""Change the log filter for console logging.
Args:
filters: A comma separated list of logger names.
filters: A comma separated list of logger names. Can also be "none" to
clear any existing filters.
"""
if set(filters.split(',')).issubset(log.LOGGER_NAMES):
log.console_filter.names = filters.split(',')
else:
if log.console_filter is None:
raise cmdexc.CommandError("No log.console_filter. Not attached "
"to a console?")
if filters.strip().lower() == 'none':
log.console_filter.names = None
return
if not set(filters.split(',')).issubset(log.LOGGER_NAMES):
raise cmdexc.CommandError("filters: Invalid value {} - expected one "
"of: {}".format(filters,
', '.join(log.LOGGER_NAMES)))
log.console_filter.names = filters.split(',')
@cmdutils.register()
@cmdutils.argument('current_win_id', win_id=True)

View File

@ -182,9 +182,10 @@ def init_log(args):
root = logging.getLogger()
global console_filter
if console is not None:
console_filter = LogFilter(None)
if args.logfilter is not None:
console_filter = LogFilter(args.logfilter.split(','))
console.addFilter(console_filter)
console_filter.names = args.logfilter.split(',')
console.addFilter(console_filter)
root.addHandler(console)
if ram is not None:
root.addHandler(ram)

View File

@ -174,12 +174,6 @@ def ensure_valid(obj):
raise QtValueError(obj)
def ensure_not_null(obj):
"""Ensure a Qt object with an .isNull() method is not null."""
if obj.isNull():
raise QtValueError(obj, null=True)
def check_qdatastream(stream):
"""Check the status of a QDataStream and raise OSError if it's not ok."""
status_to_str = {
@ -412,15 +406,12 @@ class QtValueError(ValueError):
"""Exception which gets raised by ensure_valid."""
def __init__(self, obj, null=False):
def __init__(self, obj):
try:
self.reason = obj.errorString()
except AttributeError:
self.reason = None
if null:
err = "{} is null".format(obj)
else:
err = "{} is not valid".format(obj)
err = "{} is not valid".format(obj)
if self.reason:
err += ": {}".format(self.reason)
super().__init__(err)

View File

@ -20,6 +20,7 @@
"""Other utilities which don't fit anywhere else."""
import io
import re
import sys
import enum
import json
@ -53,6 +54,10 @@ class SelectionUnsupportedError(ClipboardError):
"""Raised if [gs]et_clipboard is used and selection=True is unsupported."""
def __init__(self):
super().__init__("Primary selection is not supported on this "
"platform!")
class ClipboardEmptyError(ClipboardError):
@ -762,11 +767,23 @@ def set_clipboard(data, selection=False):
QApplication.clipboard().setText(data, mode=mode)
def get_clipboard(selection=False):
"""Get data from the clipboard."""
def get_clipboard(selection=False, fallback=False):
"""Get data from the clipboard.
Args:
selection: Use the primary selection.
fallback: Fall back to the clipboard if primary selection is
unavailable.
"""
global fake_clipboard
if fallback and not selection:
raise ValueError("fallback given without selection!")
if selection and not supports_selection():
raise SelectionUnsupportedError
if fallback:
selection = False
else:
raise SelectionUnsupportedError
if fake_clipboard is not None:
data = fake_clipboard
@ -845,3 +862,21 @@ def open_file(filename, cmdline=None):
def unused(_arg):
"""Function which does nothing to avoid pylint complaining."""
pass
def expand_windows_drive(path):
r"""Expand a drive-path like E: into E:\.
Does nothing for other paths.
Args:
path: The path to expand.
"""
# Usually, "E:" on Windows refers to the current working directory on drive
# E:\. The correct way to specifify drive E: is "E:\", but most users
# probably don't use the "multiple working directories" feature and expect
# "E:" and "E:\" to be equal.
if re.match(r'[A-Z]:$', path, re.IGNORECASE):
return path + "\\"
else:
return path

View File

@ -2,7 +2,7 @@
colorama==0.3.7
cssutils==1.0.2
Jinja2==2.9.5
Jinja2==2.9.6
MarkupSafe==1.0
Pygments==2.2.0
pyPEG2==2.15.2

View File

@ -28,6 +28,7 @@ CI machines.
from __future__ import print_function
import os
import time
import subprocess
import urllib
@ -44,23 +45,6 @@ def pip_install(pkg):
pkg])
print("Getting PyQt5...")
qt_version = '5.5.1'
pyqt_version = '5.5.1'
pyqt_url = ('https://www.qutebrowser.org/pyqt/'
'PyQt5-{}-gpl-Py3.4-Qt{}-x32.exe'.format(
pyqt_version, qt_version))
try:
urllib.urlretrieve(pyqt_url, r'C:\install-PyQt5.exe')
except (OSError, IOError):
print("Downloading PyQt failed, trying again in 10 seconds...")
time.sleep(10)
urllib.urlretrieve(pyqt_url, r'C:\install-PyQt5.exe')
print("Installing PyQt5...")
subprocess.check_call([r'C:\install-PyQt5.exe', '/S'])
print("Installing tox")
pip_install('pip')
pip_install(r'-rmisc\requirements\requirements-tox.txt')
@ -69,4 +53,23 @@ print("Linking Python...")
with open(r'C:\Windows\system32\python3.bat', 'w') as f:
f.write(r'@C:\Python34\python %*')
check_setup(r'C:\Python34\python')
if '-pyqt' not in os.environ['TESTENV']:
print("Getting PyQt5...")
qt_version = '5.5.1'
pyqt_version = '5.5.1'
pyqt_url = ('https://www.qutebrowser.org/pyqt/'
'PyQt5-{}-gpl-Py3.4-Qt{}-x32.exe'.format(
pyqt_version, qt_version))
try:
urllib.urlretrieve(pyqt_url, r'C:\install-PyQt5.exe')
except (OSError, IOError):
print("Downloading PyQt failed, trying again in 10 seconds...")
time.sleep(10)
urllib.urlretrieve(pyqt_url, r'C:\install-PyQt5.exe')
print("Installing PyQt5...")
subprocess.check_call([r'C:\install-PyQt5.exe', '/S'])
check_setup(r'C:\Python34\python')

View File

@ -1,57 +0,0 @@
#!/usr/bin/env python3
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016 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/>.
"""Wrapper around pytest to ignore segfaults on exit."""
import os
import sys
import subprocess
import signal
if __name__ == '__main__':
script_path = os.path.abspath(os.path.dirname(__file__))
pytest_status_file = os.path.join(script_path, '..', '..', '.cache',
'pytest_status')
try:
os.remove(pytest_status_file)
except FileNotFoundError:
pass
try:
subprocess.check_call([sys.executable, '-m', 'pytest'] + sys.argv[1:])
except subprocess.CalledProcessError as exc:
is_segfault = exc.returncode in [128 + signal.SIGSEGV, -signal.SIGSEGV]
if is_segfault and os.path.exists(pytest_status_file):
print("Ignoring segfault on exit!")
with open(pytest_status_file, 'r', encoding='ascii') as f:
exit_status = int(f.read())
else:
exit_status = exc.returncode
else:
exit_status = 0
try:
os.remove(pytest_status_file)
except FileNotFoundError:
pass
sys.exit(exit_status)

View File

@ -74,7 +74,7 @@ def whitelist_generator():
yield 'PyQt5.QtWebKit.QWebPage.ErrorPageExtensionReturn().fileNames'
yield 'PyQt5.QtWidgets.QStyleOptionViewItem.backgroundColor'
## qute:... handlers
## qute://... handlers
for name in qutescheme._HANDLERS: # pylint: disable=protected-access
yield 'qutebrowser.browser.qutescheme.qute_' + name

View File

@ -46,7 +46,7 @@ def parse_args():
if QWebEngineView is not None:
parser.add_argument('--webengine', help='Use QtWebEngine',
default=False, action='store_true')
return parser.parse_args()
return parser.parse_known_args()[0]
if __name__ == '__main__':

View File

@ -44,7 +44,7 @@ hypothesis.settings.register_profile('default',
hypothesis.settings.load_profile('default')
def _apply_platform_markers(item):
def _apply_platform_markers(config, item):
"""Apply a skip marker to a given item."""
markers = [
('posix', os.name != 'posix', "Requires a POSIX os"),
@ -57,6 +57,8 @@ def _apply_platform_markers(item):
('frozen', not getattr(sys, 'frozen', False),
"Can only run when frozen"),
('ci', 'CI' not in os.environ, "Only runs on CI."),
('issue2478', os.name == 'nt' and config.webengine,
"Broken with QtWebEngine on Windows"),
]
for searched_marker, condition, default_reason in markers:
@ -117,7 +119,7 @@ def pytest_collection_modifyitems(config, items):
if module_root_dir == 'end2end':
item.add_marker(pytest.mark.end2end)
_apply_platform_markers(item)
_apply_platform_markers(config, item)
if item.get_marker('xfail_norun'):
item.add_marker(pytest.mark.xfail(run=False))
if item.get_marker('js_prompt'):
@ -192,21 +194,3 @@ def pytest_runtest_makereport(item, call):
outcome = yield
rep = outcome.get_result()
setattr(item, "rep_" + rep.when, rep)
@pytest.hookimpl(hookwrapper=True)
def pytest_sessionfinish(exitstatus):
"""Create a file to tell run_pytest.py how pytest exited."""
outcome = yield
outcome.get_result()
cache_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)),
'..', '.cache')
try:
os.mkdir(cache_dir)
except FileExistsError:
pass
status_file = os.path.join(cache_dir, 'pytest_status')
with open(status_file, 'w', encoding='ascii') as f:
f.write(str(exitstatus))

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Retrying failing download with QtWebEngine</title>
</head>
<body>
<a href="/does-not-exist" download id="download">download</a>
</body>
</html>

View File

@ -5,8 +5,8 @@
<title>Test title</title>
<script type="text/javascript">
window.onload = function () {
console.log("Calling history.replaceState");
history.replaceState({}, '', window.location + '?state=2');
console.log("Called history.replaceState");
}
</script>
</head>

View File

@ -1,3 +1,5 @@
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
Feature: Ad blocking
Scenario: Simple adblock update

View File

@ -1,3 +1,5 @@
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
Feature: Going back and forward.
Testing the :back/:forward commands.

View File

@ -1,3 +1,5 @@
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
Feature: Caret mode
In caret mode, the user can select and yank text using the keyboard.

View File

@ -1,3 +1,5 @@
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
Feature: Using completion
Scenario: No warnings when completing with one entry (#1600)

View File

@ -161,6 +161,7 @@ def clean_open_tabs(quteproc):
quteproc.send_cmd(':window-only')
quteproc.send_cmd(':tab-only')
quteproc.send_cmd(':tab-close')
quteproc.wait_for_load_finished_url('about:blank')
@bdd.given('pdfjs is available')

View File

@ -1,3 +1,5 @@
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
Feature: Downloading things from a website.
Background:
@ -78,6 +80,7 @@ Feature: Downloading things from a website.
Scenario: Downloading with SSL errors (issue 1413)
When I clear SSL errors
And I set network -> ssl-strict to ask
And I set storage -> prompt-download-directory to false
And I download an SSL page
And I wait for "Entering mode KeyMode.* (reason: question asked)" in the log
And I run :prompt-accept
@ -126,6 +129,34 @@ Feature: Downloading things from a website.
Then the downloaded file ../foo should not exist
And the downloaded file foo should exist
@windows
Scenario: Downloading a file to a reserved path
When I set storage -> prompt-download-directory to true
And I open data/downloads/download.bin without waiting
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.download: 5> text='Please enter a location for <b>http://localhost:*/data/downloads/download.bin</b>' title='Save file to:'>, *" in the log
And I run :prompt-accept COM1
And I run :leave-mode
Then the error "Invalid filename" should be shown
@windows
Scenario: Downloading a file to a drive-relative working directory
When I set storage -> prompt-download-directory to true
And I open data/downloads/download.bin without waiting
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.download: 5> text='Please enter a location for <b>http://localhost:*/data/downloads/download.bin</b>' title='Save file to:'>, *" in the log
And I run :prompt-accept C:foobar
And I run :leave-mode
Then the error "Invalid filename" should be shown
@windows
Scenario: Downloading a file to a reserved path with :download
When I run :download data/downloads/download.bin --dest=COM1
Then the error "Invalid target filename" should be shown
@windows
Scenario: Download a file to a drive-relative working directory with :download
When I run :download data/downloads/download.bin --dest=C:foobar
Then the error "Invalid target filename" should be shown
## :download-retry
Scenario: Retrying a failed download
@ -137,6 +168,14 @@ Feature: Downloading things from a website.
does-not-exist
does-not-exist
@qtwebkit_skip
Scenario: Retrying a failed download with QtWebEngine
When I open data/downloads/issue2298.html
And I run :click-element id download
And I wait for "Download error: *" in the log
And I run :download-retry
Then the error "Retrying downloads is unsupported with QtWebEngine" should be shown
Scenario: Retrying with count
When I run :download http://localhost:(port)/data/downloads/download.bin
And I run :download http://localhost:(port)/does-not-exist
@ -603,3 +642,9 @@ Feature: Downloading things from a website.
And I run :follow-hint 0
And I wait until the download is finished
Then the downloaded file user-agent should contain Safari/
@qtwebengine_skip: Handled by QtWebEngine, not by us
Scenario: Downloading a "Internal server error" with disposition: inline (#2304)
When I set storage -> prompt-download-directory to false
And I open custom/500-inline
Then the error "Download error: *INTERNAL SERVER ERROR" should be shown

View File

@ -1,3 +1,5 @@
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
Feature: Opening external editors
## :edit-url

View File

@ -1,3 +1,5 @@
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
Feature: Using hints
# https://bugreports.qt.io/browse/QTBUG-58381
@ -45,17 +47,17 @@ Feature: Using hints
Scenario: Using :hint spawn with flags and -- (issue 797)
When I open data/hints/html/simple.html
And I hint with args "-- all spawn -v echo" and follow a
And I hint with args "-- all spawn -v python -c ''" and follow a
Then the message "Command exited successfully." should be shown
Scenario: Using :hint spawn with flags (issue 797)
When I open data/hints/html/simple.html
And I hint with args "all spawn -v echo" and follow a
And I hint with args "all spawn -v python -c ''" and follow a
Then the message "Command exited successfully." should be shown
Scenario: Using :hint spawn with flags and --rapid (issue 797)
When I open data/hints/html/simple.html
And I hint with args "--rapid all spawn -v echo" and follow a
And I hint with args "--rapid all spawn -v python -c ''" and follow a
Then the message "Command exited successfully." should be shown
@posix

View File

@ -1,3 +1,5 @@
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
Feature: Page history
Make sure the global page history is saved correctly.
@ -77,7 +79,15 @@ Feature: Page history
Scenario: Listing history
When I open data/numbers/3.txt
And I open data/numbers/4.txt
And I open qute:history
And I open qute://history
Then the page should contain the plaintext "3.txt"
Then the page should contain the plaintext "4.txt"
Scenario: Listing history with qute:history redirect
When I open data/numbers/3.txt
And I open data/numbers/4.txt
And I open qute:history without waiting
And I wait until qute://history is loaded
Then the page should contain the plaintext "3.txt"
Then the page should contain the plaintext "4.txt"

View File

@ -1,3 +1,5 @@
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
Feature: Invoking a new process
Simulate what happens when running qutebrowser with an existing instance

View File

@ -1,3 +1,5 @@
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
Feature: Javascript stuff
Integration with javascript.

View File

@ -1,3 +1,5 @@
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
Feature: Keyboard input
Tests for :bind and :unbind, :clear-keychain and other keyboard input

View File

@ -1,3 +1,5 @@
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
Feature: Setting positional marks
Background:

View File

@ -1,3 +1,5 @@
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
Feature: Various utility commands.
## :set-cmd-text
@ -79,6 +81,7 @@ Feature: Various utility commands.
And I run :jseval --world=1 console.log("Hello from JS!");
And I wait for the javascript message "Hello from JS!"
Then "Ignoring world ID 1" should be logged
And "No output or error" should be logged
@qtwebkit_skip
Scenario: :jseval uses separate world without --world
@ -87,6 +90,7 @@ Feature: Various utility commands.
And I run :jseval do_log()
Then the javascript message "Hello from the page!" should not be logged
And the javascript message "Uncaught ReferenceError: do_log is not defined" should be logged
And "No output or error" should be logged
@qtwebkit_skip
Scenario: :jseval using the main world
@ -94,6 +98,7 @@ Feature: Various utility commands.
And I open data/misc/jseval.html
And I run :jseval --world 0 do_log()
Then the javascript message "Hello from the page!" should be logged
And "No output or error" should be logged
@qtwebkit_skip
Scenario: :jseval using the main world as name
@ -101,12 +106,14 @@ Feature: Various utility commands.
And I open data/misc/jseval.html
And I run :jseval --world main do_log()
Then the javascript message "Hello from the page!" should be logged
And "No output or error" should be logged
Scenario: :jseval --file using a file that exists as js-code
When I set general -> log-javascript-console to info
And I run :jseval --file (testdata)/misc/jseval_file.js
Then the javascript message "Hello from JS!" should be logged
And the javascript message "Hello again from JS!" should be logged
And "No output or error" should be logged
Scenario: :jseval --file using a file that doesn't exist as js-code
When I run :jseval --file nonexistentfile
@ -398,12 +405,12 @@ Feature: Various utility commands.
# :pyeval
Scenario: Running :pyeval
When I run :debug-pyeval 1+1
And I wait until qute:pyeval is loaded
And I wait until qute://pyeval is loaded
Then the page should contain the plaintext "2"
Scenario: Causing exception in :pyeval
When I run :debug-pyeval 1/0
And I wait until qute:pyeval is loaded
And I wait until qute://pyeval is loaded
Then the page should contain the plaintext "ZeroDivisionError"
Scenario: Running :pyeval with --quiet
@ -505,12 +512,12 @@ Feature: Various utility commands.
When I run :messages cataclysmic
Then the error "Invalid log level cataclysmic!" should be shown
Scenario: Using qute:log directly
When I open qute:log
Scenario: Using qute://log directly
When I open qute://log
Then no crash should happen
Scenario: Using qute:plainlog directly
When I open qute:plainlog
Scenario: Using qute://plainlog directly
When I open qute://plainlog
Then no crash should happen
Scenario: Using :messages without messages
@ -531,6 +538,16 @@ Feature: Various utility commands.
When I run :message-i "Hello World" (invalid command)
Then the error "message-i: no such command" should be shown
Scenario: Multiple leading : in command
When I run :::::set-cmd-text ::::message-i "Hello World"
And I run :command-accept
Then the message "Hello World" should be shown
Scenario: Whitespace in command
When I run : : set-cmd-text : : message-i "Hello World"
And I run :command-accept
Then the message "Hello World" should be shown
# We can't run :message-i as startup command, so we use
# :set-cmd-text
@ -625,7 +642,7 @@ Feature: Various utility commands.
And I run :command-history-prev
And I run :command-accept
Then the message "blah" should be shown
Scenario: Browsing through commands
When I run :set-cmd-text :message-info blarg
And I run :command-accept
@ -637,7 +654,7 @@ Feature: Various utility commands.
And I run :command-history-next
And I run :command-accept
Then the message "blarg" should be shown
Scenario: Calling previous command when history is empty
Given I have a fresh instance
When I run :set-cmd-text :
@ -673,7 +690,8 @@ Feature: Various utility commands.
## Renderer crashes
@qtwebkit_skip @no_invalid_lines
# Skipped on Windows as "... has stopped working" hangs.
@qtwebkit_skip @no_invalid_lines @posix
Scenario: Renderer crash
When I run :open -t chrome://crash
Then the error "Renderer process crashed" should be shown

View File

@ -1,3 +1,5 @@
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
Feature: Using :navigate
Scenario: :navigate with invalid argument

View File

@ -1,3 +1,5 @@
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
Feature: Opening pages
Scenario: :open with URL

View File

@ -1,3 +1,5 @@
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
Feature: Prompts
Various prompts (javascript, SSL errors, authentification, etc.)
@ -150,12 +152,13 @@ Feature: Prompts
Scenario: Pasting via shift-insert without it being supported
When selection is not supported
And I put "insert test" into the primary selection
And I put "clipboard test" into the clipboard
And I open data/prompt/jsprompt.html
And I run :click-element id button
And I wait for a prompt
And I press the keys "<Shift-Insert>"
And I run :prompt-accept
Then the javascript message "Prompt reply: " should be logged
Then the javascript message "Prompt reply: clipboard test" should be logged
@js_prompt
Scenario: Using content -> ignore-javascript-prompt
@ -174,6 +177,7 @@ Feature: Prompts
Then the error "Certificate error: *" should be shown
And the page should contain the plaintext "Hello World via SSL!"
@issue2478
Scenario: SSL error with ssl-strict = true
When I clear SSL errors
And I set network -> ssl-strict to true
@ -189,6 +193,7 @@ Feature: Prompts
And I wait until the SSL page finished loading
Then the page should contain the plaintext "Hello World via SSL!"
@issue2478
Scenario: SSL error with ssl-strict = ask -> no
When I clear SSL errors
And I set network -> ssl-strict to ask
@ -197,6 +202,7 @@ Feature: Prompts
And I run :prompt-accept no
Then a SSL error page should be shown
@issue2478
Scenario: SSL error with ssl-strict = ask -> abort
When I clear SSL errors
And I set network -> ssl-strict to ask

View File

@ -1,3 +1,5 @@
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
Feature: Scrolling
Tests the various scroll commands.

View File

@ -1,3 +1,5 @@
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
Feature: Searching on a page
Searching text on the page (like /foo) with different options.
@ -110,6 +112,15 @@ Feature: Searching on a page
And I run :yank selection
Then the clipboard should contain "foo"
# https://github.com/qutebrowser/qutebrowser/issues/2438
Scenario: Jumping to next match after clearing
When I set general -> ignore-case to true
And I run :search foo
And I run :search
And I run :search-next
And I run :yank selection
Then the clipboard should contain "foo"
## :search-prev
Scenario: Jumping to previous match

View File

@ -1,3 +1,5 @@
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
Feature: Saving and loading sessions
Background:
@ -198,7 +200,7 @@ Feature: Saving and loading sessions
Scenario: Saving a session with a page using history.replaceState()
When I open data/sessions/history_replace_state.html without waiting
Then the javascript message "Calling history.replaceState" should be logged
Then the javascript message "Called history.replaceState" should be logged
And the session should look like:
windows:
- tabs:
@ -212,7 +214,7 @@ Feature: Saving and loading sessions
Scenario: Saving a session with a page using history.replaceState() and navigating away (qtwebkit)
When I open data/sessions/history_replace_state.html
And I open data/hello.txt
Then the javascript message "Calling history.replaceState" should be logged
Then the javascript message "Called history.replaceState" should be logged
And the session should look like:
windows:
- tabs:
@ -229,7 +231,7 @@ Feature: Saving and loading sessions
@qtwebkit_skip
Scenario: Saving a session with a page using history.replaceState() and navigating away
When I open data/sessions/history_replace_state.html without waiting
And I wait for "* Calling history.replaceState" in the log
And I wait for "* Called history.replaceState" in the log
And I open data/hello.txt
Then the session should look like:
windows:

View File

@ -1,3 +1,5 @@
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
Feature: Setting settings.
Background:
@ -76,15 +78,15 @@ Feature: Setting settings.
When I run :set -t colors statusbar.bg green
Then colors -> statusbar.bg should be green
# qute:settings isn't actually implemented on QtWebEngine, but this works
# qute://settings isn't actually implemented on QtWebEngine, but this works
# (and displays a page saying it's not available)
Scenario: Opening qute:settings
Scenario: Opening qute://settings
When I run :set
And I wait until qute:settings is loaded
And I wait until qute://settings is loaded
Then the following tabs should be open:
- qute:settings (active)
- qute://settings (active)
@qtwebengine_todo: qute:settings is not implemented yet
@qtwebengine_todo: qute://settings is not implemented yet
Scenario: Focusing input fields in qute://settings and entering valid value
When I set general -> ignore-case to false
And I open qute://settings
@ -99,7 +101,7 @@ Feature: Setting settings.
And I press the key "<Tab>"
Then general -> ignore-case should be true
@qtwebengine_todo: qute:settings is not implemented yet
@qtwebengine_todo: qute://settings is not implemented yet
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

View File

@ -1,3 +1,5 @@
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
Feature: :spawn
Scenario: Running :spawn

View File

@ -1,3 +1,5 @@
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
Feature: Tab management
Tests for various :tab-* commands.

View File

@ -68,6 +68,8 @@ def wait_for_download_finished_name(quteproc, name):
def wait_for_download_prompt(tmpdir, quteproc, path):
full_path = path.replace('(tmpdir)', str(tmpdir)).replace('/', os.sep)
quteproc.wait_for(message=PROMPT_MSG.format(full_path))
quteproc.wait_for(message="Entering mode KeyMode.prompt "
"(reason: question asked)")
@bdd.when("I download an SSL page")

View File

@ -17,5 +17,18 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
import logging
import pytest_bdd as bdd
bdd.scenarios('open.feature')
def test_open_s(quteproc, ssl_server):
"""Test :open with -s."""
quteproc.set_setting('network', 'ssl-strict', 'false')
quteproc.send_cmd(':open -s http://localhost:{}/'.format(ssl_server.port))
quteproc.mark_expected(category='message',
loglevel=logging.ERROR,
message="Certificate error: *")
quteproc.wait_for_load_finished('/', port=ssl_server.port, https=True,
load_status='warn')

View File

@ -1,3 +1,5 @@
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
Feature: quickmarks and bookmarks
## bookmarks
@ -223,12 +225,12 @@ Feature: quickmarks and bookmarks
Scenario: Listing quickmarks
When I run :quickmark-add http://localhost:(port)/data/numbers/20.txt twenty
And I run :quickmark-add http://localhost:(port)/data/numbers/21.txt twentyone
And I open qute:bookmarks
And I open qute://bookmarks
Then the page should contain the plaintext "twenty"
And the page should contain the plaintext "twentyone"
Scenario: Listing bookmarks
When I open data/title.html
When I open data/title.html in a new tab
And I run :bookmark-add
And I open qute:bookmarks
And I open qute://bookmarks
Then the page should contain the plaintext "Test title"

View File

@ -1,3 +1,5 @@
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
Feature: Miscellaneous utility commands exposed to the user.
Background:
@ -129,6 +131,7 @@ Feature: Miscellaneous utility commands exposed to the user.
And I hint with args "all tab-fg"
And I run :leave-mode
And I run :repeat-command
And I wait for "hints: *" in the log
And I run :follow-hint a
And I wait until data/hello.txt is loaded
Then the following tabs should be open:
@ -142,7 +145,7 @@ Feature: Miscellaneous utility commands exposed to the user.
And I run :message-info oldstuff
And I run :repeat 20 message-info otherstuff
And I run :message-info newstuff
And I open qute:log
And I open qute://log
Then the page should contain the plaintext "newstuff"
And the page should not contain the plaintext "oldstuff"
@ -161,3 +164,10 @@ Feature: Miscellaneous utility commands exposed to the user.
Scenario: Using debug-log-filter with invalid filter
When I run :debug-log-filter blah
Then the error "filters: Invalid value blah - expected one of: statusbar, *" should be shown
Scenario: Using debug-log-filter
When I run :debug-log-filter commands,ipc,webview
And I run :enter-mode insert
And I run :debug-log-filter none
And I run :leave-mode
Then "Entering mode KeyMode.insert *" should not be logged

View File

@ -1,3 +1,5 @@
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
Feature: Yanking and pasting.
:yank, {clipboard} and {primary} can be used to copy/paste the URL or title
from/to the clipboard and primary selection.
@ -99,6 +101,11 @@ Feature: Yanking and pasting.
And I run :open {primary} (invalid command)
Then the error "Primary selection is empty." should be shown
Scenario: Pasting without primary selection being supported
When selection is not supported
And I run :open {primary} (invalid command)
Then the error "Primary selection is not supported on this platform!" should be shown
Scenario: Pasting with a space in clipboard
When I put " " into the clipboard
And I run :open {clipboard} (invalid command)

View File

@ -1,3 +1,5 @@
# vim: ft=cucumber fileencoding=utf-8 sts=4 sw=4 et:
Feature: Zooming in and out
Background:

View File

@ -75,6 +75,10 @@ def is_ignored_lowlevel_message(message):
"not supported by protocol" in message):
# Makes tests fail on Quantumcross' machine
return True
elif 'Unable to locate theme engine in module_path:' in message:
return True
elif message == 'getrlimit(RLIMIT_NOFILE) failed':
return True
return False

View File

@ -149,11 +149,11 @@ def test_quteprocess_quitting(qtbot, quteproc_process):
@pytest.mark.parametrize('data, attrs', [
(
# Normal message
'{"created": 0, "msecs": 0, "levelname": "DEBUG", "name": "init", '
'{"created": 86400, "msecs": 0, "levelname": "DEBUG", "name": "init", '
'"module": "earlyinit", "funcName": "init_log", "lineno": 280, '
'"levelno": 10, "message": "Log initialized."}',
{
'timestamp': datetime.datetime.fromtimestamp(0),
'timestamp': datetime.datetime.fromtimestamp(86400),
'loglevel': logging.DEBUG,
'category': 'init',
'module': 'earlyinit',
@ -165,28 +165,28 @@ def test_quteprocess_quitting(qtbot, quteproc_process):
),
(
# VDEBUG
'{"created": 0, "msecs": 0, "levelname": "VDEBUG", "name": "foo", '
'{"created": 86400, "msecs": 0, "levelname": "VDEBUG", "name": "foo", '
'"module": "foo", "funcName": "foo", "lineno": 0, "levelno": 9, '
'"message": ""}',
{'loglevel': log.VDEBUG_LEVEL}
),
(
# Unknown module
'{"created": 0, "msecs": 0, "levelname": "DEBUG", "name": "qt", '
'{"created": 86400, "msecs": 0, "levelname": "DEBUG", "name": "qt", '
'"module": null, "funcName": null, "lineno": 0, "levelno": 10, '
'"message": "test"}',
{'module': None, 'function': None, 'line': None},
),
(
# Expected message
'{"created": 0, "msecs": 0, "levelname": "VDEBUG", "name": "foo", '
'{"created": 86400, "msecs": 0, "levelname": "VDEBUG", "name": "foo", '
'"module": "foo", "funcName": "foo", "lineno": 0, "levelno": 9, '
'"message": "SpellCheck: test"}',
{'expected': True},
),
(
# Weird Qt location
'{"created": 0, "msecs": 0, "levelname": "DEBUG", "name": "qt", '
'{"created": 86400, "msecs": 0, "levelname": "DEBUG", "name": "qt", '
'"module": "qnetworkreplyhttpimpl", "funcName": '
'"void QNetworkReplyHttpImplPrivate::error('
'QNetworkReply::NetworkError, const QString&)", "lineno": 1929, '
@ -200,7 +200,7 @@ def test_quteprocess_quitting(qtbot, quteproc_process):
}
),
(
'{"created": 0, "msecs": 0, "levelname": "DEBUG", "name": "qt", '
'{"created": 86400, "msecs": 0, "levelname": "DEBUG", "name": "qt", '
'"module": "qxcbxsettings", "funcName": "QXcbXSettings::QXcbXSettings('
'QXcbScreen*)", "lineno": 233, "levelno": 10, "message": '
'"QXcbXSettings::QXcbXSettings(QXcbScreen*) Failed to get selection '
@ -213,7 +213,7 @@ def test_quteprocess_quitting(qtbot, quteproc_process):
),
(
# ResourceWarning
'{"created": 0, "msecs": 0, "levelname": "WARNING", '
'{"created": 86400, "msecs": 0, "levelname": "WARNING", '
'"name": "py.warnings", "module": "app", "funcName": "qt_mainloop", '
'"lineno": 121, "levelno": 30, "message": '
'".../app.py:121: ResourceWarning: unclosed file <_io.TextIOWrapper '
@ -231,7 +231,7 @@ def test_log_line_parse(data, attrs):
@pytest.mark.parametrize('data, colorized, expect_error, expected', [
(
{'created': 0, 'msecs': 0, 'levelname': 'DEBUG', 'name': 'foo',
{'created': 86400, 'msecs': 0, 'levelname': 'DEBUG', 'name': 'foo',
'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 10,
'message': 'quux'},
False, False,
@ -239,7 +239,7 @@ def test_log_line_parse(data, attrs):
),
# Traceback attached
(
{'created': 0, 'msecs': 0, 'levelname': 'DEBUG', 'name': 'foo',
{'created': 86400, 'msecs': 0, 'levelname': 'DEBUG', 'name': 'foo',
'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 10,
'message': 'quux', 'traceback': 'Traceback (most recent call '
'last):\n here be dragons'},
@ -250,7 +250,7 @@ def test_log_line_parse(data, attrs):
),
# Colorized
(
{'created': 0, 'msecs': 0, 'levelname': 'DEBUG', 'name': 'foo',
{'created': 86400, 'msecs': 0, 'levelname': 'DEBUG', 'name': 'foo',
'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 10,
'message': 'quux'},
True, False,
@ -259,7 +259,7 @@ def test_log_line_parse(data, attrs):
),
# Expected error
(
{'created': 0, 'msecs': 0, 'levelname': 'ERROR', 'name': 'foo',
{'created': 86400, 'msecs': 0, 'levelname': 'ERROR', 'name': 'foo',
'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 40,
'message': 'quux'},
False, True,
@ -267,7 +267,7 @@ def test_log_line_parse(data, attrs):
),
# Expected other message (i.e. should make no difference)
(
{'created': 0, 'msecs': 0, 'levelname': 'DEBUG', 'name': 'foo',
{'created': 86400, 'msecs': 0, 'levelname': 'DEBUG', 'name': 'foo',
'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 10,
'message': 'quux'},
False, True,
@ -275,7 +275,7 @@ def test_log_line_parse(data, attrs):
),
# Expected error colorized (shouldn't be red)
(
{'created': 0, 'msecs': 0, 'levelname': 'ERROR', 'name': 'foo',
{'created': 86400, 'msecs': 0, 'levelname': 'ERROR', 'name': 'foo',
'module': 'bar', 'funcName': 'qux', 'lineno': 10, 'levelno': 40,
'message': 'quux'},
True, True,

View File

@ -74,6 +74,8 @@ class Request(testprocess.Line):
'/redirect-to': [http.client.FOUND],
'/cookies/set': [http.client.FOUND],
'/custom/500-inline': [http.client.INTERNAL_SERVER_ERROR],
}
for i in range(15):
path_to_statuses['/redirect/{}'.format(i)] = [http.client.FOUND]

View File

@ -122,6 +122,17 @@ def redirect_self():
return app.make_response(flask.redirect(flask.url_for('redirect_self')))
@app.route('/custom/500-inline')
def internal_error_attachment():
"""A 500 error with Content-Disposition: inline."""
response = flask.Response(b"", headers={
"Content-Type": "application/octet-stream",
"Content-Disposition": 'inline; filename="attachment.jpg"',
})
response.status_code = 500
return response
@app.after_request
def log_request(response):
"""Log a webserver request."""

View File

@ -77,8 +77,6 @@ def test_ascii_locale(request, httpbin, tmpdir, quteproc_new):
https://github.com/qutebrowser/qutebrowser/issues/908
https://github.com/qutebrowser/qutebrowser/issues/1726
"""
if request.config.webengine:
pytest.skip("Downloads are not implemented with QtWebEngine yet")
args = ['--temp-basedir'] + _base_args(request.config)
quteproc_new.start(args, env={'LC_ALL': 'C'})
quteproc_new.set_setting('storage', 'download-directory', str(tmpdir))
@ -113,9 +111,6 @@ def test_misconfigured_user_dirs(request, httpbin, temp_basedir_env,
https://github.com/qutebrowser/qutebrowser/issues/866
https://github.com/qutebrowser/qutebrowser/issues/1269
"""
if request.config.webengine:
pytest.skip("Downloads are not implemented with QtWebEngine yet")
home = tmpdir / 'home'
home.ensure(dir=True)
temp_basedir_env['HOME'] = str(home)
@ -143,12 +138,10 @@ def test_misconfigured_user_dirs(request, httpbin, temp_basedir_env,
def test_no_loglines(request, quteproc_new):
"""Test qute:log with --loglines=0."""
if request.config.webengine:
pytest.skip("qute:log is not implemented with QtWebEngine yet")
"""Test qute://log with --loglines=0."""
quteproc_new.start(args=['--temp-basedir', '--loglines=0'] +
_base_args(request.config))
quteproc_new.open_path('qute:log')
quteproc_new.open_path('qute://log')
assert quteproc_new.get_content() == 'Log output was disabled.'

View File

@ -17,8 +17,9 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
import datetime
import collections
import json
import os
import time
from PyQt5.QtCore import QUrl
import pytest
@ -27,30 +28,72 @@ from qutebrowser.browser import history, qutescheme
from qutebrowser.utils import objreg
Dates = collections.namedtuple('Dates', ['yesterday', 'today', 'tomorrow'])
class TestJavascriptHandler:
"""Test the qute://javascript endpoint."""
# Tuples of fake JS files and their content.
js_files = [
('foo.js', "var a = 'foo';"),
('bar.js', "var a = 'bar';"),
]
@pytest.fixture(autouse=True)
def patch_read_file(self, monkeypatch):
"""Patch utils.read_file to return few fake JS files."""
def _read_file(path, binary=False):
"""Faked utils.read_file."""
assert not binary
for filename, content in self.js_files:
if path == os.path.join('javascript', filename):
return content
raise OSError("File not found {}!".format(path))
monkeypatch.setattr('qutebrowser.utils.utils.read_file', _read_file)
@pytest.mark.parametrize("filename, content", js_files)
def test_qutejavascript(self, filename, content):
url = QUrl("qute://javascript/{}".format(filename))
_mimetype, data = qutescheme.qute_javascript(url)
assert data == content
def test_qutejavascript_404(self):
url = QUrl("qute://javascript/404.js")
with pytest.raises(qutescheme.QuteSchemeOSError):
qutescheme.data_for_url(url)
def test_qutejavascript_empty_query(self):
url = QUrl("qute://javascript")
with pytest.raises(qutescheme.QuteSchemeError):
qutescheme.qute_javascript(url)
class TestHistoryHandler:
"""Test the qute://history endpoint."""
@pytest.fixture
def dates(self):
one_day = datetime.timedelta(days=1)
today = datetime.datetime.today()
tomorrow = today + one_day
yesterday = today - one_day
return Dates(yesterday, today, tomorrow)
@pytest.fixture(scope="module")
def now(self):
return int(time.time())
@pytest.fixture
def entries(self, dates):
today = history.Entry(atime=str(dates.today.timestamp()),
url=QUrl('www.today.com'), title='today')
tomorrow = history.Entry(atime=str(dates.tomorrow.timestamp()),
url=QUrl('www.tomorrow.com'), title='tomorrow')
yesterday = history.Entry(atime=str(dates.yesterday.timestamp()),
url=QUrl('www.yesterday.com'), title='yesterday')
return Dates(yesterday, today, tomorrow)
def entries(self, now):
"""Create fake history entries."""
# create 12 history items spaced 6 hours apart, starting from now
entry_count = 12
interval = 6 * 60 * 60
items = []
for i in range(entry_count):
entry_atime = now - i * interval
entry = history.Entry(atime=str(entry_atime),
url=QUrl("www.x.com/" + str(i)), title="Page " + str(i))
items.insert(0, entry)
return items
@pytest.fixture
def fake_web_history(self, fake_save_manager, tmpdir):
@ -62,78 +105,61 @@ class TestHistoryHandler:
@pytest.fixture(autouse=True)
def fake_history(self, fake_web_history, entries):
"""Create fake history for three different days."""
fake_web_history._add_entry(entries.yesterday)
fake_web_history._add_entry(entries.today)
fake_web_history._add_entry(entries.tomorrow)
"""Create fake history."""
for item in entries:
fake_web_history._add_entry(item)
fake_web_history.save()
def test_history_without_query(self):
"""Ensure qute://history shows today's history without any query."""
_mimetype, data = qutescheme.qute_history(QUrl("qute://history"))
key = "<span class=\"date\">{}</span>".format(
datetime.date.today().strftime("%a, %d %B %Y"))
assert key in data
def test_history_with_bad_query(self):
"""Ensure qute://history shows today's history with bad query."""
url = QUrl("qute://history?date=204-blaah")
@pytest.mark.parametrize("start_time_offset, expected_item_count", [
(0, 4),
(24*60*60, 4),
(48*60*60, 4),
(72*60*60, 0)
])
def test_qutehistory_data(self, start_time_offset, expected_item_count,
now):
"""Ensure qute://history/data returns correct items."""
start_time = now - start_time_offset
url = QUrl("qute://history/data?start_time=" + str(start_time))
_mimetype, data = qutescheme.qute_history(url)
key = "<span class=\"date\">{}</span>".format(
datetime.date.today().strftime("%a, %d %B %Y"))
assert key in data
items = json.loads(data)
items = [item for item in items if 'time' in item] # skip 'next' item
def test_history_today(self):
"""Ensure qute://history shows history for today."""
url = QUrl("qute://history")
assert len(items) == expected_item_count
# test times
end_time = start_time - 24*60*60
for item in items:
assert item['time'] <= start_time * 1000
assert item['time'] > end_time * 1000
@pytest.mark.parametrize("start_time_offset, next_time", [
(0, 24*60*60),
(24*60*60, 48*60*60),
(48*60*60, -1),
(72*60*60, -1)
])
def test_qutehistory_next(self, start_time_offset, next_time, now):
"""Ensure qute://history/data returns correct items."""
start_time = now - start_time_offset
url = QUrl("qute://history/data?start_time=" + str(start_time))
_mimetype, data = qutescheme.qute_history(url)
assert "today" in data
assert "tomorrow" not in data
assert "yesterday" not in data
items = json.loads(data)
items = [item for item in items if 'next' in item] # 'next' items
assert len(items) == 1
def test_history_yesterday(self, dates):
"""Ensure qute://history shows history for yesterday."""
url = QUrl("qute://history?date=" +
dates.yesterday.strftime("%Y-%m-%d"))
_mimetype, data = qutescheme.qute_history(url)
assert "today" not in data
assert "tomorrow" not in data
assert "yesterday" in data
if next_time == -1:
assert items[0]["next"] == -1
else:
assert items[0]["next"] == now - next_time
def test_history_tomorrow(self, dates):
"""Ensure qute://history shows history for tomorrow."""
url = QUrl("qute://history?date=" +
dates.tomorrow.strftime("%Y-%m-%d"))
_mimetype, data = qutescheme.qute_history(url)
assert "today" not in data
assert "tomorrow" in data
assert "yesterday" not in data
def test_no_next_link_to_future(self, dates):
"""Ensure there's no next link pointing to the future."""
url = QUrl("qute://history")
_mimetype, data = qutescheme.qute_history(url)
assert "Next" not in data
url = QUrl("qute://history?date=" +
dates.tomorrow.strftime("%Y-%m-%d"))
_mimetype, data = qutescheme.qute_history(url)
assert "Next" not in data
def test_qute_history_benchmark(self, dates, entries, fake_web_history,
benchmark):
for i in range(100000):
def test_qute_history_benchmark(self, fake_web_history, benchmark, now):
for t in range(100000): # one history per second
entry = history.Entry(
atime=str(dates.yesterday.timestamp()),
url=QUrl('www.yesterday.com/{}'.format(i)),
title='yesterday')
atime=str(now - t),
url=QUrl('www.x.com/{}'.format(t)),
title='x at {}'.format(t))
fake_web_history._add_entry(entry)
fake_web_history._add_entry(entries.today)
fake_web_history._add_entry(entries.tomorrow)
url = QUrl("qute://history")
_mimetype, data = benchmark(qutescheme.qute_history, url)
assert "today" in data
assert "tomorrow" not in data
assert "yesterday" not in data
url = QUrl("qute://history/data?start_time={}".format(now))
_mimetype, _data = benchmark(qutescheme.qute_history, url)

View File

@ -91,3 +91,10 @@ def test_error_network_reply(qtbot, req):
assert reply.readData(1) == b''
assert reply.error() == QNetworkReply.UnknownNetworkError
assert reply.errorString() == "This is an error"
def test_redirect_network_reply():
url = QUrl('https://www.example.com/')
reply = networkreply.RedirectNetworkReply(url)
assert reply.readData(1) == b''
assert reply.attribute(QNetworkRequest.RedirectionTargetAttribute) == url

View File

@ -17,10 +17,17 @@
# 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
from PyQt5.QtCore import QUrl, QDateTime
from PyQt5.QtNetwork import QNetworkDiskCache, QNetworkCacheMetaData
from qutebrowser.browser.webkit import cache
from qutebrowser.utils import qtutils
pytestmark = pytest.mark.skipif(qtutils.version_check('5.7.1'),
reason="QNetworkDiskCache is broken on Qt >= "
"5.7.1")
def preload_cache(cache, url='http://www.example.com/', content=b'foobar'):

View File

@ -18,6 +18,7 @@
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
import pytest
from PyQt5.QtCore import Qt
from qutebrowser.mainwindow import messageview
from qutebrowser.utils import usertypes
@ -114,3 +115,17 @@ def test_replaced_messages(view, replace1, replace2, length):
view.show_message(usertypes.MessageLevel.info, 'test', replace=replace1)
view.show_message(usertypes.MessageLevel.info, 'test 2', replace=replace2)
assert len(view._messages) == length
@pytest.mark.parametrize('button, count', [
(Qt.LeftButton, 0),
(Qt.MiddleButton, 0),
(Qt.RightButton, 0),
(Qt.BackButton, 2),
])
def test_click_messages(qtbot, view, button, count):
"""Messages should dissappear when we click on them."""
view.show_message(usertypes.MessageLevel.info, 'test mouse click')
view.show_message(usertypes.MessageLevel.info, 'test mouse click 2')
qtbot.mousePress(view, button)
assert len(view._messages) == count

View File

@ -41,6 +41,7 @@ def proc(qtbot, caplog):
p._proc.terminate()
if not blocker.signal_triggered:
p._proc.kill()
p._proc.waitForFinished()
@pytest.fixture()

View File

@ -587,22 +587,20 @@ def test_timeout(qtbot, caplog, qlocalsocket, ipc_server):
assert caplog.records[-1].message.startswith("IPC connection timed out")
@pytest.mark.parametrize('method, args, is_warning', [
pytest.mark.posix(('on_error', [0], False)),
('on_disconnected', [], False),
('on_ready_read', [], True),
])
def test_ipcserver_socket_none(ipc_server, caplog, method, args, is_warning):
func = getattr(ipc_server, method)
def test_ipcserver_socket_none_readyread(ipc_server, caplog):
assert ipc_server._socket is None
assert ipc_server._old_socket is None
with caplog.at_level(logging.WARNING):
ipc_server.on_ready_read()
msg = "In on_ready_read with None socket and old_socket!"
assert msg in [r.message for r in caplog.records]
if is_warning:
with caplog.at_level(logging.WARNING):
func(*args)
else:
func(*args)
msg = "In {} with None socket!".format(method)
@pytest.mark.posix
def test_ipcserver_socket_none_error(ipc_server, caplog):
assert ipc_server._socket is None
ipc_server.on_error(0)
msg = "In on_error with None socket!"
assert msg in [r.message for r in caplog.records]

View File

@ -44,8 +44,8 @@ def expected_text(*args):
@pytest.fixture
def keyhint(qtbot, config_stub, key_config_stub):
"""Fixture to initialize a KeyHintView."""
def keyhint_config(config_stub):
"""Fixture providing the necessary config settings for the KeyHintView."""
config_stub.data = {
'colors': {
'keyhint.fg': 'white',
@ -55,9 +55,16 @@ def keyhint(qtbot, config_stub, key_config_stub):
'fonts': {'keyhint': 'Comic Sans'},
'ui': {
'keyhint-blacklist': '',
'keyhint-delay': 500,
'status-position': 'bottom',
},
}
return config_stub
@pytest.fixture
def keyhint(qtbot, keyhint_config, key_config_stub):
"""Fixture to initialize a KeyHintView."""
keyhint = KeyHintView(0, None)
qtbot.add_widget(keyhint)
assert keyhint.text() == ''
@ -161,3 +168,16 @@ def test_blacklist_all(keyhint, config_stub, key_config_stub):
keyhint.update_keyhint('normal', 'a')
assert not keyhint.text()
def test_delay(qtbot, stubs, monkeypatch, keyhint_config, key_config_stub):
timer = stubs.FakeTimer()
monkeypatch.setattr(
'qutebrowser.misc.keyhintwidget.usertypes.Timer',
lambda *_: timer)
interval = 200
keyhint_config.set('ui', 'keyhint-delay', interval)
key_config_stub.set_bindings_for('normal', OrderedDict([('aa', 'cmd-aa')]))
keyhint = KeyHintView(0, None)
keyhint.update_keyhint('normal', 'a')
assert timer.interval() == interval

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