Merge branch 'Kingdread-open-download'

This commit is contained in:
Florian Bruhin 2016-07-26 10:54:59 +02:00
commit d70f3a0417
16 changed files with 289 additions and 81 deletions

View File

@ -32,6 +32,8 @@ Added
Note that two former default bundings conflict with that binding, unbinding Note that two former default bundings conflict with that binding, unbinding
them via `:unbind .i` and `:unbind .o` is recommended. them via `:unbind .i` and `:unbind .o` is recommended.
- New `qute:bookmarks` page which displays all bookmarks and quickmarks. - New `qute:bookmarks` page which displays all bookmarks and quickmarks.
- New `:prompt-open-download` (bound to `Ctrl-X`) which can be used to open a
download directly when getting the filename prompt.
Changed Changed
~~~~~~~ ~~~~~~~

View File

@ -936,6 +936,7 @@ How many steps to zoom out.
|<<paste-primary,paste-primary>>|Paste the primary selection at cursor position. |<<paste-primary,paste-primary>>|Paste the primary selection at cursor position.
|<<prompt-accept,prompt-accept>>|Accept the current prompt. |<<prompt-accept,prompt-accept>>|Accept the current prompt.
|<<prompt-no,prompt-no>>|Answer no to a yes/no prompt. |<<prompt-no,prompt-no>>|Answer no to a yes/no prompt.
|<<prompt-open-download,prompt-open-download>>|Immediately open a download.
|<<prompt-yes,prompt-yes>>|Answer yes to a yes/no prompt. |<<prompt-yes,prompt-yes>>|Answer yes to a yes/no prompt.
|<<repeat-command,repeat-command>>|Repeat the last executed command. |<<repeat-command,repeat-command>>|Repeat the last executed command.
|<<rl-backward-char,rl-backward-char>>|Move back a character. |<<rl-backward-char,rl-backward-char>>|Move back a character.
@ -1160,6 +1161,10 @@ Accept the current prompt.
=== prompt-no === prompt-no
Answer no to a yes/no prompt. Answer no to a yes/no prompt.
[[prompt-open-download]]
=== prompt-open-download
Immediately open a download.
[[prompt-yes]] [[prompt-yes]]
=== prompt-yes === prompt-yes
Answer yes to a yes/no prompt. Answer yes to a yes/no prompt.

View File

@ -47,7 +47,7 @@ from qutebrowser.completion.models import instances as completionmodels
from qutebrowser.commands import cmdutils, runners, cmdexc from qutebrowser.commands import cmdutils, runners, cmdexc
from qutebrowser.config import style, config, websettings, configexc from qutebrowser.config import style, config, websettings, configexc
from qutebrowser.browser import urlmarks, adblock from qutebrowser.browser import urlmarks, adblock
from qutebrowser.browser.webkit import cookies, cache, history from qutebrowser.browser.webkit import cookies, cache, history, downloads
from qutebrowser.browser.webkit.network import (qutescheme, proxy, from qutebrowser.browser.webkit.network import (qutescheme, proxy,
networkmanager) networkmanager)
from qutebrowser.mainwindow import mainwindow from qutebrowser.mainwindow import mainwindow
@ -436,6 +436,8 @@ def _init_modules(args, crash_handler):
os.environ.pop('QT_WAYLAND_DISABLE_WINDOWDECORATION', None) os.environ.pop('QT_WAYLAND_DISABLE_WINDOWDECORATION', None)
_maybe_hide_mouse_cursor() _maybe_hide_mouse_cursor()
objreg.get('config').changed.connect(_maybe_hide_mouse_cursor) objreg.get('config').changed.connect(_maybe_hide_mouse_cursor)
temp_downloads = downloads.TempDownloadManager(qApp)
objreg.register('temporary-downloads', temp_downloads)
def _init_late_modules(args): def _init_late_modules(args):
@ -708,6 +710,8 @@ class Quitter:
not restart): not restart):
atexit.register(shutil.rmtree, self._args.basedir, atexit.register(shutil.rmtree, self._args.basedir,
ignore_errors=True) ignore_errors=True)
# Delete temp download dir
objreg.get('temporary-downloads').cleanup()
# If we don't kill our custom handler here we might get segfaults # If we don't kill our custom handler here we might get segfaults
log.destroy.debug("Deactivating message handler...") log.destroy.debug("Deactivating message handler...")
qInstallMessageHandler(None) qInstallMessageHandler(None)

View File

@ -27,7 +27,7 @@ import zipfile
import fnmatch import fnmatch
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import objreg, standarddir, log, message from qutebrowser.utils import objreg, standarddir, log, message, usertypes
from qutebrowser.commands import cmdutils, cmdexc from qutebrowser.commands import cmdutils, cmdexc
@ -210,7 +210,8 @@ class HostBlocker:
else: else:
fobj = io.BytesIO() fobj = io.BytesIO()
fobj.name = 'adblock: ' + url.host() fobj.name = 'adblock: ' + url.host()
download = download_manager.get(url, fileobj=fobj, target = usertypes.FileObjDownloadTarget(fobj)
download = download_manager.get(url, target=target,
auto_remove=True) auto_remove=True)
self._in_progress.append(download) self._in_progress.append(download)
download.finished.connect( download.finished.connect(

View File

@ -1267,15 +1267,23 @@ class CommandDispatcher:
" as mhtml.") " as mhtml.")
url = urlutils.qurl_from_user_input(url) url = urlutils.qurl_from_user_input(url)
urlutils.raise_cmdexc_if_invalid(url) urlutils.raise_cmdexc_if_invalid(url)
download_manager.get(url, filename=dest) if dest is None:
target = None
else:
target = usertypes.FileDownloadTarget(dest)
download_manager.get(url, target=target)
elif mhtml_: elif mhtml_:
self._download_mhtml(dest) self._download_mhtml(dest)
else: else:
# FIXME:qtwebengine have a proper API for this # FIXME:qtwebengine have a proper API for this
tab = self._current_widget() tab = self._current_widget()
page = tab._widget.page() # pylint: disable=protected-access page = tab._widget.page() # pylint: disable=protected-access
if dest is None:
target = None
else:
target = usertypes.FileDownloadTarget(dest)
download_manager.get(self._current_url(), page=page, download_manager.get(self._current_url(), page=page,
filename=dest) target=target)
def _download_mhtml(self, dest=None): def _download_mhtml(self, dest=None):
"""Download the current page as an MHTML file, including all assets. """Download the current page as an MHTML file, including all assets.

View File

@ -25,6 +25,7 @@ import sys
import os.path import os.path
import shutil import shutil
import functools import functools
import tempfile
import collections import collections
import sip import sip
@ -280,6 +281,7 @@ class DownloadItem(QObject):
_read_timer: A Timer which reads the QNetworkReply into self._buffer _read_timer: A Timer which reads the QNetworkReply into self._buffer
periodically. periodically.
_win_id: The window ID the DownloadItem runs in. _win_id: The window ID the DownloadItem runs in.
_dead: Whether the Download has _die()'d.
Signals: Signals:
data_changed: The downloads metadata changed. data_changed: The downloads metadata changed.
@ -328,6 +330,7 @@ class DownloadItem(QObject):
self.init_reply(reply) self.init_reply(reply)
self._win_id = win_id self._win_id = win_id
self.raw_headers = {} self.raw_headers = {}
self._dead = False
def __repr__(self): def __repr__(self):
return utils.get_repr(self, basename=self.basename) return utils.get_repr(self, basename=self.basename)
@ -395,6 +398,21 @@ class DownloadItem(QObject):
def _die(self, msg): def _die(self, msg):
"""Abort the download and emit an error.""" """Abort the download and emit an error."""
assert not self.successful assert not self.successful
# Prevent actions if calling _die() twice. This might happen if the
# error handler correctly connects, and the error occurs in init_reply
# between reply.error.connect and the reply.error() check. In this
# case, the connected error handlers will be called twice, once via the
# direct error.emit() and once here in _die(). The stacks look like
# this then:
# <networkmanager error.emit> -> on_reply_error -> _die ->
# self.error.emit()
# and
# [init_reply -> <single shot timer> ->] <lambda in init_reply> ->
# self.error.emit()
# which may lead to duplicate error messages (and failing tests)
if self._dead:
return
self._dead = True
self._read_timer.stop() self._read_timer.stop()
self.reply.downloadProgress.disconnect() self.reply.downloadProgress.disconnect()
self.reply.finished.disconnect() self.reply.finished.disconnect()
@ -441,7 +459,7 @@ class DownloadItem(QObject):
# Here no signals are connected to the DownloadItem yet, so we use a # Here no signals are connected to the DownloadItem yet, so we use a
# singleShot QTimer to emit them after they are connected. # singleShot QTimer to emit them after they are connected.
if reply.error() != QNetworkReply.NoError: if reply.error() != QNetworkReply.NoError:
QTimer.singleShot(0, lambda: self.error.emit(reply.errorString())) QTimer.singleShot(0, lambda: self._die(reply.errorString()))
def get_status_color(self, position): def get_status_color(self, position):
"""Choose an appropriate color for presenting the download's status. """Choose an appropriate color for presenting the download's status.
@ -513,7 +531,13 @@ class DownloadItem(QObject):
def open_file(self): def open_file(self):
"""Open the downloaded file.""" """Open the downloaded file."""
assert self.successful assert self.successful
url = QUrl.fromLocalFile(self._filename) filename = self._filename
if filename is None:
filename = getattr(self.fileobj, 'name', None)
if filename is None:
log.downloads.error("No filename to open the download!")
return
url = QUrl.fromLocalFile(filename)
QDesktopServices.openUrl(url) QDesktopServices.openUrl(url)
def set_filename(self, filename): def set_filename(self, filename):
@ -738,6 +762,9 @@ class DownloadManager(QAbstractListModel):
def _postprocess_question(self, q): def _postprocess_question(self, q):
"""Postprocess a Question object that is asked.""" """Postprocess a Question object that is asked."""
q.destroyed.connect(functools.partial(self.questions.remove, q)) q.destroyed.connect(functools.partial(self.questions.remove, q))
# We set the mode here so that other code that uses ask_for_filename
# doesn't need to handle the special download mode.
q.mode = usertypes.PromptMode.download
self.questions.append(q) self.questions.append(q)
@pyqtSlot() @pyqtSlot()
@ -757,10 +784,7 @@ class DownloadManager(QAbstractListModel):
**kwargs: passed to get_request(). **kwargs: passed to get_request().
Return: Return:
If the download could start immediately, (fileobj/filename given), The created DownloadItem.
the created DownloadItem.
If not, None.
""" """
if not url.isValid(): if not url.isValid():
urlutils.invalid_url_error(self._win_id, url, "start download") urlutils.invalid_url_error(self._win_id, url, "start download")
@ -768,27 +792,17 @@ class DownloadManager(QAbstractListModel):
req = QNetworkRequest(url) req = QNetworkRequest(url)
return self.get_request(req, **kwargs) return self.get_request(req, **kwargs)
def get_request(self, request, *, fileobj=None, filename=None, def get_request(self, request, *, target=None, **kwargs):
prompt_download_directory=None, **kwargs):
"""Start a download with a QNetworkRequest. """Start a download with a QNetworkRequest.
Args: Args:
request: The QNetworkRequest to download. request: The QNetworkRequest to download.
fileobj: The file object to write the answer to. target: Where to save the download as usertypes.DownloadTarget.
filename: A path to write the data to.
prompt_download_directory: Whether to prompt for the download dir
or automatically download. If None, the
config is used.
**kwargs: Passed to fetch_request. **kwargs: Passed to fetch_request.
Return: Return:
If the download could start immediately, (fileobj/filename given), The created DownloadItem.
the created DownloadItem.
If not, None.
""" """
if fileobj is not None and filename is not None: # pragma: no cover
raise TypeError("Only one of fileobj/filename may be given!")
# WORKAROUND for Qt corrupting data loaded from cache: # WORKAROUND for Qt corrupting data loaded from cache:
# https://bugreports.qt.io/browse/QTBUG-42757 # https://bugreports.qt.io/browse/QTBUG-42757
request.setAttribute(QNetworkRequest.CacheLoadControlAttribute, request.setAttribute(QNetworkRequest.CacheLoadControlAttribute,
@ -816,27 +830,10 @@ class DownloadManager(QAbstractListModel):
if suggested_fn is None: if suggested_fn is None:
suggested_fn = 'qutebrowser-download' suggested_fn = 'qutebrowser-download'
# We won't need a question if a filename or fileobj is already given return self.fetch_request(request,
if fileobj is None and filename is None: target=target,
filename, q = ask_for_filename( suggested_filename=suggested_fn,
suggested_fn, self._win_id, parent=self, **kwargs)
prompt_download_directory=prompt_download_directory
)
if fileobj is not None or filename is not None:
return self.fetch_request(request,
fileobj=fileobj,
filename=filename,
suggested_filename=suggested_fn,
**kwargs)
q.answered.connect(
lambda fn: self.fetch_request(request,
filename=fn,
suggested_filename=suggested_fn,
**kwargs))
self._postprocess_question(q)
q.ask()
return None
def fetch_request(self, request, *, page=None, **kwargs): def fetch_request(self, request, *, page=None, **kwargs):
"""Download a QNetworkRequest to disk. """Download a QNetworkRequest to disk.
@ -857,27 +854,25 @@ class DownloadManager(QAbstractListModel):
return self.fetch(reply, **kwargs) return self.fetch(reply, **kwargs)
@pyqtSlot('QNetworkReply') @pyqtSlot('QNetworkReply')
def fetch(self, reply, *, fileobj=None, filename=None, auto_remove=False, def fetch(self, reply, *, target=None, auto_remove=False,
suggested_filename=None, prompt_download_directory=None): suggested_filename=None, prompt_download_directory=None):
"""Download a QNetworkReply to disk. """Download a QNetworkReply to disk.
Args: Args:
reply: The QNetworkReply to download. reply: The QNetworkReply to download.
fileobj: The file object to write the answer to. target: Where to save the download as usertypes.DownloadTarget.
filename: A path to write the data to.
auto_remove: Whether to remove the download even if auto_remove: Whether to remove the download even if
ui -> remove-finished-downloads is set to -1. ui -> remove-finished-downloads is set to -1.
Return: Return:
The created DownloadItem. The created DownloadItem.
""" """
if fileobj is not None and filename is not None: # pragma: no cover
raise TypeError("Only one of fileobj/filename may be given!")
if not suggested_filename: if not suggested_filename:
if filename is not None: if isinstance(target, usertypes.FileDownloadTarget):
suggested_filename = os.path.basename(filename) suggested_filename = os.path.basename(target.filename)
elif fileobj is not None and getattr(fileobj, 'name', None): elif (isinstance(target, usertypes.FileObjDownloadTarget) and
suggested_filename = fileobj.name getattr(target.fileobj, 'name', None)):
suggested_filename = target.fileobj.name
else: else:
_, suggested_filename = http.parse_content_disposition(reply) _, suggested_filename = http.parse_content_disposition(reply)
log.downloads.debug("fetch: {} -> {}".format(reply.url(), log.downloads.debug("fetch: {} -> {}".format(reply.url(),
@ -909,13 +904,8 @@ class DownloadManager(QAbstractListModel):
if not self._update_timer.isActive(): if not self._update_timer.isActive():
self._update_timer.start() self._update_timer.start()
if fileobj is not None: if target is not None:
download.set_fileobj(fileobj) self._set_download_target(download, suggested_filename, target)
download.autoclose = False
return download
if filename is not None:
download.set_filename(filename)
return download return download
# Neither filename nor fileobj were given, prepare a question # Neither filename nor fileobj were given, prepare a question
@ -926,12 +916,15 @@ class DownloadManager(QAbstractListModel):
# User doesn't want to be asked, so just use the download_dir # User doesn't want to be asked, so just use the download_dir
if filename is not None: if filename is not None:
download.set_filename(filename) target = usertypes.FileDownloadTarget(filename)
self._set_download_target(download, suggested_filename, target)
return download return download
# Ask the user for a filename # Ask the user for a filename
self._postprocess_question(q) self._postprocess_question(q)
q.answered.connect(download.set_filename) q.answered.connect(
functools.partial(self._set_download_target, download,
suggested_filename))
q.cancelled.connect(download.cancel) q.cancelled.connect(download.cancel)
download.cancelled.connect(q.abort) download.cancelled.connect(q.abort)
download.error.connect(q.abort) download.error.connect(q.abort)
@ -939,6 +932,28 @@ class DownloadManager(QAbstractListModel):
return download return download
def _set_download_target(self, download, suggested_filename, target):
"""Set the target for a given download.
Args:
download: The download to set the filename for.
suggested_filename: The suggested filename.
target: The usertypes.DownloadTarget for this download.
"""
if isinstance(target, usertypes.FileObjDownloadTarget):
download.set_fileobj(target.fileobj)
download.autoclose = False
elif isinstance(target, usertypes.FileDownloadTarget):
download.set_filename(target.filename)
elif isinstance(target, usertypes.OpenFileDownloadTarget):
tmp_manager = objreg.get('temporary-downloads')
fobj = tmp_manager.get_tmpfile(suggested_filename)
download.finished.connect(download.open_file)
download.autoclose = True
download.set_fileobj(fobj)
else:
log.downloads.error("Unknown download target: {}".format(target))
def raise_no_download(self, count): def raise_no_download(self, count):
"""Raise an exception that the download doesn't exist. """Raise an exception that the download doesn't exist.
@ -1249,3 +1264,59 @@ class DownloadManager(QAbstractListModel):
The number of unfinished downloads. The number of unfinished downloads.
""" """
return sum(1 for download in self.downloads if not download.done) return sum(1 for download in self.downloads if not download.done)
class TempDownloadManager(QObject):
"""Manager to handle temporary download files.
The downloads are downloaded to a temporary location and then openened with
the system standard application. The temporary files are deleted when
qutebrowser is shutdown.
Attributes:
files: A list of NamedTemporaryFiles of downloaded items.
"""
def __init__(self, parent=None):
super().__init__(parent)
self.files = []
self._tmpdir = None
def cleanup(self):
"""Clean up any temporary files."""
if self._tmpdir is not None:
self._tmpdir.cleanup()
self._tmpdir = None
def _get_tmpdir(self):
"""Return the temporary directory that is used for downloads.
The directory is created lazily on first access.
Return:
The tempfile.TemporaryDirectory that is used.
"""
if self._tmpdir is None:
self._tmpdir = tempfile.TemporaryDirectory(
prefix='qutebrowser-downloads-')
return self._tmpdir
def get_tmpfile(self, suggested_name):
"""Return a temporary file in the temporary downloads directory.
The files are kept as long as qutebrowser is running and automatically
cleaned up at program exit.
Args:
suggested_name: str of the "suggested"/original filename. Used as a
suffix, so any file extenions are preserved.
Return:
A tempfile.NamedTemporaryFile that should be used to save the file.
"""
tmpdir = self._get_tmpdir()
fobj = tempfile.NamedTemporaryFile(dir=tmpdir.name, delete=False,
suffix=suggested_name)
self.files.append(fobj)
return fobj

View File

@ -343,7 +343,8 @@ class _Downloader:
download_manager = objreg.get('download-manager', scope='window', download_manager = objreg.get('download-manager', scope='window',
window=self._win_id) window=self._win_id)
item = download_manager.get(url, fileobj=_NoCloseBytesIO(), target = usertypes.FileObjDownloadTarget(_NoCloseBytesIO())
item = download_manager.get(url, target=target,
auto_remove=True) auto_remove=True)
self.pending_downloads.add((url, item)) self.pending_downloads.add((url, item))
item.finished.connect(functools.partial(self._finished, url, item)) item.finished.connect(functools.partial(self._finished, url, item))

View File

@ -1574,6 +1574,7 @@ KEY_DATA = collections.OrderedDict([
('prompt-accept', RETURN_KEYS), ('prompt-accept', RETURN_KEYS),
('prompt-yes', ['y']), ('prompt-yes', ['y']),
('prompt-no', ['n']), ('prompt-no', ['n']),
('prompt-open-download', ['<Ctrl-X>']),
])), ])),
('command,prompt', collections.OrderedDict([ ('command,prompt', collections.OrderedDict([

View File

@ -80,6 +80,7 @@ class Prompter(QObject):
usertypes.PromptMode.text: usertypes.KeyMode.prompt, usertypes.PromptMode.text: usertypes.KeyMode.prompt,
usertypes.PromptMode.user_pwd: usertypes.KeyMode.prompt, usertypes.PromptMode.user_pwd: usertypes.KeyMode.prompt,
usertypes.PromptMode.alert: usertypes.KeyMode.prompt, usertypes.PromptMode.alert: usertypes.KeyMode.prompt,
usertypes.PromptMode.download: usertypes.KeyMode.prompt,
} }
show_prompt = pyqtSignal() show_prompt = pyqtSignal()
@ -164,12 +165,9 @@ class Prompter(QObject):
suffix = " (no)" suffix = " (no)"
prompt.txt.setText(self._question.text + suffix) prompt.txt.setText(self._question.text + suffix)
prompt.lineedit.hide() prompt.lineedit.hide()
elif self._question.mode == usertypes.PromptMode.text: elif self._question.mode in [usertypes.PromptMode.text,
prompt.txt.setText(self._question.text) usertypes.PromptMode.user_pwd,
if self._question.default: usertypes.PromptMode.download]:
prompt.lineedit.setText(self._question.default)
prompt.lineedit.show()
elif self._question.mode == usertypes.PromptMode.user_pwd:
prompt.txt.setText(self._question.text) prompt.txt.setText(self._question.text)
if self._question.default: if self._question.default:
prompt.lineedit.setText(self._question.default) prompt.lineedit.setText(self._question.default)
@ -248,6 +246,13 @@ class Prompter(QObject):
modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt, modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt,
'prompt accept') 'prompt accept')
self._question.done() self._question.done()
elif self._question.mode == usertypes.PromptMode.download:
# User just entered a path for a download.
target = usertypes.FileDownloadTarget(prompt.lineedit.text())
self._question.answer = target
modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt,
'prompt accept')
self._question.done()
elif self._question.mode == usertypes.PromptMode.yesno: elif self._question.mode == usertypes.PromptMode.yesno:
# User wants to accept the default of a yes/no question. # User wants to accept the default of a yes/no question.
self._question.answer = self._question.default self._question.answer = self._question.default
@ -287,6 +292,18 @@ class Prompter(QObject):
'prompt accept') 'prompt accept')
self._question.done() self._question.done()
@cmdutils.register(instance='prompter', hide=True, scope='window',
modes=[usertypes.KeyMode.prompt])
def prompt_open_download(self):
"""Immediately open a download."""
if self._question.mode != usertypes.PromptMode.download:
# We just ignore this if we don't have a download question.
return
self._question.answer = usertypes.OpenFileDownloadTarget()
modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt,
'download open')
self._question.done()
@pyqtSlot(usertypes.Question, bool) @pyqtSlot(usertypes.Question, bool)
def ask_question(self, question, blocking): def ask_question(self, question, blocking):
"""Display a question in the statusbar. """Display a question in the statusbar.

View File

@ -221,7 +221,8 @@ class NeighborList(collections.abc.Sequence):
# The mode of a Question. # The mode of a Question.
PromptMode = enum('PromptMode', ['yesno', 'text', 'user_pwd', 'alert']) PromptMode = enum('PromptMode', ['yesno', 'text', 'user_pwd', 'alert',
'download'])
# Where to open a clicked link. # Where to open a clicked link.
@ -255,6 +256,50 @@ LoadStatus = enum('LoadStatus', ['none', 'success', 'success_https', 'error',
Backend = enum('Backend', ['QtWebKit', 'QtWebEngine']) Backend = enum('Backend', ['QtWebKit', 'QtWebEngine'])
# 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."""
def __init__(self):
# pylint: disable=super-init-not-called
pass
class Question(QObject): class Question(QObject):
"""A question asked to the user, e.g. via the status bar. """A question asked to the user, e.g. via the status bar.

View File

@ -81,14 +81,14 @@ def check_spelling():
"""Check commonly misspelled words.""" """Check commonly misspelled words."""
# Words which I often misspell # Words which I often misspell
words = {'[Bb]ehaviour', '[Qq]uitted', 'Ll]ikelyhood', '[Ss]ucessfully', words = {'[Bb]ehaviour', '[Qq]uitted', 'Ll]ikelyhood', '[Ss]ucessfully',
'[Oo]ccur[^r .]', '[Ss]eperator', '[Ee]xplicitely', '[Rr]esetted', '[Oo]ccur[^rs .]', '[Ss]eperator', '[Ee]xplicitely',
'[Aa]uxillary', '[Aa]ccidentaly', '[Aa]mbigious', '[Ll]oosly', '[Aa]uxillary', '[Aa]ccidentaly', '[Aa]mbigious', '[Ll]oosly',
'[Ii]nitialis', '[Cc]onvienence', '[Ss]imiliar', '[Uu]ncommited', '[Ii]nitialis', '[Cc]onvienence', '[Ss]imiliar', '[Uu]ncommited',
'[Rr]eproducable', '[Aa]n [Uu]ser', '[Cc]onvienience', '[Rr]eproducable', '[Aa]n [Uu]ser', '[Cc]onvienience',
'[Ww]ether', '[Pp]rogramatically', '[Ss]plitted', '[Ee]xitted', '[Ww]ether', '[Pp]rogramatically', '[Ss]plitted', '[Ee]xitted',
'[Mm]ininum', '[Rr]esett?ed', '[Rr]ecieved', '[Rr]egularily', '[Mm]ininum', '[Rr]esett?ed', '[Rr]ecieved', '[Rr]egularily',
'[Uu]nderlaying', '[Ii]nexistant', '[Ee]lipsis', 'commiting', '[Uu]nderlaying', '[Ii]nexistant', '[Ee]lipsis', 'commiting',
'existant'} 'existant', '[Rr]esetted'}
# Words which look better when splitted, but might need some fine tuning. # Words which look better when splitted, but might need some fine tuning.
words |= {'[Ww]ebelements', '[Mm]ouseevent', '[Kk]eysequence', words |= {'[Ww]ebelements', '[Mm]ouseevent', '[Kk]eysequence',

View File

@ -30,9 +30,8 @@ Feature: Downloading things from a website.
And I open data/downloads/issue1243.html And I open data/downloads/issue1243.html
And I run :hint links download And I run :hint links download
And I run :follow-hint a And I run :follow-hint a
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='qutebrowser-download' mode=<PromptMode.text: 2> text='Save file to:'>, *" in the log And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='qutebrowser-download' mode=<PromptMode.download: 5> text='Save file to:'>, *" in the log
And I run :leave-mode Then the error "Download error: No handler found for qute://!" should be shown
Then no crash should happen
Scenario: Downloading a data: link (issue 1214) Scenario: Downloading a data: link (issue 1214)
When I set completion -> download-path-suggestion to filename When I set completion -> download-path-suggestion to filename
@ -40,7 +39,7 @@ Feature: Downloading things from a website.
And I open data/downloads/issue1214.html And I open data/downloads/issue1214.html
And I run :hint links download And I run :hint links download
And I run :follow-hint a And I run :follow-hint a
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='binary blob' mode=<PromptMode.text: 2> text='Save file to:'>, *" in the log And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='binary blob' mode=<PromptMode.download: 5> text='Save file to:'>, *" in the log
And I run :leave-mode And I run :leave-mode
Then no crash should happen Then no crash should happen

View File

@ -312,7 +312,7 @@ Feature: Various utility commands.
And I open data/misc/test.pdf And I open data/misc/test.pdf
And I wait for "[qute://pdfjs/*] PDF * (PDF.js: *)" in the log And I wait for "[qute://pdfjs/*] PDF * (PDF.js: *)" in the log
And I run :jseval document.getElementById("download").click() And I run :jseval document.getElementById("download").click()
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='test.pdf' mode=<PromptMode.text: 2> text='Save file to:'>, *" in the log And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='test.pdf' mode=<PromptMode.download: 5> text='Save file to:'>, *" in the log
And I run :leave-mode And I run :leave-mode
Then no crash should happen Then no crash should happen

View File

@ -64,7 +64,7 @@ def download_should_exist(filename, tmpdir):
def download_prompt(tmpdir, quteproc, path): def download_prompt(tmpdir, quteproc, path):
full_path = path.replace('{downloaddir}', str(tmpdir)).replace('/', os.sep) full_path = path.replace('{downloaddir}', str(tmpdir)).replace('/', os.sep)
msg = ("Asking question <qutebrowser.utils.usertypes.Question " msg = ("Asking question <qutebrowser.utils.usertypes.Question "
"default={full_path!r} mode=<PromptMode.text: 2> " "default={full_path!r} mode=<PromptMode.download: 5> "
"text='Save file to:'>, *".format(full_path=full_path)) "text='Save file to:'>, *".format(full_path=full_path))
quteproc.wait_for(message=msg) quteproc.wait_for(message=msg)
quteproc.send_cmd(':leave-mode') quteproc.send_cmd(':leave-mode')

View File

@ -85,12 +85,12 @@ class FakeDownloadManager:
"""Mock browser.downloads.DownloadManager.""" """Mock browser.downloads.DownloadManager."""
def get(self, url, fileobj, **kwargs): def get(self, url, target, **kwargs):
"""Return a FakeDownloadItem instance with a fileobj. """Return a FakeDownloadItem instance with a fileobj.
The content is copied from the file the given url links to. The content is copied from the file the given url links to.
""" """
download_item = FakeDownloadItem(fileobj, name=url.path()) download_item = FakeDownloadItem(target.fileobj, name=url.path())
with open(url.path(), 'rb') as fake_url_file: with open(url.path(), 'rb') as fake_url_file:
shutil.copyfileobj(fake_url_file, download_item.fileobj) shutil.copyfileobj(fake_url_file, download_item.fileobj)
return download_item return download_item

View File

@ -0,0 +1,54 @@
# 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():
# Just make sure no error is raised, that should be enough.
usertypes.OpenFileDownloadTarget()
@pytest.mark.parametrize('obj', [
usertypes.FileDownloadTarget('foobar'),
usertypes.FileObjDownloadTarget(None),
usertypes.OpenFileDownloadTarget(),
])
def test_class_hierarchy(obj):
assert isinstance(obj, usertypes.DownloadTarget)