This commit is contained in:
thuck 2016-11-10 00:28:31 +01:00
commit 9f70fa3ec8
36 changed files with 1249 additions and 935 deletions

View File

@ -75,4 +75,5 @@ ignored-modules=pytest
# UnsetObject because pylint infers any objreg.get(...) as UnsetObject.
ignored-classes=qutebrowser.utils.objreg.UnsetObject,
qutebrowser.browser.webkit.webelem.WebElementWrapper,
scripts.dev.check_coverage.MsgType
scripts.dev.check_coverage.MsgType,
qutebrowser.browser.downloads.UnsupportedAttribute

View File

@ -140,6 +140,8 @@ Changed
- `qute:version` and `qutebrowser --version` now show various important paths
- `:spawn`/userscripts now show a nicer error when a script wasn't found
- Various functionality now works when javascript is disabled with QtWebKit
- Various commands/settings taking `left`/`right`/`previous` arguments now take
`prev`/`next`/`last-used` to remove ambiguity.
Deprecated
~~~~~~~~~~

View File

@ -154,8 +154,8 @@ Contributors, sorted by the number of commits in descending order:
* Bruno Oliveira
* Alexander Cogneau
* Felix Van der Jeugt
* Martin Tournoij
* Daniel Karbach
* Martin Tournoij
* Kevin Velghe
* Raphael Pierzina
* Joel Torstensson

View File

@ -779,13 +779,13 @@ Duplicate the current tab.
[[tab-close]]
=== tab-close
Syntax: +:tab-close [*--left*] [*--right*] [*--opposite*]+
Syntax: +:tab-close [*--prev*] [*--next*] [*--opposite*]+
Close the current/[count]th tab.
==== optional arguments
* +*-l*+, +*--left*+: Force selecting the tab to the left of the current tab.
* +*-r*+, +*--right*+: Force selecting the tab to the right of the current tab.
* +*-p*+, +*--prev*+: Force selecting the tab before the current tab.
* +*-n*+, +*--next*+: Force selecting the tab after the current tab.
* +*-o*+, +*--opposite*+: Force selecting the tab in the opposite direction of what's configured in 'tabs->select-on-remove'.
@ -841,13 +841,13 @@ How many tabs to switch forward.
[[tab-only]]
=== tab-only
Syntax: +:tab-only [*--left*] [*--right*]+
Syntax: +:tab-only [*--prev*] [*--next*]+
Close all tabs except for the current one.
==== optional arguments
* +*-l*+, +*--left*+: Keep tabs to the left of the current.
* +*-r*+, +*--right*+: Keep tabs to the right of the current.
* +*-p*+, +*--prev*+: Keep tabs before the current.
* +*-n*+, +*--next*+: Keep tabs after the current.
[[tab-prev]]
=== tab-prev

View File

@ -1038,11 +1038,11 @@ Which tab to select when the focused tab is removed.
Valid values:
* +left+: Select the tab on the left.
* +right+: Select the tab on the right.
* +previous+: Select the previously selected tab.
* +prev+: Select the tab which came before the closed one (left in horizontal, above in vertical).
* +next+: Select the tab which came after the closed one (right in horizontal, below in vertical).
* +last-used+: Select the previously selected tab.
Default: +pass:[right]+
Default: +pass:[next]+
[[tabs-new-tab-position]]
=== new-tab-position
@ -1050,12 +1050,12 @@ How new tabs are positioned.
Valid values:
* +left+: On the left of the current tab.
* +right+: On the right of the current tab.
* +first+: At the left end.
* +last+: At the right end.
* +prev+: Before the current tab.
* +next+: After the current tab.
* +first+: At the beginning.
* +last+: At the end.
Default: +pass:[right]+
Default: +pass:[next]+
[[tabs-new-tab-position-explicit]]
=== new-tab-position-explicit
@ -1063,10 +1063,10 @@ How new tabs opened explicitly are positioned.
Valid values:
* +left+: On the left of the current tab.
* +right+: On the right of the current tab.
* +first+: At the left end.
* +last+: At the right end.
* +prev+: Before the current tab.
* +next+: After the current tab.
* +first+: At the beginning.
* +last+: At the end.
Default: +pass:[last]+

View File

@ -45,8 +45,9 @@ import qutebrowser.resources
from qutebrowser.completion.models import instances as completionmodels
from qutebrowser.commands import cmdutils, runners, cmdexc
from qutebrowser.config import style, config, websettings, configexc
from qutebrowser.browser import urlmarks, adblock, history, browsertab
from qutebrowser.browser.webkit import cookies, cache, downloads
from qutebrowser.browser import (urlmarks, adblock, history, browsertab,
downloads)
from qutebrowser.browser.webkit import cookies, cache
from qutebrowser.browser.webkit.network import networkmanager
from qutebrowser.mainwindow import mainwindow, prompt
from qutebrowser.misc import (readline, ipc, savemanager, sessions,
@ -371,7 +372,6 @@ def _init_modules(args, crash_handler):
args: The argparse namespace.
crash_handler: The CrashHandler instance.
"""
# pylint: disable=too-many-statements
log.init.debug("Initializing prompts...")
prompt.init()
@ -435,8 +435,6 @@ def _init_modules(args, crash_handler):
os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1'
else:
os.environ.pop('QT_WAYLAND_DISABLE_WINDOWDECORATION', None)
temp_downloads = downloads.TempDownloadManager(qApp)
objreg.register('temporary-downloads', temp_downloads)
# Init backend-specific stuff
browsertab.init(args)
@ -705,6 +703,7 @@ class Quitter:
atexit.register(shutil.rmtree, self._args.basedir,
ignore_errors=True)
# Delete temp download dir
downloads.temp_download_manager.cleanup()
# If we don't kill our custom handler here we might get segfaults
log.destroy.debug("Deactivating message handler...")
qInstallMessageHandler(None)

View File

@ -26,8 +26,9 @@ import posixpath
import zipfile
import fnmatch
from qutebrowser.browser import downloads
from qutebrowser.config import config
from qutebrowser.utils import objreg, standarddir, log, message, usertypes
from qutebrowser.utils import objreg, standarddir, log, message
from qutebrowser.commands import cmdutils, cmdexc
@ -190,8 +191,8 @@ class HostBlocker:
self._blocked_hosts = set()
self._done_count = 0
urls = config.get('content', 'host-block-lists')
download_manager = objreg.get('download-manager', scope='window',
window='last-focused')
download_manager = objreg.get('qtnetwork-download-manager',
scope='window', window='last-focused')
if urls is None:
return
for url in urls:
@ -208,7 +209,7 @@ class HostBlocker:
else:
fobj = io.BytesIO()
fobj.name = 'adblock: ' + url.host()
target = usertypes.FileObjDownloadTarget(fobj)
target = downloads.FileObjDownloadTarget(fobj)
download = download_manager.get(url, target=target,
auto_remove=True)
self._in_progress.append(download)

View File

@ -43,8 +43,7 @@ import pygments.formatters
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
from qutebrowser.config import config, configexc
from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate,
webelem)
from qutebrowser.browser.webkit import downloads
webelem, downloads)
try:
from qutebrowser.browser.webkit import mhtml
except ImportError:
@ -179,12 +178,12 @@ class CommandDispatcher:
raise cmdexc.CommandError("Last focused tab vanished!")
self._set_current_index(idx)
def _get_selection_override(self, left, right, opposite):
def _get_selection_override(self, prev, next_, opposite):
"""Helper function for tab_close to get the tab to select.
Args:
left: Force selecting the tab to the left of the current tab.
right: Force selecting the tab to the right of the current tab.
prev: Force selecting the tab before the current tab.
next_: Force selecting the tab after the current tab.
opposite: Force selecting the tab in the opposite direction of
what's configured in 'tabs->select-on-remove'.
@ -192,10 +191,10 @@ class CommandDispatcher:
QTabBar.SelectLeftTab, QTabBar.SelectRightTab, or None if no change
should be made.
"""
cmdutils.check_exclusive((left, right, opposite), 'lro')
if left:
cmdutils.check_exclusive((prev, next_, opposite), 'pno')
if prev:
return QTabBar.SelectLeftTab
elif right:
elif next_:
return QTabBar.SelectRightTab
elif opposite:
conf_selection = config.get('tabs', 'select-on-remove')
@ -206,25 +205,24 @@ class CommandDispatcher:
elif conf_selection == QTabBar.SelectPreviousTab:
raise cmdexc.CommandError(
"-o is not supported with 'tabs->select-on-remove' set to "
"'previous'!")
"'last-used'!")
else: # pragma: no cover
raise ValueError("Invalid select-on-remove value "
"{!r}!".format(conf_selection))
return None
def _tab_close(self, tab, left=False, right=False, opposite=False):
def _tab_close(self, tab, prev=False, next_=False, opposite=False):
"""Helper function for tab_close be able to handle message.async.
Args:
tab: Tab select to be closed.
left: Force selecting the tab to the left of the current tab.
right: Force selecting the tab to the right of the current tab.
prev: Force selecting the tab before the current tab.
next_: Force selecting the tab after the current tab.
opposite: Force selecting the tab in the opposite direction of
what's configured in 'tabs->select-on-remove'.
count: The tab index to close, or None
"""
tabbar = self._tabbed_browser.tabBar()
selection_override = self._get_selection_override(left, right,
selection_override = self._get_selection_override(prev, next_,
opposite)
if tab.data.pinned:
@ -240,12 +238,11 @@ class CommandDispatcher:
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('count', count=True)
def tab_close(self, left=False, right=False, opposite=False, count=None):
def tab_close(self, prev=False, next_=False, opposite=False, count=None):
"""Close the current/[count]th tab.
Args:
left: Force selecting the tab to the left of the current tab.
right: Force selecting the tab to the right of the current tab.
prev: Force selecting the tab before the current tab.
next_: Force selecting the tab after the current tab.
opposite: Force selecting the tab in the opposite direction of
what's configured in 'tabs->select-on-remove'.
count: The tab index to close, or None
@ -253,9 +250,8 @@ class CommandDispatcher:
tab = self._cntwidget(count)
if tab is None:
return
close = functools.partial(self._tab_close, tab, left,
right, opposite)
close = functools.partial(self._tab_close, tab, prev,
next_, opposite)
if tab.data.pinned:
message.confirm_async(title='Pinned Tab',
@ -854,20 +850,20 @@ class CommandDispatcher:
message.info("Zoom level: {}%".format(level))
@cmdutils.register(instance='command-dispatcher', scope='window')
def tab_only(self, left=False, right=False):
def tab_only(self, prev=False, next_=False):
"""Close all tabs except for the current one.
Args:
left: Keep tabs to the left of the current.
right: Keep tabs to the right of the current.
prev: Keep tabs before the current.
next_: Keep tabs after the current.
"""
cmdutils.check_exclusive((left, right), 'lr')
cmdutils.check_exclusive((prev, next_), 'pn')
cur_idx = self._tabbed_browser.currentIndex()
assert cur_idx != -1
for i, tab in enumerate(self._tabbed_browser.widgets()):
if (i == cur_idx or (left and i < cur_idx) or
(right and i > cur_idx)):
if (i == cur_idx or (prev and i < cur_idx) or
(next_ and i > cur_idx)):
continue
else:
self._tabbed_browser.close_tab(tab)
@ -1348,8 +1344,7 @@ class CommandDispatcher:
except inspector.WebInspectorError as e:
raise cmdexc.CommandError(e)
@cmdutils.register(instance='command-dispatcher', scope='window',
backend=usertypes.Backend.QtWebKit)
@cmdutils.register(instance='command-dispatcher', scope='window')
@cmdutils.argument('dest_old', hide=True)
def download(self, url=None, dest_old=None, *, mhtml_=False, dest=None):
"""Download a given URL, or current page if no URL given.
@ -1371,8 +1366,9 @@ class CommandDispatcher:
" download.")
dest = dest_old
download_manager = objreg.get('download-manager', scope='window',
window=self._win_id)
# FIXME:qtwebengine do this with the QtWebEngine download manager?
download_manager = objreg.get('qtnetwork-download-manager',
scope='window', window=self._win_id)
if url:
if mhtml_:
raise cmdexc.CommandError("Can only download the current page"
@ -1382,21 +1378,16 @@ class CommandDispatcher:
if dest is None:
target = None
else:
target = usertypes.FileDownloadTarget(dest)
target = downloads.FileDownloadTarget(dest)
download_manager.get(url, target=target)
elif mhtml_:
self._download_mhtml(dest)
else:
tab = self._current_widget()
# FIXME:qtwebengine have a proper API for this
# pylint: disable=protected-access
qnam = tab._widget.page().networkAccessManager()
# pylint: enable=protected-access
if dest is None:
target = None
else:
target = usertypes.FileDownloadTarget(dest)
download_manager.get(self._current_url(), qnam=qnam, target=target)
target = downloads.FileDownloadTarget(dest)
download_manager.get(self._current_url(), target=target)
def _download_mhtml(self, dest=None):
"""Download the current page as an MHTML file, including all assets.
@ -1405,17 +1396,23 @@ class CommandDispatcher:
dest: The file path to write the download to.
"""
tab = self._current_widget()
if tab.backend == usertypes.Backend.QtWebEngine:
raise cmdexc.CommandError("Download --mhtml is not implemented "
"with QtWebEngine yet")
if dest is None:
suggested_fn = self._current_title() + ".mht"
suggested_fn = utils.sanitize_filename(suggested_fn)
filename, q = downloads.ask_for_filename(suggested_fn, parent=tab,
url=tab.url())
filename = downloads.immediate_download_path()
if filename is not None:
mhtml.start_download_checked(filename, tab=tab)
else:
q.answered.connect(functools.partial(
question = downloads.get_filename_question(
suggested_filename=suggested_fn, url=tab.url(), parent=tab)
question.answered.connect(functools.partial(
mhtml.start_download_checked, tab=tab))
q.ask()
message.global_bridge.ask(question, blocking=False)
else:
mhtml.start_download_checked(dest, tab=tab)

View File

@ -25,7 +25,7 @@ import sip
from PyQt5.QtCore import pyqtSlot, QSize, Qt, QTimer
from PyQt5.QtWidgets import QListView, QSizePolicy, QMenu
from qutebrowser.browser.webkit import downloads
from qutebrowser.browser import downloads
from qutebrowser.config import style
from qutebrowser.utils import qtutils, utils, objreg

View File

@ -283,14 +283,10 @@ class HintActions:
else:
prompt = None
# FIXME:qtwebengine get a proper API for this
# pylint: disable=protected-access
qnam = elem._elem.webFrame().page().networkAccessManager()
# pylint: enable=protected-access
download_manager = objreg.get('download-manager', scope='window',
window=self._win_id)
download_manager.get(url, qnam=qnam, prompt_download_directory=prompt)
# FIXME:qtwebengine do this with QtWebEngine downloads?
download_manager = objreg.get('qtnetwork-download-manager',
scope='window', window=self._win_id)
download_manager.get(url, prompt_download_directory=prompt)
def call_userscript(self, elem, context):
"""Call a userscript from a hint.
@ -662,11 +658,6 @@ class HintManager(QObject):
tab = tabbed_browser.currentWidget()
if tab is None:
raise cmdexc.CommandError("No WebView available yet!")
if (tab.backend == usertypes.Backend.QtWebEngine and
target == Target.download):
message.error("The download target is not available yet with "
"QtWebEngine.")
return
mode_manager = objreg.get('mode-manager', scope='window',
window=self._win_id)

View File

@ -0,0 +1,469 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-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/>.
"""Download manager."""
import io
import shutil
import functools
import collections
from PyQt5.QtCore import pyqtSlot, QTimer
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
from qutebrowser.utils import message, usertypes, log, urlutils
from qutebrowser.browser import downloads
from qutebrowser.browser.webkit import http
from qutebrowser.browser.webkit.network import networkmanager
_RetryInfo = collections.namedtuple('_RetryInfo', ['request', 'manager'])
class DownloadItem(downloads.AbstractDownloadItem):
"""A single download currently running.
There are multiple ways the data can flow from the QNetworkReply to the
disk.
If the filename/file object is known immediately when starting the
download, QNetworkReply's readyRead writes to the target file directly.
If not, readyRead is ignored and with self._read_timer we periodically read
into the self._buffer BytesIO slowly, so some broken servers don't close
our connection.
As soon as we know the file object, we copy self._buffer over and the next
readyRead will write to the real file object.
Class attributes:
_MAX_REDIRECTS: The maximum redirection count.
Attributes:
_retry_info: A _RetryInfo instance.
_redirects: How many time we were redirected already.
_buffer: A BytesIO object to buffer incoming data until we know the
target file.
_read_timer: A Timer which reads the QNetworkReply into self._buffer
periodically.
_manager: The DownloadManager which started this download
_reply: The QNetworkReply associated with this download.
_autoclose: Whether to close the associated file when the download is
done.
"""
_MAX_REDIRECTS = 10
def __init__(self, reply, manager):
"""Constructor.
Args:
reply: The QNetworkReply to download.
"""
super().__init__(parent=manager)
self.fileobj = None
self.raw_headers = {}
self._autoclose = True
self._manager = manager
self._retry_info = None
self._reply = None
self._buffer = io.BytesIO()
self._read_timer = usertypes.Timer(self, name='download-read-timer')
self._read_timer.setInterval(500)
self._read_timer.timeout.connect(self._on_read_timer_timeout)
self._redirects = 0
self._init_reply(reply)
def _create_fileobj(self):
"""Create a file object using the internal filename."""
try:
fileobj = open(self._filename, 'wb')
except OSError as e:
self._die(e.strerror)
else:
self._set_fileobj(fileobj)
def _do_die(self):
"""Abort the download and emit an error."""
self._read_timer.stop()
self._reply.downloadProgress.disconnect()
self._reply.finished.disconnect()
self._reply.error.disconnect()
self._reply.readyRead.disconnect()
with log.hide_qt_warning('QNetworkReplyImplPrivate::error: Internal '
'problem, this method must only be called '
'once.'):
# See https://codereview.qt-project.org/#/c/107863/
self._reply.abort()
self._reply.deleteLater()
self._reply = None
if self.fileobj is not None:
try:
self.fileobj.close()
except OSError:
log.downloads.exception("Error while closing file object")
def _init_reply(self, reply):
"""Set a new reply and connect its signals.
Args:
reply: The QNetworkReply to handle.
"""
self.done = False
self.successful = False
self._reply = reply
reply.setReadBufferSize(16 * 1024 * 1024) # 16 MB
reply.downloadProgress.connect(self.stats.on_download_progress)
reply.finished.connect(self._on_reply_finished)
reply.error.connect(self._on_reply_error)
reply.readyRead.connect(self._on_ready_read)
reply.metaDataChanged.connect(self._on_meta_data_changed)
self._retry_info = _RetryInfo(request=reply.request(),
manager=reply.manager())
if not self.fileobj:
self._read_timer.start()
# We could have got signals before we connected slots to them.
# Here no signals are connected to the DownloadItem yet, so we use a
# singleShot QTimer to emit them after they are connected.
if reply.error() != QNetworkReply.NoError:
QTimer.singleShot(0, lambda: self._die(reply.errorString()))
def _do_cancel(self):
if self._reply is not None:
self._reply.finished.disconnect(self._on_reply_finished)
self._reply.abort()
self._reply.deleteLater()
self._reply = None
if self.fileobj is not None:
self.fileobj.close()
self.cancelled.emit()
@pyqtSlot()
def retry(self):
"""Retry a failed download."""
assert self.done
assert not self.successful
new_reply = self._retry_info.manager.get(self._retry_info.request)
self._manager.fetch(new_reply, suggested_filename=self.basename)
self.cancel()
def _get_open_filename(self):
filename = self._filename
if filename is None:
filename = getattr(self.fileobj, 'name', None)
return filename
def _ensure_can_set_filename(self, filename):
if self.fileobj is not None: # pragma: no cover
raise ValueError("fileobj was already set! filename: {}, "
"existing: {}, fileobj {}".format(
filename, self._filename, self.fileobj))
def _after_set_filename(self):
self._create_fileobj()
def _ask_confirm_question(self, title, msg):
no_action = functools.partial(self.cancel, remove_data=False)
message.confirm_async(title=title, text=msg,
yes_action=self._after_set_filename,
no_action=no_action, cancel_action=no_action,
abort_on=[self.cancelled, self.error])
def _set_fileobj(self, fileobj, *, autoclose=True):
""""Set the file object to write the download to.
Args:
fileobj: A file-like object.
"""
if self.fileobj is not None: # pragma: no cover
raise ValueError("fileobj was already set! Old: {}, new: "
"{}".format(self.fileobj, fileobj))
self.fileobj = fileobj
self._autoclose = autoclose
try:
self._read_timer.stop()
log.downloads.debug("buffer: {} bytes".format(self._buffer.tell()))
self._buffer.seek(0)
shutil.copyfileobj(self._buffer, fileobj)
self._buffer.close()
if self._reply.isFinished():
# Downloading to the buffer in RAM has already finished so we
# write out the data and clean up now.
self._on_reply_finished()
else:
# Since the buffer already might be full, on_ready_read might
# not be called at all anymore, so we force it here to flush
# the buffer and continue receiving new data.
self._on_ready_read()
except OSError as e:
self._die(e.strerror)
def _set_tempfile(self, fileobj):
self._set_fileobj(fileobj)
def _finish_download(self):
"""Write buffered data to disk and finish the QNetworkReply."""
log.downloads.debug("Finishing download...")
if self._reply.isOpen():
self.fileobj.write(self._reply.readAll())
if self._autoclose:
self.fileobj.close()
self.successful = self._reply.error() == QNetworkReply.NoError
self._reply.close()
self._reply.deleteLater()
self._reply = None
self.finished.emit()
self.done = True
log.downloads.debug("Download {} finished".format(self.basename))
self.data_changed.emit()
@pyqtSlot()
def _on_reply_finished(self):
"""Clean up when the download was finished.
Note when this gets called, only the QNetworkReply has finished. This
doesn't mean the download (i.e. writing data to the disk) is finished
as well. Therefore, we can't close() the QNetworkReply in here yet.
"""
if self._reply is None:
return
self._read_timer.stop()
self.stats.finish()
is_redirected = self._handle_redirect()
if is_redirected:
return
log.downloads.debug("Reply finished, fileobj {}".format(self.fileobj))
if self.fileobj is not None:
# We can do a "delayed" write immediately to empty the buffer and
# clean up.
self._finish_download()
@pyqtSlot()
def _on_ready_read(self):
"""Read available data and save file when ready to read."""
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).
return
if not self._reply.isOpen():
raise OSError("Reply is closed!")
try:
self.fileobj.write(self._reply.readAll())
except OSError as e:
self._die(e.strerror)
@pyqtSlot('QNetworkReply::NetworkError')
def _on_reply_error(self, code):
"""Handle QNetworkReply errors."""
if code == QNetworkReply.OperationCanceledError:
return
else:
self._die(self._reply.errorString())
@pyqtSlot()
def _on_read_timer_timeout(self):
"""Read some bytes from the QNetworkReply periodically."""
if not self._reply.isOpen():
raise OSError("Reply is closed!")
data = self._reply.read(1024)
if data is not None:
self._buffer.write(data)
@pyqtSlot()
def _on_meta_data_changed(self):
"""Update the download's metadata."""
if self._reply is None:
return
self.raw_headers = {}
for key, value in self._reply.rawHeaderPairs():
self.raw_headers[bytes(key)] = bytes(value)
def _handle_redirect(self):
"""Handle an HTTP redirect.
Return:
True if the download was redirected, False otherwise.
"""
redirect = self._reply.attribute(
QNetworkRequest.RedirectionTargetAttribute)
if redirect is None or redirect.isEmpty():
return False
new_url = self._reply.url().resolved(redirect)
new_request = self._reply.request()
if new_url == new_request.url():
return False
if self._redirects > self._MAX_REDIRECTS:
self._die("Maximum redirection count reached!")
self.delete()
return True # so on_reply_finished aborts
log.downloads.debug("{}: Handling redirect".format(self))
self._redirects += 1
new_request.setUrl(new_url)
old_reply = self._reply
old_reply.finished.disconnect(self._on_reply_finished)
self._read_timer.stop()
self._reply = None
if self.fileobj is not None:
self.fileobj.seek(0)
log.downloads.debug("redirected: {} -> {}".format(
old_reply.url(), new_request.url()))
new_reply = old_reply.manager().get(new_request)
self._init_reply(new_reply)
old_reply.deleteLater()
return True
class DownloadManager(downloads.AbstractDownloadManager):
"""Manager for currently running downloads.
Attributes:
_networkmanager: A NetworkManager for generic downloads.
"""
def __init__(self, win_id, parent=None):
super().__init__(parent)
self._networkmanager = networkmanager.NetworkManager(
win_id, None, self)
@pyqtSlot('QUrl')
def get(self, url, **kwargs):
"""Start a download with a link URL.
Args:
url: The URL to get, as QUrl
**kwargs: passed to get_request().
Return:
The created DownloadItem.
"""
if not url.isValid():
urlutils.invalid_url_error(url, "start download")
return
req = QNetworkRequest(url)
return self.get_request(req, **kwargs)
def get_request(self, request, *, target=None, **kwargs):
"""Start a download with a QNetworkRequest.
Args:
request: The QNetworkRequest to download.
target: Where to save the download as downloads.DownloadTarget.
**kwargs: Passed to _fetch_request.
Return:
The created DownloadItem.
"""
# WORKAROUND for Qt corrupting data loaded from cache:
# https://bugreports.qt.io/browse/QTBUG-42757
request.setAttribute(QNetworkRequest.CacheLoadControlAttribute,
QNetworkRequest.AlwaysNetwork)
if request.url().scheme().lower() != 'data':
suggested_fn = urlutils.filename_from_url(request.url())
else:
# We might be downloading a binary blob embedded on a page or even
# generated dynamically via javascript. We try to figure out a more
# sensible name than the base64 content of the data.
origin = request.originatingObject()
try:
origin_url = origin.url()
except AttributeError:
# Raised either if origin is None or some object that doesn't
# have its own url. We're probably fine with a default fallback
# then.
suggested_fn = 'binary blob'
else:
# Use the originating URL as a base for the filename (works
# e.g. for pdf.js).
suggested_fn = urlutils.filename_from_url(origin_url)
if suggested_fn is None:
suggested_fn = 'qutebrowser-download'
return self._fetch_request(request,
target=target,
suggested_filename=suggested_fn,
**kwargs)
def _fetch_request(self, request, **kwargs):
"""Download a QNetworkRequest to disk.
Args:
request: The QNetworkRequest to download.
**kwargs: passed to fetch().
Return:
The created DownloadItem.
"""
reply = self._networkmanager.get(request)
return self.fetch(reply, **kwargs)
@pyqtSlot('QNetworkReply')
def fetch(self, reply, *, target=None, auto_remove=False,
suggested_filename=None, prompt_download_directory=None):
"""Download a QNetworkReply to disk.
Args:
reply: The QNetworkReply to download.
target: Where to save the download as downloads.DownloadTarget.
auto_remove: Whether to remove the download even if
ui -> remove-finished-downloads is set to -1.
Return:
The created DownloadItem.
"""
if not suggested_filename:
try:
suggested_filename = target.suggested_filename()
except downloads.NoFilenameError:
_, suggested_filename = http.parse_content_disposition(reply)
log.downloads.debug("fetch: {} -> {}".format(reply.url(),
suggested_filename))
download = DownloadItem(reply, manager=self)
self._init_item(download, auto_remove, suggested_filename)
if target is not None:
download.set_target(target)
return download
# Neither filename nor fileobj were given
filename = downloads.immediate_download_path(prompt_download_directory)
if filename is not None:
# User doesn't want to be asked, so just use the download_dir
target = downloads.FileDownloadTarget(filename)
download.set_target(target)
return download
# Ask the user for a filename
question = downloads.get_filename_question(
suggested_filename=suggested_filename, url=reply.url(),
parent=self)
self._init_filename_question(question, download)
message.global_bridge.ask(question, blocking=False)
return download

View File

@ -0,0 +1,158 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-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/>.
"""QtWebEngine specific code for downloads."""
import os.path
import functools
from PyQt5.QtCore import pyqtSlot, Qt
# pylint: disable=no-name-in-module,import-error,useless-suppression
from PyQt5.QtWebEngineWidgets import QWebEngineDownloadItem
# pylint: enable=no-name-in-module,import-error,useless-suppression
from qutebrowser.browser import downloads
from qutebrowser.utils import debug, usertypes, message, log
class DownloadItem(downloads.AbstractDownloadItem):
"""A wrapper over a QWebEngineDownloadItem.
Attributes:
_qt_item: The wrapped item.
"""
def __init__(self, qt_item, parent=None):
super().__init__(parent)
self._qt_item = qt_item
qt_item.downloadProgress.connect(self.stats.on_download_progress)
qt_item.stateChanged.connect(self._on_state_changed)
@pyqtSlot(QWebEngineDownloadItem.DownloadState)
def _on_state_changed(self, state):
state_name = debug.qenum_key(QWebEngineDownloadItem, state)
log.downloads.debug("State for {!r} changed to {}".format(
self, state_name))
if state == QWebEngineDownloadItem.DownloadRequested:
pass
elif state == QWebEngineDownloadItem.DownloadInProgress:
pass
elif state == QWebEngineDownloadItem.DownloadCompleted:
log.downloads.debug("Download {} finished".format(self.basename))
self.successful = True
self.done = True
self.finished.emit()
self.stats.finish()
elif state == QWebEngineDownloadItem.DownloadCancelled:
self.successful = False
self.done = True
self.cancelled.emit()
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()
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()
def _do_cancel(self):
self._qt_item.cancel()
def retry(self):
# https://bugreports.qt.io/browse/QTBUG-56840
raise downloads.UnsupportedOperationError
def _get_open_filename(self):
return self._filename
def _set_fileobj(self, fileobj):
raise downloads.UnsupportedOperationError
def _set_tempfile(self, fileobj):
self._set_filename(fileobj.name, force_overwrite=True)
def _ensure_can_set_filename(self, filename):
state = self._qt_item.state()
if state != QWebEngineDownloadItem.DownloadRequested:
state_name = debug.qenum_key(QWebEngineDownloadItem, state)
raise ValueError("Trying to set filename {} on {!r} which is "
"state {} (not in requested state)!".format(
filename, self, state_name))
def _ask_confirm_question(self, title, msg):
no_action = functools.partial(self.cancel, remove_data=False)
question = usertypes.Question()
question.title = title
question.text = msg
question.mode = usertypes.PromptMode.yesno
question.answered_yes.connect(self._after_set_filename)
question.answered_no.connect(no_action)
question.cancelled.connect(no_action)
self.cancelled.connect(question.abort)
self.error.connect(question.abort)
message.global_bridge.ask(question, blocking=True)
def _after_set_filename(self):
self._qt_item.setPath(self._filename)
self._qt_item.accept()
class DownloadManager(downloads.AbstractDownloadManager):
"""Manager for currently running downloads."""
def install(self, profile):
"""Set up the download manager on a QWebEngineProfile."""
profile.downloadRequested.connect(self.handle_download,
Qt.DirectConnection)
@pyqtSlot(QWebEngineDownloadItem)
def handle_download(self, qt_item):
"""Start a download coming from a QWebEngineProfile."""
suggested_filename = os.path.basename(qt_item.path())
download = DownloadItem(qt_item)
self._init_item(download, auto_remove=False,
suggested_filename=suggested_filename)
filename = downloads.immediate_download_path()
if filename is not None:
# User doesn't want to be asked, so just use the download_dir
target = downloads.FileDownloadTarget(filename)
download.set_target(target)
return
# Ask the user for a filename - needs to be blocking!
question = downloads.get_filename_question(
suggested_filename=suggested_filename, url=qt_item.url(),
parent=self)
self._init_filename_question(question, download)
message.global_bridge.ask(question, blocking=True)
# The filename is set via the question.answered signal, connected in
# _init_filename_question.

View File

@ -34,7 +34,8 @@ from PyQt5.QtWebEngineWidgets import (QWebEnginePage, QWebEngineScript,
from qutebrowser.browser import browsertab, mouse
from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
interceptor, webenginequtescheme)
interceptor, webenginequtescheme,
webenginedownloads)
from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
objreg)
@ -61,6 +62,11 @@ def init():
host_blocker, parent=app)
req_interceptor.install(profile)
log.init.debug("Initializing QtWebEngine downloads...")
download_manager = webenginedownloads.DownloadManager(parent=app)
download_manager.install(profile)
objreg.register('webengine-download-manager', download_manager)
# Mapping worlds from usertypes.JsWorld to QWebEngineScript world IDs.
_JS_WORLD_MAP = {

View File

@ -19,6 +19,7 @@
"""Utils for writing an MHTML file."""
import html
import functools
import io
import os
@ -34,7 +35,8 @@ import email.message
from PyQt5.QtCore import QUrl
from qutebrowser.browser.webkit import webkitelem, downloads
from qutebrowser.browser import downloads
from qutebrowser.browser.webkit import webkitelem
from qutebrowser.utils import log, objreg, message, usertypes, utils, urlutils
_File = collections.namedtuple('_File',
@ -341,9 +343,9 @@ class _Downloader:
self.writer.add_file(urlutils.encoded_url(url), b'')
return
download_manager = objreg.get('download-manager', scope='window',
window=self._win_id)
target = usertypes.FileObjDownloadTarget(_NoCloseBytesIO())
download_manager = objreg.get('qtnetwork-download-manager',
scope='window', window=self._win_id)
target = downloads.FileObjDownloadTarget(_NoCloseBytesIO())
item = download_manager.get(url, target=target,
auto_remove=True)
self.pending_downloads.add((url, item))
@ -536,10 +538,10 @@ def start_download_checked(dest, tab):
q = usertypes.Question()
q.mode = usertypes.PromptMode.yesno
q.text = "{} exists. Overwrite?".format(path)
q.title = "Overwrite existing file?"
q.text = "<b>{}</b> already exists. Overwrite?".format(
html.escape(path))
q.completed.connect(q.deleteLater)
q.answered_yes.connect(functools.partial(
_start_download, path, tab=tab))
message_bridge = objreg.get('message-bridge', scope='window',
window=tab.win_id)
message_bridge.ask(q, blocking=False)
message.global_bridge.ask(q, blocking=False)

View File

@ -135,11 +135,6 @@ class NetworkManager(QNetworkAccessManager):
"""Our own QNetworkAccessManager.
Attributes:
adopted_downloads: If downloads are running with this QNAM but the
associated tab gets closed already, the NAM gets
reparented to the DownloadManager. This counts the
still running downloads, so the QNAM can clean
itself up when this reaches zero again.
_requests: Pending requests.
_scheme_handlers: A dictionary (scheme -> handler) of supported custom
schemes.
@ -161,7 +156,6 @@ class NetworkManager(QNetworkAccessManager):
# http://www.riverbankcomputing.com/pipermail/pyqt/2014-November/035045.html
super().__init__(parent)
log.init.debug("NetworkManager init done")
self.adopted_downloads = 0
self._win_id = win_id
self._tab_id = tab_id
self._requests = []
@ -394,28 +388,6 @@ class NetworkManager(QNetworkAccessManager):
# switched from private mode to normal mode
self._set_cookiejar()
@pyqtSlot()
def on_adopted_download_destroyed(self):
"""Check if we can clean up if an adopted download was destroyed.
See the description for adopted_downloads for details.
"""
self.adopted_downloads -= 1
log.downloads.debug("Adopted download destroyed, {} left.".format(
self.adopted_downloads))
assert self.adopted_downloads >= 0
if self.adopted_downloads == 0:
self.deleteLater()
@pyqtSlot(object) # DownloadItem
def adopt_download(self, download):
"""Adopt a new DownloadItem."""
self.adopted_downloads += 1
log.downloads.debug("Adopted download, {} adopted.".format(
self.adopted_downloads))
download.destroyed.connect(self.on_adopted_download_destroyed)
download.do_retry.connect(self.adopt_download)
def set_referer(self, req, current_url):
"""Set the referer header."""
referer_header_conf = config.get('network', 'referer-header')

View File

@ -219,13 +219,7 @@ class BrowserPage(QWebPage):
"""Prepare the web page for being deleted."""
self._is_shutting_down = True
self.shutting_down.emit()
download_manager = objreg.get('download-manager', scope='window',
window=self._win_id)
nam = self.networkAccessManager()
if download_manager.has_downloads_with_nam(nam):
nam.setParent(download_manager)
else:
nam.shutdown()
self.networkAccessManager().shutdown()
def display_content(self, reply, mimetype):
"""Display a QNetworkReply with an explicitly set mimetype."""
@ -252,9 +246,9 @@ class BrowserPage(QWebPage):
after this slot returns.
"""
req = QNetworkRequest(request)
download_manager = objreg.get('download-manager', scope='window',
window=self._win_id)
download_manager.get_request(req, qnam=self.networkAccessManager())
download_manager = objreg.get('qtnetwork-download-manager',
scope='window', window=self._win_id)
download_manager.get_request(req)
@pyqtSlot('QNetworkReply*')
def on_unsupported_content(self, reply):
@ -267,8 +261,8 @@ class BrowserPage(QWebPage):
here: http://mimesniff.spec.whatwg.org/
"""
inline, suggested_filename = http.parse_content_disposition(reply)
download_manager = objreg.get('download-manager', scope='window',
window=self._win_id)
download_manager = objreg.get('qtnetwork-download-manager',
scope='window', window=self._win_id)
if not inline:
# Content-Disposition: attachment -> force download
download_manager.fetch(reply,

View File

@ -29,7 +29,7 @@ from qutebrowser.utils import message, log, objreg, standarddir
from qutebrowser.commands import runners
from qutebrowser.config import config
from qutebrowser.misc import guiprocess
from qutebrowser.browser.webkit import downloads
from qutebrowser.browser import downloads
class _QtFIFOReader(QObject):

View File

@ -413,7 +413,20 @@ class ConfigManager(QObject):
CHANGED_OPTIONS = {
('content', 'cookies-accept'):
_get_value_transformer({'default': 'no-3rdparty'}),
('tabs', 'new-tab-position'):
_get_value_transformer({
'left': 'prev',
'right': 'next'}),
('tabs', 'new-tab-position-explicit'):
_get_value_transformer({
'left': 'prev',
'right': 'next'}),
('tabs', 'position'): _transform_position,
('tabs', 'select-on-remove'):
_get_value_transformer({
'left': 'prev',
'right': 'next',
'previous': 'last-used'}),
('ui', 'downloads-position'): _transform_position,
('ui', 'remove-finished-downloads'):
_get_value_transformer({'false': '-1', 'true': '1000'}),

View File

@ -584,11 +584,11 @@ def data(readonly=False):
"background."),
('select-on-remove',
SettingValue(typ.SelectOnRemove(), 'right'),
SettingValue(typ.SelectOnRemove(), 'next'),
"Which tab to select when the focused tab is removed."),
('new-tab-position',
SettingValue(typ.NewTabPosition(), 'right'),
SettingValue(typ.NewTabPosition(), 'next'),
"How new tabs are positioned."),
('new-tab-position-explicit',
@ -1814,4 +1814,14 @@ CHANGED_KEY_COMMANDS = [
(re.compile(r'^prompt-yes$'), r'prompt-accept yes'),
(re.compile(r'^prompt-no$'), r'prompt-accept no'),
(re.compile(r'^tab-close -l$'), r'tab-close --prev'),
(re.compile(r'^tab-close --left$'), r'tab-close --prev'),
(re.compile(r'^tab-close -r$'), r'tab-close --next'),
(re.compile(r'^tab-close --right$'), r'tab-close --next'),
(re.compile(r'^tab-only -l$'), r'tab-only --prev'),
(re.compile(r'^tab-only --left$'), r'tab-only --prev'),
(re.compile(r'^tab-only -r$'), r'tab-only --next'),
(re.compile(r'^tab-only --right$'), r'tab-only --next'),
]

View File

@ -1379,18 +1379,20 @@ class SelectOnRemove(MappingType):
"""Which tab to select when the focused tab is removed."""
MAPPING = {
'left': QTabBar.SelectLeftTab,
'right': QTabBar.SelectRightTab,
'previous': QTabBar.SelectPreviousTab,
'prev': QTabBar.SelectLeftTab,
'next': QTabBar.SelectRightTab,
'last-used': QTabBar.SelectPreviousTab,
}
def __init__(self, none_ok=False):
super().__init__(
none_ok,
valid_values=ValidValues(
('left', "Select the tab on the left."),
('right', "Select the tab on the right."),
('previous', "Select the previously selected tab.")))
('prev', "Select the tab which came before the closed one "
"(left in horizontal, above in vertical)."),
('next', "Select the tab which came after the closed one "
"(right in horizontal, below in vertical)."),
('last-used', "Select the previously selected tab.")))
class ConfirmQuit(FlagList):
@ -1434,10 +1436,10 @@ class NewTabPosition(BaseType):
def __init__(self, none_ok=False):
super().__init__(none_ok)
self.valid_values = ValidValues(
('left', "On the left of the current tab."),
('right', "On the right of the current tab."),
('first', "At the left end."),
('last', "At the right end."))
('prev', "Before the current tab."),
('next', "After the current tab."),
('first', "At the beginning."),
('last', "At the end."))
class IgnoreCase(Bool):

View File

@ -35,8 +35,8 @@ from qutebrowser.mainwindow import tabbedbrowser, messageview, prompt
from qutebrowser.mainwindow.statusbar import bar
from qutebrowser.completion import completionwidget, completer
from qutebrowser.keyinput import modeman
from qutebrowser.browser import commands, downloadview, hints
from qutebrowser.browser.webkit import downloads
from qutebrowser.browser import (commands, downloadview, hints,
qtnetworkdownloads, downloads)
from qutebrowser.misc import crashsignal, keyhintwidget
@ -258,10 +258,20 @@ class MainWindow(QWidget):
def _init_downloadmanager(self):
log.init.debug("Initializing downloads...")
download_manager = downloads.DownloadManager(self.win_id, self)
objreg.register('download-manager', download_manager, scope='window',
window=self.win_id)
download_model = downloads.DownloadModel(download_manager)
qtnetwork_download_manager = qtnetworkdownloads.DownloadManager(
self.win_id, self)
objreg.register('qtnetwork-download-manager',
qtnetwork_download_manager,
scope='window', window=self.win_id)
try:
webengine_download_manager = objreg.get(
'webengine-download-manager')
except KeyError:
webengine_download_manager = None
download_model = downloads.DownloadModel(qtnetwork_download_manager,
webengine_download_manager)
objreg.register('download-model', download_model, scope='window',
window=self.win_id)

View File

@ -29,6 +29,7 @@ from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelIndex,
from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit,
QLabel, QFileSystemModel, QTreeView, QSizePolicy)
from qutebrowser.browser import downloads
from qutebrowser.config import style
from qutebrowser.utils import usertypes, log, utils, qtutils, objreg, message
from qutebrowser.keyinput import modeman
@ -687,11 +688,11 @@ class DownloadFilenamePrompt(FilenamePrompt):
def accept(self, value=None):
text = value if value is not None else self._lineedit.text()
self.question.answer = usertypes.FileDownloadTarget(text)
self.question.answer = downloads.FileDownloadTarget(text)
return True
def download_open(self, cmdline):
self.question.answer = usertypes.OpenFileDownloadTarget(cmdline)
self.question.answer = downloads.OpenFileDownloadTarget(cmdline)
self.question.done()
message.global_bridge.prompt_done.emit(self.KEY_MODE)

View File

@ -61,8 +61,8 @@ class TabbedBrowser(tabwidget.TabWidget):
_filter: A SignalFilter instance.
_now_focused: The tab which is focused now.
_tab_insert_idx_left: Where to insert a new tab with
tabbar -> new-tab-position set to 'left'.
_tab_insert_idx_right: Same as above, for 'right'.
tabbar -> new-tab-position set to 'prev'.
_tab_insert_idx_right: Same as above, for 'next'.
_undo_stack: List of UndoEntry namedtuples of closed tabs.
shutting_down: Whether we're currently shutting down.
_local_marks: Jump markers local to each page
@ -411,14 +411,14 @@ class TabbedBrowser(tabwidget.TabWidget):
pos = config.get('tabs', 'new-tab-position-explicit')
else:
pos = config.get('tabs', 'new-tab-position')
if pos == 'left':
if pos == 'prev':
idx = self._tab_insert_idx_left
# On first sight, we'd think we have to decrement
# self._tab_insert_idx_left here, as we want the next tab to be
# *before* the one we just opened. However, since we opened a tab
# *to the left* of the currently focused tab, indices will shift by
# *before* the currently focused tab, indices will shift by
# 1 automatically.
elif pos == 'right':
elif pos == 'next':
idx = self._tab_insert_idx_right
self._tab_insert_idx_right += 1
elif pos == 'first':

View File

@ -362,7 +362,7 @@ class IPCServer(QObject):
@pyqtSlot()
def on_timeout(self):
"""Cancel the current connection if it was idle for too long."""
if self._socket is None:
if self._socket is None: # pragma: no cover
log.ipc.error("on_timeout got called with None socket!")
return
log.ipc.error("IPC connection timed out "

View File

@ -23,7 +23,6 @@
import collections
import functools
import sip
from PyQt5.QtCore import QObject, QTimer
from qutebrowser.utils import log
@ -144,8 +143,12 @@ class ObjectRegistry(collections.UserDict):
"""Dump all objects as a list of strings."""
lines = []
for name, obj in self.data.items():
if not (isinstance(obj, QObject) and sip.isdeleted(obj)):
lines.append("{}: {}".format(name, repr(obj)))
try:
obj_repr = repr(obj)
except (RuntimeError, TypeError):
# Underlying object deleted probably
obj_repr = '<deleted>'
lines.append("{}: {}".format(name, obj_repr))
return lines

View File

@ -268,56 +268,6 @@ JsWorld = enum('JsWorld', ['main', 'application', 'user', 'jseval'])
MessageLevel = enum('MessageLevel', ['error', 'warning', 'info'])
# Where a download should be saved
class DownloadTarget:
"""Abstract base class for different download targets."""
def __init__(self):
raise NotImplementedError
class FileDownloadTarget(DownloadTarget):
"""Save the download to the given file.
Attributes:
filename: Filename where the download should be saved.
"""
def __init__(self, filename):
# pylint: disable=super-init-not-called
self.filename = filename
class FileObjDownloadTarget(DownloadTarget):
"""Save the download to the given file-like object.
Attributes:
fileobj: File-like object where the download should be written to.
"""
def __init__(self, fileobj):
# pylint: disable=super-init-not-called
self.fileobj = fileobj
class OpenFileDownloadTarget(DownloadTarget):
"""Save the download in a temp dir and directly open it.
Attributes:
cmdline: The command to use as string. A `{}` is expanded to the
filename. None means to use the system's default application.
If no `{}` is found, the filename is appended to the cmdline.
"""
def __init__(self, cmdline=None):
# pylint: disable=super-init-not-called
self.cmdline = cmdline
class Question(QObject):
"""A question asked to the user, e.g. via the status bar.

View File

@ -75,6 +75,7 @@ Feature: Downloading things from a website.
And I run :leave-mode
Then no crash should happen
@qtwebengine_todo: ssl-strict is not implemented yet
Scenario: Downloading with SSL errors (issue 1413)
When I run :debug-clear-ssl-errors
And I set network -> ssl-strict to ask
@ -85,7 +86,7 @@ Feature: Downloading things from a website.
Scenario: Closing window with remove-finished-downloads timeout (issue 1242)
When I set ui -> remove-finished-downloads to 500
And I open data/downloads/download.bin in a new window
And I open data/downloads/download.bin in a new window without waiting
And I wait until the download is finished
And I run :close
And I wait 0.5s
@ -95,7 +96,7 @@ Feature: Downloading things from a website.
Given I have a fresh instance
When I set storage -> prompt-download-directory to false
And I set ui -> confirm-quit to downloads
And I open data/downloads/download.bin
And I open data/downloads/download.bin without waiting
And I wait until the download is finished
And I run :close
Then qutebrowser should quit
@ -171,18 +172,36 @@ Feature: Downloading things from a website.
## mhtml downloads
@qtwebengine_todo: :download --mhtml is not implemented yet
Scenario: Downloading as mhtml is available
When I open html
And I run :download --mhtml
And I wait for "File successfully written." in the log
Then no crash should happen
@qtwebengine_todo: :download --mhtml is not implemented yet
Scenario: Downloading as mhtml with non-ASCII headers
When I open response-headers?Content-Type=text%2Fpl%C3%A4in
And I run :download --mhtml --dest mhtml-response-headers.mht
And I wait for "File successfully written." in the log
Then no crash should happen
@qtwebengine_todo: :download --mhtml is not implemented yet
Scenario: Overwriting existing mhtml file
When I set storage -> prompt-download-directory to true
And I open html
And I run :download --mhtml
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.text: 2> text='Please enter a location for <b>http://localhost:*/html</b>' title='Save file to:'>, *" in the log
And I run :prompt-accept
And I wait for "File successfully written." in the log
And I run :download --mhtml
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.text: 2> text='Please enter a location for <b>http://localhost:*/html</b>' title='Save file to:'>, *" in the log
And I run :prompt-accept
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default=None mode=<PromptMode.yesno: 1> text='<b>*</b> already exists. Overwrite?' title='Overwrite existing file?'>, *" in the log
And I run :prompt-accept yes
And I wait for "File successfully written." in the log
Then no crash should happen
## :download-cancel
Scenario: Cancelling a download
@ -199,13 +218,13 @@ Feature: Downloading things from a website.
Then the error "There's no download 42!" should be shown
Scenario: Cancelling a download which is already done
When I open data/downloads/download.bin
When I open data/downloads/download.bin without waiting
And I wait until the download is finished
And I run :download-cancel
Then the error "Download 1 is already done!" should be shown
Scenario: Cancelling a download which is already done (with count)
When I open data/downloads/download.bin
When I open data/downloads/download.bin without waiting
And I wait until the download is finished
And I run :download-cancel with count 1
Then the error "Download 1 is already done!" should be shown
@ -218,6 +237,7 @@ Feature: Downloading things from a website.
And "cancelled" should be logged
# https://github.com/The-Compiler/qutebrowser/issues/1535
@qtwebengine_todo: :download --mhtml is not implemented yet
Scenario: Cancelling an MHTML download (issue 1535)
When I open data/downloads/issue1535.html
And I run :download --mhtml
@ -228,7 +248,7 @@ Feature: Downloading things from a website.
## :download-remove / :download-clear
Scenario: Removing a download
When I open data/downloads/download.bin
When I open data/downloads/download.bin without waiting
And I wait until the download is finished
And I run :download-remove
Then "Removed download *" should be logged
@ -248,17 +268,17 @@ Feature: Downloading things from a website.
Then the error "Download 1 is not done!" should be shown
Scenario: Removing all downloads via :download-remove
When I open data/downloads/download.bin
When I open data/downloads/download.bin without waiting
And I wait until the download is finished
And I open data/downloads/download2.bin
And I open data/downloads/download2.bin without waiting
And I wait until the download is finished
And I run :download-remove --all
Then "Removed download *" should be logged
Scenario: Removing all downloads via :download-clear
When I open data/downloads/download.bin
When I open data/downloads/download.bin without waiting
And I wait until the download is finished
And I open data/downloads/download2.bin
And I open data/downloads/download2.bin without waiting
And I wait until the download is finished
And I run :download-clear
Then "Removed download *" should be logged
@ -266,7 +286,7 @@ Feature: Downloading things from a website.
## :download-delete
Scenario: Deleting a download
When I open data/downloads/download.bin
When I open data/downloads/download.bin without waiting
And I wait until the download is finished
And I run :download-delete
And I wait for "deleted download *" in the log
@ -289,13 +309,13 @@ Feature: Downloading things from a website.
## :download-open
Scenario: Opening a download
When I open data/downloads/download.bin
When I open data/downloads/download.bin without waiting
And I wait until the download is finished
And I open the download
Then "Opening *download.bin* with [*python*]" should be logged
Scenario: Opening a download with a placeholder
When I open data/downloads/download.bin
When I open data/downloads/download.bin without waiting
And I wait until the download is finished
And I open the download with a placeholder
Then "Opening *download.bin* with [*python*]" should be logged
@ -318,7 +338,8 @@ Feature: Downloading things from a website.
Scenario: Opening a download directly
When I set storage -> prompt-download-directory to true
And I open data/downloads/download.bin
And I open data/downloads/download.bin without waiting
And I wait for the download prompt for "*"
And I directly open the download
And I wait until the download is finished
Then "Opening *download.bin* with [*python*]" should be logged
@ -328,6 +349,7 @@ Feature: Downloading things from a website.
Scenario: Cancelling a download that should be opened
When I set storage -> prompt-download-directory to true
And I run :download http://localhost:(port)/drip?numbytes=128&duration=5
And I wait for the download prompt for "*"
And I directly open the download
And I run :download-cancel
Then "* finished but not successful, not opening!" should be logged
@ -338,7 +360,7 @@ Feature: Downloading things from a website.
When I set storage -> prompt-download-directory to true
And I open data/downloads/issue1725.html
And I run :click-element id long-link
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default=* mode=<PromptMode.download: 5> text=* title='Save file to:'>, *" in the log
And I wait for the download prompt for "*"
And I directly open the download
And I wait until the download is finished
Then "Opening * with [*python*]" should be logged
@ -348,19 +370,19 @@ Feature: Downloading things from a website.
Scenario: completion -> download-path-suggestion = path
When I set storage -> prompt-download-directory to true
And I set completion -> download-path-suggestion to path
And I open data/downloads/download.bin
And I open data/downloads/download.bin without waiting
Then the download prompt should be shown with "(tmpdir)/"
Scenario: completion -> download-path-suggestion = filename
When I set storage -> prompt-download-directory to true
And I set completion -> download-path-suggestion to filename
And I open data/downloads/download.bin
And I open data/downloads/download.bin without waiting
Then the download prompt should be shown with "download.bin"
Scenario: completion -> download-path-suggestion = both
When I set storage -> prompt-download-directory to true
And I set completion -> download-path-suggestion to both
And I open data/downloads/download.bin
And I open data/downloads/download.bin without waiting
Then the download prompt should be shown with "(tmpdir)/download.bin"
## storage -> remember-download-directory
@ -369,20 +391,20 @@ Feature: Downloading things from a website.
When I set storage -> prompt-download-directory to true
And I set completion -> download-path-suggestion to both
And I set storage -> remember-download-directory to true
And I open data/downloads/download.bin
And I open data/downloads/download.bin without waiting
And I wait for the download prompt for "*/download.bin"
And I run :prompt-accept (tmpdir)(dirsep)subdir
And I open data/downloads/download2.bin
And I open data/downloads/download2.bin without waiting
Then the download prompt should be shown with "(tmpdir)/subdir/download2.bin"
Scenario: Not remembering the last download directory
When I set storage -> prompt-download-directory to true
And I set completion -> download-path-suggestion to both
And I set storage -> remember-download-directory to false
And I open data/downloads/download.bin
And I open data/downloads/download.bin without waiting
And I wait for the download prompt for "(tmpdir)/download.bin"
And I run :prompt-accept (tmpdir)(dirsep)subdir
And I open data/downloads/download2.bin
And I open data/downloads/download2.bin without waiting
Then the download prompt should be shown with "(tmpdir)/download2.bin"
# Overwriting files
@ -475,6 +497,7 @@ Feature: Downloading things from a website.
And I run :download foo!
Then the error "Invalid URL" should be shown
@qtwebengine_todo: pdfjs is not implemented yet
Scenario: Downloading via pdfjs
Given pdfjs is available
When I set storage -> prompt-download-directory to false
@ -496,3 +519,9 @@ Feature: Downloading things from a website.
And I wait until the download is finished
Then the downloaded file download.bin should exist
And the downloaded file download2.bin should not exist
Scenario: Downloading a file with unknown size
When I set storage -> prompt-download-directory to false
And I open stream-bytes/1024 without waiting
And I wait until the download is finished
Then the downloaded file 1024 should exist

View File

@ -75,8 +75,8 @@ Feature: Opening pages
Scenario: Opening in a new tab (explicit)
Given I open about:blank
When I set tabs -> new-tab-position-explicit to right
And I set tabs -> new-tab-position to left
When I set tabs -> new-tab-position-explicit to next
And I set tabs -> new-tab-position to prev
And I run :tab-only
And I run :open -t http://localhost:(port)/data/numbers/7.txt
And I wait until data/numbers/7.txt is loaded
@ -86,8 +86,8 @@ Feature: Opening pages
Scenario: Opening in a new tab (implicit)
Given I open about:blank
When I set tabs -> new-tab-position-explicit to right
And I set tabs -> new-tab-position to left
When I set tabs -> new-tab-position-explicit to next
And I set tabs -> new-tab-position to prev
And I run :tab-only
And I run :open -t -i http://localhost:(port)/data/numbers/8.txt
And I wait until data/numbers/8.txt is loaded

View File

@ -35,8 +35,8 @@ Feature: Tab management
- data/numbers/2.txt
- data/numbers/3.txt (active)
Scenario: :tab-close with select-on-remove = right
When I set tabs -> select-on-remove to right
Scenario: :tab-close with select-on-remove = next
When I set tabs -> select-on-remove to next
And I open data/numbers/1.txt
And I open data/numbers/2.txt in a new tab
And I open data/numbers/3.txt in a new tab
@ -46,8 +46,8 @@ Feature: Tab management
- data/numbers/1.txt
- data/numbers/3.txt (active)
Scenario: :tab-close with select-on-remove = left
When I set tabs -> select-on-remove to left
Scenario: :tab-close with select-on-remove = prev
When I set tabs -> select-on-remove to prev
And I open data/numbers/1.txt
And I open data/numbers/2.txt in a new tab
And I open data/numbers/3.txt in a new tab
@ -57,8 +57,8 @@ Feature: Tab management
- data/numbers/1.txt (active)
- data/numbers/3.txt
Scenario: :tab-close with select-on-remove = previous
When I set tabs -> select-on-remove to previous
Scenario: :tab-close with select-on-remove = last-used
When I set tabs -> select-on-remove to last-used
And I open data/numbers/1.txt
And I open data/numbers/2.txt in a new tab
And I open data/numbers/3.txt in a new tab
@ -70,30 +70,30 @@ Feature: Tab management
- data/numbers/3.txt
- data/numbers/4.txt (active)
Scenario: :tab-close with select-on-remove = left and --right
When I set tabs -> select-on-remove to left
Scenario: :tab-close with select-on-remove = prev and --next
When I set tabs -> select-on-remove to prev
And I open data/numbers/1.txt
And I open data/numbers/2.txt in a new tab
And I open data/numbers/3.txt in a new tab
And I run :tab-focus 2
And I run :tab-close --right
And I run :tab-close --next
Then the following tabs should be open:
- data/numbers/1.txt
- data/numbers/3.txt (active)
Scenario: :tab-close with select-on-remove = right and --left
When I set tabs -> select-on-remove to right
Scenario: :tab-close with select-on-remove = next and --prev
When I set tabs -> select-on-remove to next
And I open data/numbers/1.txt
And I open data/numbers/2.txt in a new tab
And I open data/numbers/3.txt in a new tab
And I run :tab-focus 2
And I run :tab-close --left
And I run :tab-close --prev
Then the following tabs should be open:
- data/numbers/1.txt (active)
- data/numbers/3.txt
Scenario: :tab-close with select-on-remove = left and --opposite
When I set tabs -> select-on-remove to left
Scenario: :tab-close with select-on-remove = prev and --opposite
When I set tabs -> select-on-remove to prev
And I open data/numbers/1.txt
And I open data/numbers/2.txt in a new tab
And I open data/numbers/3.txt in a new tab
@ -103,8 +103,8 @@ Feature: Tab management
- data/numbers/1.txt
- data/numbers/3.txt (active)
Scenario: :tab-close with select-on-remove = right and --opposite
When I set tabs -> select-on-remove to right
Scenario: :tab-close with select-on-remove = next and --opposite
When I set tabs -> select-on-remove to next
And I open data/numbers/1.txt
And I open data/numbers/2.txt in a new tab
And I open data/numbers/3.txt in a new tab
@ -114,19 +114,19 @@ Feature: Tab management
- data/numbers/1.txt (active)
- data/numbers/3.txt
Scenario: :tab-close with select-on-remove = previous and --opposite
When I set tabs -> select-on-remove to previous
Scenario: :tab-close with select-on-remove = last-used and --opposite
When I set tabs -> select-on-remove to last-used
And I run :tab-close --opposite
Then the error "-o is not supported with 'tabs->select-on-remove' set to 'previous'!" should be shown
Then the error "-o is not supported with 'tabs->select-on-remove' set to 'last-used'!" should be shown
Scenario: :tab-close should restore selection behavior
When I set tabs -> select-on-remove to right
When I set tabs -> select-on-remove to next
And I open data/numbers/1.txt
And I open data/numbers/2.txt in a new tab
And I open data/numbers/3.txt in a new tab
And I open data/numbers/4.txt in a new tab
And I run :tab-focus 2
And I run :tab-close --left
And I run :tab-close --prev
And I run :tab-focus 2
And I run :tab-close
Then the following tabs should be open:
@ -143,29 +143,29 @@ Feature: Tab management
Then the following tabs should be open:
- data/numbers/3.txt (active)
Scenario: :tab-only with --left
Scenario: :tab-only with --prev
When I open data/numbers/1.txt
And I open data/numbers/2.txt in a new tab
And I open data/numbers/3.txt in a new tab
And I run :tab-focus 2
And I run :tab-only --left
And I run :tab-only --prev
Then the following tabs should be open:
- data/numbers/1.txt
- data/numbers/2.txt (active)
Scenario: :tab-only with --right
Scenario: :tab-only with --next
When I open data/numbers/1.txt
And I open data/numbers/2.txt in a new tab
And I open data/numbers/3.txt in a new tab
And I run :tab-focus 2
And I run :tab-only --right
And I run :tab-only --next
Then the following tabs should be open:
- data/numbers/2.txt (active)
- data/numbers/3.txt
Scenario: :tab-only with --left and --right
When I run :tab-only --left --right
Then the error "Only one of -l/-r can be given!" should be shown
Scenario: :tab-only with --prev and --next
When I run :tab-only --prev --next
Then the error "Only one of -p/-n can be given!" should be shown
# :tab-focus
@ -821,8 +821,8 @@ Feature: Tab management
- data/hints/html/simple.html (active)
- data/hello.txt
Scenario: opening tab with tabs->new-tab-position left
When I set tabs -> new-tab-position to left
Scenario: opening tab with tabs->new-tab-position prev
When I set tabs -> new-tab-position to prev
And I set tabs -> background-tabs to false
And I open about:blank
And I open data/hints/html/simple.html in a new tab
@ -833,8 +833,8 @@ Feature: Tab management
- data/hello.txt (active)
- data/hints/html/simple.html
Scenario: opening tab with tabs->new-tab-position right
When I set tabs -> new-tab-position to right
Scenario: opening tab with tabs->new-tab-position next
When I set tabs -> new-tab-position to next
And I set tabs -> background-tabs to false
And I open about:blank
And I open data/hints/html/simple.html in a new tab

View File

@ -21,15 +21,10 @@ import os
import sys
import shlex
import pytest
import pytest_bdd as bdd
bdd.scenarios('downloads.feature')
pytestmark = pytest.mark.qtwebengine_todo("Downloads not implemented yet",
run=False)
PROMPT_MSG = ("Asking question <qutebrowser.utils.usertypes.Question "
"default={!r} mode=<PromptMode.download: 5> text=* "
"title='Save file to:'>, *")

View File

@ -263,6 +263,7 @@ Feature: Yanking and pasting.
And I run :click-element id qute-textarea
And I wait for "Clicked editable element!" in the log
And I run :insert-text Hello world
And I wait for "Inserting text into element *" in the log
And I run :jseval console.log("textarea contents: " + document.getElementById('qute-textarea').value);
# Enable javascript again for the other tests
And I set content -> allow-javascript to true

View File

@ -101,10 +101,11 @@ class FakeDownloadManager:
def download_stub(win_registry):
"""Register a FakeDownloadManager."""
stub = FakeDownloadManager()
objreg.register('download-manager', stub,
objreg.register('qtnetwork-download-manager', stub,
scope='window', window='last-focused')
yield
objreg.delete('download-manager', scope='window', window='last-focused')
objreg.delete('qtnetwork-download-manager', scope='window',
window='last-focused')
def create_zipfile(directory, files, zipname='test'):

View File

@ -17,13 +17,46 @@
# 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 qutebrowser.browser.webkit import downloads
from qutebrowser.browser import downloads, qtnetworkdownloads
def test_download_model(qapp, qtmodeltester, config_stub, cookiejar_and_cache):
"""Simple check for download model internals."""
config_stub.data = {'general': {'private-browsing': False}}
manager = downloads.DownloadManager(win_id=0)
manager = qtnetworkdownloads.DownloadManager(win_id=0)
model = downloads.DownloadModel(manager)
qtmodeltester.check(model)
class TestDownloadTarget:
def test_base(self):
with pytest.raises(NotImplementedError):
downloads._DownloadTarget()
def test_filename(self):
target = downloads.FileDownloadTarget("/foo/bar")
assert target.filename == "/foo/bar"
def test_fileobj(self):
fobj = object()
target = downloads.FileObjDownloadTarget(fobj)
assert target.fileobj is fobj
def test_openfile(self):
target = downloads.OpenFileDownloadTarget()
assert target.cmdline is None
def test_openfile_custom_command(self):
target = downloads.OpenFileDownloadTarget('echo')
assert target.cmdline == 'echo'
@pytest.mark.parametrize('obj', [
downloads.FileDownloadTarget('foobar'),
downloads.FileObjDownloadTarget(None),
downloads.OpenFileDownloadTarget(),
])
def test_class_hierarchy(self, obj):
assert isinstance(obj, downloads._DownloadTarget)

View File

@ -348,6 +348,16 @@ class TestKeyConfigParser:
('prompt-yes', 'prompt-accept yes'),
('prompt-no', 'prompt-accept no'),
('tab-close -l', 'tab-close --prev'),
('tab-close --left', 'tab-close --prev'),
('tab-close -r', 'tab-close --next'),
('tab-close --right', 'tab-close --next'),
('tab-only -l', 'tab-only --prev'),
('tab-only --left', 'tab-only --prev'),
('tab-only -r', 'tab-only --next'),
('tab-only --right', 'tab-only --next'),
]
)
def test_migrations(self, old, new_expected):

View File

@ -1,59 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016 Daniel Schadt
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Tests for the DownloadTarget class."""
from qutebrowser.utils import usertypes
import pytest
def test_base():
with pytest.raises(NotImplementedError):
usertypes.DownloadTarget()
def test_filename():
target = usertypes.FileDownloadTarget("/foo/bar")
assert target.filename == "/foo/bar"
def test_fileobj():
fobj = object()
target = usertypes.FileObjDownloadTarget(fobj)
assert target.fileobj is fobj
def test_openfile():
target = usertypes.OpenFileDownloadTarget()
assert target.cmdline is None
def test_openfile_custom_command():
target = usertypes.OpenFileDownloadTarget('echo')
assert target.cmdline == 'echo'
@pytest.mark.parametrize('obj', [
usertypes.FileDownloadTarget('foobar'),
usertypes.FileObjDownloadTarget(None),
usertypes.OpenFileDownloadTarget(),
])
def test_class_hierarchy(obj):
assert isinstance(obj, usertypes.DownloadTarget)