Merge branch 'Kingdread-open-download'
This commit is contained in:
commit
d70f3a0417
@ -32,6 +32,8 @@ Added
|
||||
Note that two former default bundings conflict with that binding, unbinding
|
||||
them via `:unbind .i` and `:unbind .o` is recommended.
|
||||
- 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
|
||||
~~~~~~~
|
||||
|
@ -936,6 +936,7 @@ How many steps to zoom out.
|
||||
|<<paste-primary,paste-primary>>|Paste the primary selection at cursor position.
|
||||
|<<prompt-accept,prompt-accept>>|Accept the current 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.
|
||||
|<<repeat-command,repeat-command>>|Repeat the last executed command.
|
||||
|<<rl-backward-char,rl-backward-char>>|Move back a character.
|
||||
@ -1160,6 +1161,10 @@ Accept the current prompt.
|
||||
=== 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.
|
||||
|
@ -47,7 +47,7 @@ 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
|
||||
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,
|
||||
networkmanager)
|
||||
from qutebrowser.mainwindow import mainwindow
|
||||
@ -436,6 +436,8 @@ def _init_modules(args, crash_handler):
|
||||
os.environ.pop('QT_WAYLAND_DISABLE_WINDOWDECORATION', None)
|
||||
_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):
|
||||
@ -708,6 +710,8 @@ class Quitter:
|
||||
not restart):
|
||||
atexit.register(shutil.rmtree, self._args.basedir,
|
||||
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
|
||||
log.destroy.debug("Deactivating message handler...")
|
||||
qInstallMessageHandler(None)
|
||||
|
@ -27,7 +27,7 @@ import zipfile
|
||||
import fnmatch
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -210,7 +210,8 @@ class HostBlocker:
|
||||
else:
|
||||
fobj = io.BytesIO()
|
||||
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)
|
||||
self._in_progress.append(download)
|
||||
download.finished.connect(
|
||||
|
@ -1267,15 +1267,23 @@ class CommandDispatcher:
|
||||
" as mhtml.")
|
||||
url = urlutils.qurl_from_user_input(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_:
|
||||
self._download_mhtml(dest)
|
||||
else:
|
||||
# FIXME:qtwebengine have a proper API for this
|
||||
tab = self._current_widget()
|
||||
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,
|
||||
filename=dest)
|
||||
target=target)
|
||||
|
||||
def _download_mhtml(self, dest=None):
|
||||
"""Download the current page as an MHTML file, including all assets.
|
||||
|
@ -25,6 +25,7 @@ import sys
|
||||
import os.path
|
||||
import shutil
|
||||
import functools
|
||||
import tempfile
|
||||
import collections
|
||||
|
||||
import sip
|
||||
@ -280,6 +281,7 @@ class DownloadItem(QObject):
|
||||
_read_timer: A Timer which reads the QNetworkReply into self._buffer
|
||||
periodically.
|
||||
_win_id: The window ID the DownloadItem runs in.
|
||||
_dead: Whether the Download has _die()'d.
|
||||
|
||||
Signals:
|
||||
data_changed: The downloads metadata changed.
|
||||
@ -328,6 +330,7 @@ class DownloadItem(QObject):
|
||||
self.init_reply(reply)
|
||||
self._win_id = win_id
|
||||
self.raw_headers = {}
|
||||
self._dead = False
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, basename=self.basename)
|
||||
@ -395,6 +398,21 @@ class DownloadItem(QObject):
|
||||
def _die(self, msg):
|
||||
"""Abort the download and emit an error."""
|
||||
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.reply.downloadProgress.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
|
||||
# singleShot QTimer to emit them after they are connected.
|
||||
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):
|
||||
"""Choose an appropriate color for presenting the download's status.
|
||||
@ -513,7 +531,13 @@ class DownloadItem(QObject):
|
||||
def open_file(self):
|
||||
"""Open the downloaded file."""
|
||||
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)
|
||||
|
||||
def set_filename(self, filename):
|
||||
@ -738,6 +762,9 @@ class DownloadManager(QAbstractListModel):
|
||||
def _postprocess_question(self, q):
|
||||
"""Postprocess a Question object that is asked."""
|
||||
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)
|
||||
|
||||
@pyqtSlot()
|
||||
@ -757,10 +784,7 @@ class DownloadManager(QAbstractListModel):
|
||||
**kwargs: passed to get_request().
|
||||
|
||||
Return:
|
||||
If the download could start immediately, (fileobj/filename given),
|
||||
the created DownloadItem.
|
||||
|
||||
If not, None.
|
||||
The created DownloadItem.
|
||||
"""
|
||||
if not url.isValid():
|
||||
urlutils.invalid_url_error(self._win_id, url, "start download")
|
||||
@ -768,27 +792,17 @@ class DownloadManager(QAbstractListModel):
|
||||
req = QNetworkRequest(url)
|
||||
return self.get_request(req, **kwargs)
|
||||
|
||||
def get_request(self, request, *, fileobj=None, filename=None,
|
||||
prompt_download_directory=None, **kwargs):
|
||||
def get_request(self, request, *, target=None, **kwargs):
|
||||
"""Start a download with a QNetworkRequest.
|
||||
|
||||
Args:
|
||||
request: The QNetworkRequest to download.
|
||||
fileobj: The file object to write the answer to.
|
||||
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.
|
||||
target: Where to save the download as usertypes.DownloadTarget.
|
||||
**kwargs: Passed to fetch_request.
|
||||
|
||||
Return:
|
||||
If the download could start immediately, (fileobj/filename given),
|
||||
the created DownloadItem.
|
||||
|
||||
If not, None.
|
||||
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!")
|
||||
# WORKAROUND for Qt corrupting data loaded from cache:
|
||||
# https://bugreports.qt.io/browse/QTBUG-42757
|
||||
request.setAttribute(QNetworkRequest.CacheLoadControlAttribute,
|
||||
@ -816,27 +830,10 @@ class DownloadManager(QAbstractListModel):
|
||||
if suggested_fn is None:
|
||||
suggested_fn = 'qutebrowser-download'
|
||||
|
||||
# We won't need a question if a filename or fileobj is already given
|
||||
if fileobj is None and filename is None:
|
||||
filename, q = ask_for_filename(
|
||||
suggested_fn, self._win_id, parent=self,
|
||||
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
|
||||
return self.fetch_request(request,
|
||||
target=target,
|
||||
suggested_filename=suggested_fn,
|
||||
**kwargs)
|
||||
|
||||
def fetch_request(self, request, *, page=None, **kwargs):
|
||||
"""Download a QNetworkRequest to disk.
|
||||
@ -857,27 +854,25 @@ class DownloadManager(QAbstractListModel):
|
||||
return self.fetch(reply, **kwargs)
|
||||
|
||||
@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):
|
||||
"""Download a QNetworkReply to disk.
|
||||
|
||||
Args:
|
||||
reply: The QNetworkReply to download.
|
||||
fileobj: The file object to write the answer to.
|
||||
filename: A path to write the data to.
|
||||
target: Where to save the download as usertypes.DownloadTarget.
|
||||
auto_remove: Whether to remove the download even if
|
||||
ui -> remove-finished-downloads is set to -1.
|
||||
|
||||
Return:
|
||||
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 filename is not None:
|
||||
suggested_filename = os.path.basename(filename)
|
||||
elif fileobj is not None and getattr(fileobj, 'name', None):
|
||||
suggested_filename = fileobj.name
|
||||
if isinstance(target, usertypes.FileDownloadTarget):
|
||||
suggested_filename = os.path.basename(target.filename)
|
||||
elif (isinstance(target, usertypes.FileObjDownloadTarget) and
|
||||
getattr(target.fileobj, 'name', None)):
|
||||
suggested_filename = target.fileobj.name
|
||||
else:
|
||||
_, suggested_filename = http.parse_content_disposition(reply)
|
||||
log.downloads.debug("fetch: {} -> {}".format(reply.url(),
|
||||
@ -909,13 +904,8 @@ class DownloadManager(QAbstractListModel):
|
||||
if not self._update_timer.isActive():
|
||||
self._update_timer.start()
|
||||
|
||||
if fileobj is not None:
|
||||
download.set_fileobj(fileobj)
|
||||
download.autoclose = False
|
||||
return download
|
||||
|
||||
if filename is not None:
|
||||
download.set_filename(filename)
|
||||
if target is not None:
|
||||
self._set_download_target(download, suggested_filename, target)
|
||||
return download
|
||||
|
||||
# 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
|
||||
if filename is not None:
|
||||
download.set_filename(filename)
|
||||
target = usertypes.FileDownloadTarget(filename)
|
||||
self._set_download_target(download, suggested_filename, target)
|
||||
return download
|
||||
|
||||
# Ask the user for a filename
|
||||
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)
|
||||
download.cancelled.connect(q.abort)
|
||||
download.error.connect(q.abort)
|
||||
@ -939,6 +932,28 @@ class DownloadManager(QAbstractListModel):
|
||||
|
||||
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):
|
||||
"""Raise an exception that the download doesn't exist.
|
||||
|
||||
@ -1249,3 +1264,59 @@ class DownloadManager(QAbstractListModel):
|
||||
The number of unfinished downloads.
|
||||
"""
|
||||
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
|
||||
|
@ -343,7 +343,8 @@ class _Downloader:
|
||||
|
||||
download_manager = objreg.get('download-manager', scope='window',
|
||||
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)
|
||||
self.pending_downloads.add((url, item))
|
||||
item.finished.connect(functools.partial(self._finished, url, item))
|
||||
|
@ -1574,6 +1574,7 @@ KEY_DATA = collections.OrderedDict([
|
||||
('prompt-accept', RETURN_KEYS),
|
||||
('prompt-yes', ['y']),
|
||||
('prompt-no', ['n']),
|
||||
('prompt-open-download', ['<Ctrl-X>']),
|
||||
])),
|
||||
|
||||
('command,prompt', collections.OrderedDict([
|
||||
|
@ -80,6 +80,7 @@ class Prompter(QObject):
|
||||
usertypes.PromptMode.text: usertypes.KeyMode.prompt,
|
||||
usertypes.PromptMode.user_pwd: usertypes.KeyMode.prompt,
|
||||
usertypes.PromptMode.alert: usertypes.KeyMode.prompt,
|
||||
usertypes.PromptMode.download: usertypes.KeyMode.prompt,
|
||||
}
|
||||
|
||||
show_prompt = pyqtSignal()
|
||||
@ -164,12 +165,9 @@ class Prompter(QObject):
|
||||
suffix = " (no)"
|
||||
prompt.txt.setText(self._question.text + suffix)
|
||||
prompt.lineedit.hide()
|
||||
elif self._question.mode == usertypes.PromptMode.text:
|
||||
prompt.txt.setText(self._question.text)
|
||||
if self._question.default:
|
||||
prompt.lineedit.setText(self._question.default)
|
||||
prompt.lineedit.show()
|
||||
elif self._question.mode == usertypes.PromptMode.user_pwd:
|
||||
elif self._question.mode in [usertypes.PromptMode.text,
|
||||
usertypes.PromptMode.user_pwd,
|
||||
usertypes.PromptMode.download]:
|
||||
prompt.txt.setText(self._question.text)
|
||||
if 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,
|
||||
'prompt accept')
|
||||
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:
|
||||
# User wants to accept the default of a yes/no question.
|
||||
self._question.answer = self._question.default
|
||||
@ -287,6 +292,18 @@ class Prompter(QObject):
|
||||
'prompt accept')
|
||||
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)
|
||||
def ask_question(self, question, blocking):
|
||||
"""Display a question in the statusbar.
|
||||
|
@ -221,7 +221,8 @@ class NeighborList(collections.abc.Sequence):
|
||||
|
||||
|
||||
# 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.
|
||||
@ -255,6 +256,50 @@ LoadStatus = enum('LoadStatus', ['none', 'success', 'success_https', 'error',
|
||||
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):
|
||||
|
||||
"""A question asked to the user, e.g. via the status bar.
|
||||
|
@ -81,14 +81,14 @@ def check_spelling():
|
||||
"""Check commonly misspelled words."""
|
||||
# Words which I often misspell
|
||||
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',
|
||||
'[Ii]nitialis', '[Cc]onvienence', '[Ss]imiliar', '[Uu]ncommited',
|
||||
'[Rr]eproducable', '[Aa]n [Uu]ser', '[Cc]onvienience',
|
||||
'[Ww]ether', '[Pp]rogramatically', '[Ss]plitted', '[Ee]xitted',
|
||||
'[Mm]ininum', '[Rr]esett?ed', '[Rr]ecieved', '[Rr]egularily',
|
||||
'[Uu]nderlaying', '[Ii]nexistant', '[Ee]lipsis', 'commiting',
|
||||
'existant'}
|
||||
'existant', '[Rr]esetted'}
|
||||
|
||||
# Words which look better when splitted, but might need some fine tuning.
|
||||
words |= {'[Ww]ebelements', '[Mm]ouseevent', '[Kk]eysequence',
|
||||
|
@ -30,9 +30,8 @@ Feature: Downloading things from a website.
|
||||
And I open data/downloads/issue1243.html
|
||||
And I run :hint links download
|
||||
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 run :leave-mode
|
||||
Then no crash should happen
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='qutebrowser-download' mode=<PromptMode.download: 5> text='Save file to:'>, *" in the log
|
||||
Then the error "Download error: No handler found for qute://!" should be shown
|
||||
|
||||
Scenario: Downloading a data: link (issue 1214)
|
||||
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 run :hint links download
|
||||
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
|
||||
Then no crash should happen
|
||||
|
||||
|
@ -312,7 +312,7 @@ Feature: Various utility commands.
|
||||
And I open data/misc/test.pdf
|
||||
And I wait for "[qute://pdfjs/*] PDF * (PDF.js: *)" in the log
|
||||
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
|
||||
Then no crash should happen
|
||||
|
||||
|
@ -64,7 +64,7 @@ def download_should_exist(filename, tmpdir):
|
||||
def download_prompt(tmpdir, quteproc, path):
|
||||
full_path = path.replace('{downloaddir}', str(tmpdir)).replace('/', os.sep)
|
||||
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))
|
||||
quteproc.wait_for(message=msg)
|
||||
quteproc.send_cmd(':leave-mode')
|
||||
|
@ -85,12 +85,12 @@ class FakeDownloadManager:
|
||||
|
||||
"""Mock browser.downloads.DownloadManager."""
|
||||
|
||||
def get(self, url, fileobj, **kwargs):
|
||||
def get(self, url, target, **kwargs):
|
||||
"""Return a FakeDownloadItem instance with a fileobj.
|
||||
|
||||
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:
|
||||
shutil.copyfileobj(fake_url_file, download_item.fileobj)
|
||||
return download_item
|
||||
|
54
tests/unit/utils/usertypes/test_downloadtarget.py
Normal file
54
tests/unit/utils/usertypes/test_downloadtarget.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user