Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
1698c60124
@ -3,6 +3,7 @@ branch = true
|
|||||||
omit =
|
omit =
|
||||||
qutebrowser/__main__.py
|
qutebrowser/__main__.py
|
||||||
*/__init__.py
|
*/__init__.py
|
||||||
|
qutebrowser/resources.py
|
||||||
|
|
||||||
[report]
|
[report]
|
||||||
exclude_lines =
|
exclude_lines =
|
||||||
|
@ -32,6 +32,11 @@ Added
|
|||||||
- New arguments `--basedir` and `--temp-basedir` (intended for debugging) to set a different base directory for all data, which allows multiple invocations.
|
- New arguments `--basedir` and `--temp-basedir` (intended for debugging) to set a different base directory for all data, which allows multiple invocations.
|
||||||
- New argument `--no-err-windows` to suppress all error windows.
|
- New argument `--no-err-windows` to suppress all error windows.
|
||||||
- New visual/caret mode (bound to `v`) to select text by keyboard.
|
- New visual/caret mode (bound to `v`) to select text by keyboard.
|
||||||
|
- New setting `tabs -> mousewheel-tab-switching` to control mousewheel behavior on the tab bar.
|
||||||
|
- New arguments `--top-navigate` and `--bottom-navigate` (`-t`/`-b`) for `:scroll-page` to specify a navigation action (e.g. automatically go to the next page when arriving at the bottom).
|
||||||
|
- New flag `-d`/`--detach` for `:spawn` to detach the spawned process so it's not closed when qutebrowser is.
|
||||||
|
- New (hidden) command `:follow-selected` (bound to `Enter`/`Ctrl-Enter` by default) to follow the link which is currently selected (e.g. after searching via `/`).
|
||||||
|
- New setting `ui -> modal-js-dialog` to use the standard modal dialogs for javascript questions instead of using the statusbar.
|
||||||
|
|
||||||
Changed
|
Changed
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
@ -73,6 +78,7 @@ Fixed
|
|||||||
- Various fixes for deprecated key bindings and auto-migrations.
|
- Various fixes for deprecated key bindings and auto-migrations.
|
||||||
- Workaround for qutebrowser not starting when there are NUL-bytes in the history (because of a currently unknown bug)
|
- Workaround for qutebrowser not starting when there are NUL-bytes in the history (because of a currently unknown bug)
|
||||||
- Fixed handling of keybindings containing Ctrl/Meta on OS X.
|
- Fixed handling of keybindings containing Ctrl/Meta on OS X.
|
||||||
|
- Fixed crash when downloading an URL without filename (e.g. magnet links) via "Save as...".
|
||||||
|
|
||||||
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.1[v0.2.1]
|
https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.1[v0.2.1]
|
||||||
-----------------------------------------------------------------------
|
-----------------------------------------------------------------------
|
||||||
|
@ -86,7 +86,7 @@ Useful utilities
|
|||||||
Checkers
|
Checkers
|
||||||
~~~~~~~~
|
~~~~~~~~
|
||||||
|
|
||||||
qutbebrowser uses http://tox.readthedocs.org/en/latest/[tox] to run its
|
qutebrowser uses http://tox.readthedocs.org/en/latest/[tox] to run its
|
||||||
unittests and several linters/checkers.
|
unittests and several linters/checkers.
|
||||||
|
|
||||||
Currently, the following tools will be invoked when you run `tox`:
|
Currently, the following tools will be invoked when you run `tox`:
|
||||||
|
@ -139,7 +139,9 @@ Contributors, sorted by the number of commits in descending order:
|
|||||||
* Joel Torstensson
|
* Joel Torstensson
|
||||||
* Claude
|
* Claude
|
||||||
* Artur Shaik
|
* Artur Shaik
|
||||||
|
* Antoni Boucher
|
||||||
* ZDarian
|
* ZDarian
|
||||||
|
* Martin Tournoij
|
||||||
* Peter Vilim
|
* Peter Vilim
|
||||||
* John ShaggyTwoDope Jenkins
|
* John ShaggyTwoDope Jenkins
|
||||||
* Jimmy
|
* Jimmy
|
||||||
@ -150,6 +152,7 @@ Contributors, sorted by the number of commits in descending order:
|
|||||||
* Error 800
|
* Error 800
|
||||||
* Brian Jackson
|
* Brian Jackson
|
||||||
* sbinix
|
* sbinix
|
||||||
|
* Tobias Patzl
|
||||||
* Johannes Altmanninger
|
* Johannes Altmanninger
|
||||||
* Samir Benmendil
|
* Samir Benmendil
|
||||||
* Regina Hug
|
* Regina Hug
|
||||||
|
@ -692,6 +692,7 @@ How many steps to zoom out.
|
|||||||
|<<drop-selection,drop-selection>>|Drop selection and keep selection mode enabled.
|
|<<drop-selection,drop-selection>>|Drop selection and keep selection mode enabled.
|
||||||
|<<enter-mode,enter-mode>>|Enter a key mode.
|
|<<enter-mode,enter-mode>>|Enter a key mode.
|
||||||
|<<follow-hint,follow-hint>>|Follow the currently selected hint.
|
|<<follow-hint,follow-hint>>|Follow the currently selected hint.
|
||||||
|
|<<follow-selected,follow-selected>>|Follow the selected text.
|
||||||
|<<leave-mode,leave-mode>>|Leave the mode we're currently in.
|
|<<leave-mode,leave-mode>>|Leave the mode we're currently in.
|
||||||
|<<message-error,message-error>>|Show an error message in the statusbar.
|
|<<message-error,message-error>>|Show an error message in the statusbar.
|
||||||
|<<message-info,message-info>>|Show an info message in the statusbar.
|
|<<message-info,message-info>>|Show an info message in the statusbar.
|
||||||
@ -774,6 +775,15 @@ Enter a key mode.
|
|||||||
=== follow-hint
|
=== follow-hint
|
||||||
Follow the currently selected hint.
|
Follow the currently selected hint.
|
||||||
|
|
||||||
|
[[follow-selected]]
|
||||||
|
=== follow-selected
|
||||||
|
Syntax: +:follow-selected [*--tab*]+
|
||||||
|
|
||||||
|
Follow the selected text.
|
||||||
|
|
||||||
|
==== optional arguments
|
||||||
|
* +*-t*+, +*--tab*+: Load the selected link in a new tab.
|
||||||
|
|
||||||
[[leave-mode]]
|
[[leave-mode]]
|
||||||
=== leave-mode
|
=== leave-mode
|
||||||
Leave the mode we're currently in.
|
Leave the mode we're currently in.
|
||||||
@ -1009,7 +1019,7 @@ multiplier
|
|||||||
|
|
||||||
[[scroll-page]]
|
[[scroll-page]]
|
||||||
=== scroll-page
|
=== scroll-page
|
||||||
Syntax: +:scroll-page 'x' 'y'+
|
Syntax: +:scroll-page [*--top-navigate* 'ACTION'] [*--bottom-navigate* 'ACTION'] 'x' 'y'+
|
||||||
|
|
||||||
Scroll the frame page-wise.
|
Scroll the frame page-wise.
|
||||||
|
|
||||||
@ -1017,6 +1027,12 @@ Scroll the frame page-wise.
|
|||||||
* +'x'+: How many pages to scroll to the right.
|
* +'x'+: How many pages to scroll to the right.
|
||||||
* +'y'+: How many pages to scroll down.
|
* +'y'+: How many pages to scroll down.
|
||||||
|
|
||||||
|
==== optional arguments
|
||||||
|
* +*-t*+, +*--top-navigate*+: :navigate action (prev, decrement) to run when scrolling up at the top of the page.
|
||||||
|
|
||||||
|
* +*-b*+, +*--bottom-navigate*+: :navigate action (next, increment) to run when scrolling down at the bottom of the page.
|
||||||
|
|
||||||
|
|
||||||
==== count
|
==== count
|
||||||
multiplier
|
multiplier
|
||||||
|
|
||||||
|
@ -45,6 +45,7 @@
|
|||||||
|<<ui-hide-statusbar,hide-statusbar>>|Whether to hide the statusbar unless a message is shown.
|
|<<ui-hide-statusbar,hide-statusbar>>|Whether to hide the statusbar unless a message is shown.
|
||||||
|<<ui-window-title-format,window-title-format>>|The format to use for the window title. The following placeholders are defined:
|
|<<ui-window-title-format,window-title-format>>|The format to use for the window title. The following placeholders are defined:
|
||||||
|<<ui-hide-mouse-cursor,hide-mouse-cursor>>|Whether to hide the mouse cursor.
|
|<<ui-hide-mouse-cursor,hide-mouse-cursor>>|Whether to hide the mouse cursor.
|
||||||
|
|<<ui-modal-js-dialog,modal-js-dialog>>|Use standard JavaScript modal dialog for alert() and confirm()
|
||||||
|==============
|
|==============
|
||||||
|
|
||||||
.Quick reference for section ``network''
|
.Quick reference for section ``network''
|
||||||
@ -111,6 +112,7 @@
|
|||||||
|<<tabs-indicator-space,indicator-space>>|Spacing between tab edge and indicator.
|
|<<tabs-indicator-space,indicator-space>>|Spacing between tab edge and indicator.
|
||||||
|<<tabs-tabs-are-windows,tabs-are-windows>>|Whether to open windows instead of tabs.
|
|<<tabs-tabs-are-windows,tabs-are-windows>>|Whether to open windows instead of tabs.
|
||||||
|<<tabs-title-format,title-format>>|The format to use for the tab title. The following placeholders are defined:
|
|<<tabs-title-format,title-format>>|The format to use for the tab title. The following placeholders are defined:
|
||||||
|
|<<tabs-mousewheel-tab-switching,mousewheel-tab-switching>>|Switch between tabs using the mouse wheel.
|
||||||
|==============
|
|==============
|
||||||
|
|
||||||
.Quick reference for section ``storage''
|
.Quick reference for section ``storage''
|
||||||
@ -593,6 +595,17 @@ Valid values:
|
|||||||
|
|
||||||
Default: +pass:[false]+
|
Default: +pass:[false]+
|
||||||
|
|
||||||
|
[[ui-modal-js-dialog]]
|
||||||
|
=== modal-js-dialog
|
||||||
|
Use standard JavaScript modal dialog for alert() and confirm()
|
||||||
|
|
||||||
|
Valid values:
|
||||||
|
|
||||||
|
* +true+
|
||||||
|
* +false+
|
||||||
|
|
||||||
|
Default: +pass:[false]+
|
||||||
|
|
||||||
== network
|
== network
|
||||||
Settings related to the network.
|
Settings related to the network.
|
||||||
|
|
||||||
@ -1031,6 +1044,17 @@ The format to use for the tab title. The following placeholders are defined:
|
|||||||
|
|
||||||
Default: +pass:[{index}: {title}]+
|
Default: +pass:[{index}: {title}]+
|
||||||
|
|
||||||
|
[[tabs-mousewheel-tab-switching]]
|
||||||
|
=== mousewheel-tab-switching
|
||||||
|
Switch between tabs using the mouse wheel.
|
||||||
|
|
||||||
|
Valid values:
|
||||||
|
|
||||||
|
* +true+
|
||||||
|
* +false+
|
||||||
|
|
||||||
|
Default: +pass:[true]+
|
||||||
|
|
||||||
== storage
|
== storage
|
||||||
Settings related to cache and storage.
|
Settings related to cache and storage.
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ What to do now
|
|||||||
* View the http://qutebrowser.org/img/cheatsheet-big.png[key binding cheatsheet]
|
* View the http://qutebrowser.org/img/cheatsheet-big.png[key binding cheatsheet]
|
||||||
to make yourself familiar with the key bindings: +
|
to make yourself familiar with the key bindings: +
|
||||||
image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="http://qutebrowser.org/img/cheatsheet-big.png"]
|
image:http://qutebrowser.org/img/cheatsheet-small.png["qutebrowser key binding cheatsheet",link="http://qutebrowser.org/img/cheatsheet-big.png"]
|
||||||
|
* Run `:adblock-update` to download adblock lists and activate adblocking.
|
||||||
* If you just cloned the repository, you'll need to run
|
* If you just cloned the repository, you'll need to run
|
||||||
`scripts/asciidoc2html.py` to generate the documentation.
|
`scripts/asciidoc2html.py` to generate the documentation.
|
||||||
* Go to the link:qute://settings[settings page] to set up qutebrowser the way you want it.
|
* Go to the link:qute://settings[settings page] to set up qutebrowser the way you want it.
|
||||||
|
@ -71,7 +71,7 @@ def run(args):
|
|||||||
sys.exit(usertypes.Exit.ok)
|
sys.exit(usertypes.Exit.ok)
|
||||||
|
|
||||||
if args.temp_basedir:
|
if args.temp_basedir:
|
||||||
args.basedir = tempfile.mkdtemp()
|
args.basedir = tempfile.mkdtemp(prefix='qutebrowser-basedir-')
|
||||||
|
|
||||||
quitter = Quitter(args)
|
quitter = Quitter(args)
|
||||||
objreg.register('quitter', quitter)
|
objreg.register('quitter', quitter)
|
||||||
|
@ -25,7 +25,9 @@ import shlex
|
|||||||
import subprocess
|
import subprocess
|
||||||
import posixpath
|
import posixpath
|
||||||
import functools
|
import functools
|
||||||
|
import xml.etree.ElementTree
|
||||||
|
|
||||||
|
from PyQt5.QtWebKit import QWebSettings
|
||||||
from PyQt5.QtWidgets import QApplication, QTabBar
|
from PyQt5.QtWidgets import QApplication, QTabBar
|
||||||
from PyQt5.QtCore import Qt, QUrl, QEvent
|
from PyQt5.QtCore import Qt, QUrl, QEvent
|
||||||
from PyQt5.QtGui import QClipboard, QKeyEvent
|
from PyQt5.QtGui import QClipboard, QKeyEvent
|
||||||
@ -647,14 +649,37 @@ class CommandDispatcher:
|
|||||||
|
|
||||||
@cmdutils.register(instance='command-dispatcher', hide=True,
|
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||||
scope='window', count='count')
|
scope='window', count='count')
|
||||||
def scroll_page(self, x: {'type': float}, y: {'type': float}, count=1):
|
def scroll_page(self, x: {'type': float}, y: {'type': float}, *,
|
||||||
|
top_navigate: {'type': ('prev', 'decrement'),
|
||||||
|
'metavar': 'ACTION'}=None,
|
||||||
|
bottom_navigate: {'type': ('next', 'increment'),
|
||||||
|
'metavar': 'ACTION'}=None,
|
||||||
|
count=1):
|
||||||
"""Scroll the frame page-wise.
|
"""Scroll the frame page-wise.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
x: How many pages to scroll to the right.
|
x: How many pages to scroll to the right.
|
||||||
y: How many pages to scroll down.
|
y: How many pages to scroll down.
|
||||||
|
bottom_navigate: :navigate action (next, increment) to run when
|
||||||
|
scrolling down at the bottom of the page.
|
||||||
|
top_navigate: :navigate action (prev, decrement) to run when
|
||||||
|
scrolling up at the top of the page.
|
||||||
count: multiplier
|
count: multiplier
|
||||||
"""
|
"""
|
||||||
|
frame = self._current_widget().page().currentFrame()
|
||||||
|
if not frame.url().isValid():
|
||||||
|
# See https://github.com/The-Compiler/qutebrowser/issues/701
|
||||||
|
return
|
||||||
|
|
||||||
|
if (bottom_navigate is not None and
|
||||||
|
frame.scrollPosition().y() >=
|
||||||
|
frame.scrollBarMaximum(Qt.Vertical)):
|
||||||
|
self.navigate(bottom_navigate)
|
||||||
|
return
|
||||||
|
elif top_navigate is not None and frame.scrollPosition().y() == 0:
|
||||||
|
self.navigate(top_navigate)
|
||||||
|
return
|
||||||
|
|
||||||
mult_x = count * x
|
mult_x = count * x
|
||||||
mult_y = count * y
|
mult_y = count * y
|
||||||
if mult_y.is_integer():
|
if mult_y.is_integer():
|
||||||
@ -667,7 +692,6 @@ class CommandDispatcher:
|
|||||||
mult_y = 0
|
mult_y = 0
|
||||||
if mult_x == 0 and mult_y == 0:
|
if mult_x == 0 and mult_y == 0:
|
||||||
return
|
return
|
||||||
frame = self._current_widget().page().currentFrame()
|
|
||||||
size = frame.geometry()
|
size = frame.geometry()
|
||||||
dx = mult_x * size.width()
|
dx = mult_x * size.width()
|
||||||
dy = mult_y * size.height()
|
dy = mult_y * size.height()
|
||||||
@ -1010,6 +1034,39 @@ class CommandDispatcher:
|
|||||||
"""
|
"""
|
||||||
self._open(QUrl(url), tab, bg, window)
|
self._open(QUrl(url), tab, bg, window)
|
||||||
|
|
||||||
|
@cmdutils.register(instance='command-dispatcher', hide=True,
|
||||||
|
scope='window')
|
||||||
|
def follow_selected(self, tab=False):
|
||||||
|
"""Follow the selected text.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tab: Load the selected link in a new tab.
|
||||||
|
"""
|
||||||
|
widget = self._current_widget()
|
||||||
|
page = widget.page()
|
||||||
|
if not page.hasSelection():
|
||||||
|
return
|
||||||
|
if QWebSettings.globalSettings().testAttribute(
|
||||||
|
QWebSettings.JavascriptEnabled):
|
||||||
|
if tab:
|
||||||
|
page.open_target = usertypes.ClickTarget.tab
|
||||||
|
page.currentFrame().evaluateJavaScript(
|
||||||
|
'window.getSelection().anchorNode.parentNode.click()')
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
selected_element = xml.etree.ElementTree.fromstring(
|
||||||
|
'<html>' + widget.selectedHtml() + '</html>').find('a')
|
||||||
|
except xml.etree.ElementTree.ParseError:
|
||||||
|
raise cmdexc.CommandError('Could not parse selected element!')
|
||||||
|
|
||||||
|
if selected_element is not None:
|
||||||
|
try:
|
||||||
|
url = selected_element.attrib['href']
|
||||||
|
except KeyError:
|
||||||
|
raise cmdexc.CommandError('Anchor element without href!')
|
||||||
|
url = self._current_url().resolved(QUrl(url))
|
||||||
|
self._open(url, tab)
|
||||||
|
|
||||||
@cmdutils.register(instance='command-dispatcher', name='inspector',
|
@cmdutils.register(instance='command-dispatcher', name='inspector',
|
||||||
scope='window')
|
scope='window')
|
||||||
def toggle_inspector(self):
|
def toggle_inspector(self):
|
||||||
|
@ -686,8 +686,11 @@ class DownloadManager(QAbstractListModel):
|
|||||||
if fileobj is not None or filename is not None:
|
if fileobj is not None or filename is not None:
|
||||||
return self.fetch_request(request, page, fileobj, filename,
|
return self.fetch_request(request, page, fileobj, filename,
|
||||||
auto_remove, suggested_fn)
|
auto_remove, suggested_fn)
|
||||||
encoding = sys.getfilesystemencoding()
|
if suggested_fn is None:
|
||||||
suggested_fn = utils.force_encoding(suggested_fn, encoding)
|
suggested_fn = 'qutebrowser-download'
|
||||||
|
else:
|
||||||
|
encoding = sys.getfilesystemencoding()
|
||||||
|
suggested_fn = utils.force_encoding(suggested_fn, encoding)
|
||||||
q = self._prepare_question()
|
q = self._prepare_question()
|
||||||
q.default = _path_suggestion(suggested_fn)
|
q.default = _path_suggestion(suggested_fn)
|
||||||
message_bridge = objreg.get('message-bridge', scope='window',
|
message_bridge = objreg.get('message-bridge', scope='window',
|
||||||
|
@ -478,17 +478,23 @@ class BrowserPage(QWebPage):
|
|||||||
return super().extension(ext, opt, out)
|
return super().extension(ext, opt, out)
|
||||||
return handler(opt, out)
|
return handler(opt, out)
|
||||||
|
|
||||||
def javaScriptAlert(self, _frame, msg):
|
def javaScriptAlert(self, frame, msg):
|
||||||
"""Override javaScriptAlert to use the statusbar."""
|
"""Override javaScriptAlert to use the statusbar."""
|
||||||
log.js.debug("alert: {}".format(msg))
|
log.js.debug("alert: {}".format(msg))
|
||||||
|
if config.get('ui', 'modal-js-dialog'):
|
||||||
|
return super().javaScriptAlert(frame, msg)
|
||||||
|
|
||||||
if (self._is_shutting_down or
|
if (self._is_shutting_down or
|
||||||
config.get('content', 'ignore-javascript-alert')):
|
config.get('content', 'ignore-javascript-alert')):
|
||||||
return
|
return
|
||||||
self._ask("[js alert] {}".format(msg), usertypes.PromptMode.alert)
|
self._ask("[js alert] {}".format(msg), usertypes.PromptMode.alert)
|
||||||
|
|
||||||
def javaScriptConfirm(self, _frame, msg):
|
def javaScriptConfirm(self, frame, msg):
|
||||||
"""Override javaScriptConfirm to use the statusbar."""
|
"""Override javaScriptConfirm to use the statusbar."""
|
||||||
log.js.debug("confirm: {}".format(msg))
|
log.js.debug("confirm: {}".format(msg))
|
||||||
|
if config.get('ui', 'modal-js-dialog'):
|
||||||
|
return super().javaScriptConfirm(frame, msg)
|
||||||
|
|
||||||
if self._is_shutting_down:
|
if self._is_shutting_down:
|
||||||
return False
|
return False
|
||||||
ans = self._ask("[js confirm] {}".format(msg),
|
ans = self._ask("[js confirm] {}".format(msg),
|
||||||
|
@ -487,12 +487,25 @@ class WebView(QWebView):
|
|||||||
old_scroll_pos = self.scroll_pos
|
old_scroll_pos = self.scroll_pos
|
||||||
flags = QWebPage.FindFlags(flags)
|
flags = QWebPage.FindFlags(flags)
|
||||||
found = self.findText(text, flags)
|
found = self.findText(text, flags)
|
||||||
if not found and not flags & QWebPage.HighlightAllOccurrences and text:
|
backward = flags & QWebPage.FindBackward
|
||||||
message.error(self.win_id, "Text '{}' not found on "
|
|
||||||
"page!".format(text), immediately=True)
|
|
||||||
else:
|
|
||||||
backward = int(flags) & QWebPage.FindBackward
|
|
||||||
|
|
||||||
|
if not found and not flags & QWebPage.HighlightAllOccurrences and text:
|
||||||
|
# User disabled wrapping; but findText() just returns False. If we
|
||||||
|
# have a selection, we know there's a match *somewhere* on the page
|
||||||
|
if (not flags & QWebPage.FindWrapsAroundDocument and
|
||||||
|
self.hasSelection()):
|
||||||
|
if not backward:
|
||||||
|
message.warning(self.win_id, "Search hit BOTTOM without "
|
||||||
|
"match for: {}".format(text),
|
||||||
|
immediately=True)
|
||||||
|
else:
|
||||||
|
message.warning(self.win_id, "Search hit TOP without "
|
||||||
|
"match for: {}".format(text),
|
||||||
|
immediately=True)
|
||||||
|
else:
|
||||||
|
message.error(self.win_id, "Text '{}' not found on "
|
||||||
|
"page!".format(text), immediately=True)
|
||||||
|
else:
|
||||||
def check_scroll_pos():
|
def check_scroll_pos():
|
||||||
"""Check if the scroll position got smaller and show info."""
|
"""Check if the scroll position got smaller and show info."""
|
||||||
if not backward and self.scroll_pos < old_scroll_pos:
|
if not backward and self.scroll_pos < old_scroll_pos:
|
||||||
|
@ -61,7 +61,8 @@ class Command:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
AnnotationInfo = collections.namedtuple('AnnotationInfo',
|
AnnotationInfo = collections.namedtuple('AnnotationInfo',
|
||||||
['kwargs', 'type', 'flag', 'hide'])
|
['kwargs', 'type', 'flag', 'hide',
|
||||||
|
'metavar'])
|
||||||
|
|
||||||
def __init__(self, *, handler, name, instance=None, maxsplit=None,
|
def __init__(self, *, handler, name, instance=None, maxsplit=None,
|
||||||
hide=False, completion=None, modes=None, not_modes=None,
|
hide=False, completion=None, modes=None, not_modes=None,
|
||||||
@ -257,10 +258,10 @@ class Command:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
if isinstance(typ, tuple):
|
if isinstance(typ, tuple):
|
||||||
pass
|
kwargs['metavar'] = annotation_info.metavar or param.name
|
||||||
elif utils.is_enum(typ):
|
elif utils.is_enum(typ):
|
||||||
kwargs['choices'] = [e.name.replace('_', '-') for e in typ]
|
kwargs['choices'] = [e.name.replace('_', '-') for e in typ]
|
||||||
kwargs['metavar'] = param.name
|
kwargs['metavar'] = annotation_info.metavar or param.name
|
||||||
elif typ is bool:
|
elif typ is bool:
|
||||||
kwargs['action'] = 'store_true'
|
kwargs['action'] = 'store_true'
|
||||||
elif typ is not None:
|
elif typ is not None:
|
||||||
@ -287,7 +288,7 @@ class Command:
|
|||||||
A list of args.
|
A list of args.
|
||||||
"""
|
"""
|
||||||
args = []
|
args = []
|
||||||
name = param.name.rstrip('_')
|
name = param.name.rstrip('_').replace('_', '-')
|
||||||
shortname = annotation_info.flag or name[0]
|
shortname = annotation_info.flag or name[0]
|
||||||
if len(shortname) != 1:
|
if len(shortname) != 1:
|
||||||
raise ValueError("Flag '{}' of parameter {} (command {}) must be "
|
raise ValueError("Flag '{}' of parameter {} (command {}) must be "
|
||||||
@ -322,11 +323,12 @@ class Command:
|
|||||||
flag: The short name/flag if overridden.
|
flag: The short name/flag if overridden.
|
||||||
name: The long name if overridden.
|
name: The long name if overridden.
|
||||||
"""
|
"""
|
||||||
info = {'kwargs': {}, 'type': None, 'flag': None, 'hide': False}
|
info = {'kwargs': {}, 'type': None, 'flag': None, 'hide': False,
|
||||||
|
'metavar': None}
|
||||||
if param.annotation is not inspect.Parameter.empty:
|
if param.annotation is not inspect.Parameter.empty:
|
||||||
log.commands.vdebug("Parsing annotation {}".format(
|
log.commands.vdebug("Parsing annotation {}".format(
|
||||||
param.annotation))
|
param.annotation))
|
||||||
for field in ('type', 'flag', 'name', 'hide'):
|
for field in ('type', 'flag', 'name', 'hide', 'metavar'):
|
||||||
if field in param.annotation:
|
if field in param.annotation:
|
||||||
info[field] = param.annotation[field]
|
info[field] = param.annotation[field]
|
||||||
if 'nargs' in param.annotation:
|
if 'nargs' in param.annotation:
|
||||||
|
@ -305,6 +305,10 @@ def data(readonly=False):
|
|||||||
SettingValue(typ.Bool(), 'false'),
|
SettingValue(typ.Bool(), 'false'),
|
||||||
"Whether to hide the mouse cursor."),
|
"Whether to hide the mouse cursor."),
|
||||||
|
|
||||||
|
('modal-js-dialog',
|
||||||
|
SettingValue(typ.Bool(), 'false'),
|
||||||
|
"Use standard JavaScript modal dialog for alert() and confirm()"),
|
||||||
|
|
||||||
readonly=readonly
|
readonly=readonly
|
||||||
)),
|
)),
|
||||||
|
|
||||||
@ -522,6 +526,10 @@ def data(readonly=False):
|
|||||||
"* `{index}`: The index of this tab.\n"
|
"* `{index}`: The index of this tab.\n"
|
||||||
"* `{id}`: The internal tab ID of this tab."),
|
"* `{id}`: The internal tab ID of this tab."),
|
||||||
|
|
||||||
|
('mousewheel-tab-switching',
|
||||||
|
SettingValue(typ.Bool(), 'true'),
|
||||||
|
"Switch between tabs using the mouse wheel."),
|
||||||
|
|
||||||
readonly=readonly
|
readonly=readonly
|
||||||
)),
|
)),
|
||||||
|
|
||||||
@ -1236,6 +1244,8 @@ KEY_DATA = collections.OrderedDict([
|
|||||||
('stop', ['<Ctrl-s>']),
|
('stop', ['<Ctrl-s>']),
|
||||||
('print', ['<Ctrl-Alt-p>']),
|
('print', ['<Ctrl-Alt-p>']),
|
||||||
('open qute:settings', ['Ss']),
|
('open qute:settings', ['Ss']),
|
||||||
|
('follow-selected', ['<Return>']),
|
||||||
|
('follow-selected -t', ['<Ctrl-Return>']),
|
||||||
])),
|
])),
|
||||||
|
|
||||||
('insert', collections.OrderedDict([
|
('insert', collections.OrderedDict([
|
||||||
|
@ -577,3 +577,14 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
"""
|
"""
|
||||||
super().resizeEvent(e)
|
super().resizeEvent(e)
|
||||||
self.resized.emit(self.geometry())
|
self.resized.emit(self.geometry())
|
||||||
|
|
||||||
|
def wheelEvent(self, e):
|
||||||
|
"""Override wheelEvent of QWidget to forward it to the focused tab.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
e: The QWheelEvent
|
||||||
|
"""
|
||||||
|
if self._now_focused is not None:
|
||||||
|
self._now_focused.wheelEvent(e)
|
||||||
|
else:
|
||||||
|
e.ignore()
|
||||||
|
@ -480,6 +480,19 @@ class TabBar(QTabBar):
|
|||||||
new_idx = super().insertTab(idx, icon, '')
|
new_idx = super().insertTab(idx, icon, '')
|
||||||
self.set_page_title(new_idx, text)
|
self.set_page_title(new_idx, text)
|
||||||
|
|
||||||
|
def wheelEvent(self, e):
|
||||||
|
"""Override wheelEvent to make the action configurable.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
e: The QWheelEvent
|
||||||
|
"""
|
||||||
|
if config.get('tabs', 'mousewheel-tab-switching'):
|
||||||
|
super().wheelEvent(e)
|
||||||
|
else:
|
||||||
|
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||||
|
window=self._win_id)
|
||||||
|
tabbed_browser.wheelEvent(e)
|
||||||
|
|
||||||
|
|
||||||
class TabBarStyle(QCommonStyle):
|
class TabBarStyle(QCommonStyle):
|
||||||
|
|
||||||
|
@ -122,7 +122,8 @@ class ExternalEditor(QObject):
|
|||||||
raise ValueError("Already editing a file!")
|
raise ValueError("Already editing a file!")
|
||||||
self._text = text
|
self._text = text
|
||||||
try:
|
try:
|
||||||
self._oshandle, self._filename = tempfile.mkstemp(text=True)
|
self._oshandle, self._filename = tempfile.mkstemp(
|
||||||
|
text=True, prefix='qutebrowser-editor-')
|
||||||
if text:
|
if text:
|
||||||
encoding = config.get('general', 'editor-encoding')
|
encoding = config.get('general', 'editor-encoding')
|
||||||
with open(self._filename, 'w', encoding=encoding) as f:
|
with open(self._filename, 'w', encoding=encoding) as f:
|
||||||
|
@ -269,8 +269,6 @@ def qt_message_handler(msg_type, context, msg):
|
|||||||
# https://bugreports.qt-project.org/browse/QTBUG-30298
|
# https://bugreports.qt-project.org/browse/QTBUG-30298
|
||||||
"QNetworkReplyImplPrivate::error: Internal problem, this method must "
|
"QNetworkReplyImplPrivate::error: Internal problem, this method must "
|
||||||
"only be called once.",
|
"only be called once.",
|
||||||
# Not much information about this, but it seems harmless
|
|
||||||
'QXcbWindow: Unhandled client message: "_GTK_LOAD_ICONTHEMES"',
|
|
||||||
# Sometimes indicates missing text, but most of the time harmless
|
# Sometimes indicates missing text, but most of the time harmless
|
||||||
"load glyph failed ",
|
"load glyph failed ",
|
||||||
# Harmless, see https://bugreports.qt-project.org/browse/QTBUG-42479
|
# Harmless, see https://bugreports.qt-project.org/browse/QTBUG-42479
|
||||||
@ -282,7 +280,11 @@ def qt_message_handler(msg_type, context, msg):
|
|||||||
# Hopefully harmless
|
# Hopefully harmless
|
||||||
'"Method "GetAll" with signature "s" on interface '
|
'"Method "GetAll" with signature "s" on interface '
|
||||||
'"org.freedesktop.DBus.Properties" doesn\'t exist',
|
'"org.freedesktop.DBus.Properties" doesn\'t exist',
|
||||||
'WOFF support requires QtWebKit to be built with zlib support.'
|
'WOFF support requires QtWebKit to be built with zlib support.',
|
||||||
|
# Weird Enlightment/GTK X extensions
|
||||||
|
'QXcbWindow: Unhandled client message: "_E_',
|
||||||
|
'QXcbWindow: Unhandled client message: "_ECORE_',
|
||||||
|
'QXcbWindow: Unhandled client message: "_GTK_',
|
||||||
)
|
)
|
||||||
if any(msg.strip().startswith(pattern) for pattern in suppressed_msgs):
|
if any(msg.strip().startswith(pattern) for pattern in suppressed_msgs):
|
||||||
level = logging.DEBUG
|
level = logging.DEBUG
|
||||||
|
@ -131,7 +131,7 @@ def ensure_valid(obj):
|
|||||||
def ensure_not_null(obj):
|
def ensure_not_null(obj):
|
||||||
"""Ensure a Qt object with an .isNull() method is not null."""
|
"""Ensure a Qt object with an .isNull() method is not null."""
|
||||||
if obj.isNull():
|
if obj.isNull():
|
||||||
raise QtValueError(obj)
|
raise QtValueError(obj, null=True)
|
||||||
|
|
||||||
|
|
||||||
def check_qdatastream(stream):
|
def check_qdatastream(stream):
|
||||||
@ -180,7 +180,7 @@ def deserialize_stream(stream, obj):
|
|||||||
def savefile_open(filename, binary=False, encoding='utf-8'):
|
def savefile_open(filename, binary=False, encoding='utf-8'):
|
||||||
"""Context manager to easily use a QSaveFile."""
|
"""Context manager to easily use a QSaveFile."""
|
||||||
f = QSaveFile(filename)
|
f = QSaveFile(filename)
|
||||||
new_f = None
|
cancelled = False
|
||||||
try:
|
try:
|
||||||
ok = f.open(QIODevice.WriteOnly)
|
ok = f.open(QIODevice.WriteOnly)
|
||||||
if not ok:
|
if not ok:
|
||||||
@ -192,13 +192,14 @@ def savefile_open(filename, binary=False, encoding='utf-8'):
|
|||||||
yield new_f
|
yield new_f
|
||||||
except:
|
except:
|
||||||
f.cancelWriting()
|
f.cancelWriting()
|
||||||
|
cancelled = True
|
||||||
raise
|
raise
|
||||||
|
else:
|
||||||
|
new_f.flush()
|
||||||
finally:
|
finally:
|
||||||
if new_f is not None:
|
|
||||||
new_f.flush()
|
|
||||||
commit_ok = f.commit()
|
commit_ok = f.commit()
|
||||||
if not commit_ok:
|
if not commit_ok and not cancelled:
|
||||||
raise OSError(f.errorString())
|
raise OSError("Commit failed!")
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
@ -221,27 +222,58 @@ class PyQIODevice(io.BufferedIOBase):
|
|||||||
"""Wrapper for a QIODevice which provides a python interface.
|
"""Wrapper for a QIODevice which provides a python interface.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
_dev: The underlying QIODevice.
|
dev: The underlying QIODevice.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# pylint: disable=missing-docstring
|
# pylint: disable=missing-docstring
|
||||||
|
|
||||||
def __init__(self, dev):
|
def __init__(self, dev):
|
||||||
self._dev = dev
|
self.dev = dev
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
return self._dev.size()
|
return self.dev.size()
|
||||||
|
|
||||||
def _check_open(self):
|
def _check_open(self):
|
||||||
"""Check if the device is open, raise OSError if not."""
|
"""Check if the device is open, raise ValueError if not."""
|
||||||
if not self._dev.isOpen():
|
if not self.dev.isOpen():
|
||||||
raise OSError("IO operation on closed device!")
|
raise ValueError("IO operation on closed device!")
|
||||||
|
|
||||||
def _check_random(self):
|
def _check_random(self):
|
||||||
"""Check if the device supports random access, raise OSError if not."""
|
"""Check if the device supports random access, raise OSError if not."""
|
||||||
if not self.seekable():
|
if not self.seekable():
|
||||||
raise OSError("Random access not allowed!")
|
raise OSError("Random access not allowed!")
|
||||||
|
|
||||||
|
def _check_readable(self):
|
||||||
|
"""Check if the device is readable, raise OSError if not."""
|
||||||
|
if not self.dev.isReadable():
|
||||||
|
raise OSError("Trying to read unreadable file!")
|
||||||
|
|
||||||
|
def _check_writable(self):
|
||||||
|
"""Check if the device is writable, raise OSError if not."""
|
||||||
|
if not self.writable():
|
||||||
|
raise OSError("Trying to write to unwritable file!")
|
||||||
|
|
||||||
|
def open(self, mode):
|
||||||
|
"""Open the underlying device and ensure opening succeeded.
|
||||||
|
|
||||||
|
Raises OSError if opening failed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mode: QIODevice::OpenMode flags.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
A contextlib.closing() object so this can be used as
|
||||||
|
contextmanager.
|
||||||
|
"""
|
||||||
|
ok = self.dev.open(mode)
|
||||||
|
if not ok:
|
||||||
|
raise OSError(self.dev.errorString())
|
||||||
|
return contextlib.closing(self)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Close the underlying device."""
|
||||||
|
self.dev.close()
|
||||||
|
|
||||||
def fileno(self):
|
def fileno(self):
|
||||||
raise io.UnsupportedOperation
|
raise io.UnsupportedOperation
|
||||||
|
|
||||||
@ -249,85 +281,102 @@ class PyQIODevice(io.BufferedIOBase):
|
|||||||
self._check_open()
|
self._check_open()
|
||||||
self._check_random()
|
self._check_random()
|
||||||
if whence == io.SEEK_SET:
|
if whence == io.SEEK_SET:
|
||||||
ok = self._dev.seek(offset)
|
ok = self.dev.seek(offset)
|
||||||
elif whence == io.SEEK_CUR:
|
elif whence == io.SEEK_CUR:
|
||||||
ok = self._dev.seek(self.tell() + offset)
|
ok = self.dev.seek(self.tell() + offset)
|
||||||
elif whence == io.SEEK_END:
|
elif whence == io.SEEK_END:
|
||||||
ok = self._dev.seek(len(self) + offset)
|
ok = self.dev.seek(len(self) + offset)
|
||||||
else:
|
else:
|
||||||
raise io.UnsupportedOperation("whence = {} is not "
|
raise io.UnsupportedOperation("whence = {} is not "
|
||||||
"supported!".format(whence))
|
"supported!".format(whence))
|
||||||
if not ok:
|
if not ok:
|
||||||
raise OSError(self._dev.errorString())
|
raise OSError("seek failed!")
|
||||||
|
|
||||||
def truncate(self, size=None): # pylint: disable=unused-argument
|
def truncate(self, size=None): # pylint: disable=unused-argument
|
||||||
raise io.UnsupportedOperation
|
raise io.UnsupportedOperation
|
||||||
|
|
||||||
def close(self):
|
|
||||||
self._dev.close()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def closed(self):
|
def closed(self):
|
||||||
return not self._dev.isOpen()
|
return not self.dev.isOpen()
|
||||||
|
|
||||||
def flush(self):
|
def flush(self):
|
||||||
self._check_open()
|
self._check_open()
|
||||||
self._dev.waitForBytesWritten(-1)
|
self.dev.waitForBytesWritten(-1)
|
||||||
|
|
||||||
def isatty(self):
|
def isatty(self):
|
||||||
self._check_open()
|
self._check_open()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def readable(self):
|
def readable(self):
|
||||||
return self._dev.isReadable()
|
return self.dev.isReadable()
|
||||||
|
|
||||||
def readline(self, size=-1):
|
def readline(self, size=-1):
|
||||||
self._check_open()
|
self._check_open()
|
||||||
if size == -1:
|
self._check_readable()
|
||||||
size = 0
|
|
||||||
return self._dev.readLine(size)
|
if size < 0:
|
||||||
|
qt_size = 0 # no maximum size
|
||||||
|
elif size == 0:
|
||||||
|
return QByteArray()
|
||||||
|
else:
|
||||||
|
qt_size = size + 1 # Qt also counts the NUL byte
|
||||||
|
|
||||||
|
if self.dev.canReadLine():
|
||||||
|
buf = self.dev.readLine(qt_size)
|
||||||
|
else:
|
||||||
|
if size < 0:
|
||||||
|
buf = self.dev.readAll()
|
||||||
|
else:
|
||||||
|
buf = self.dev.read(size)
|
||||||
|
|
||||||
|
if buf is None:
|
||||||
|
raise OSError(self.dev.errorString())
|
||||||
|
return buf
|
||||||
|
|
||||||
def seekable(self):
|
def seekable(self):
|
||||||
return not self._dev.isSequential()
|
return not self.dev.isSequential()
|
||||||
|
|
||||||
def tell(self):
|
def tell(self):
|
||||||
self._check_open()
|
self._check_open()
|
||||||
self._check_random()
|
self._check_random()
|
||||||
return self._dev.pos()
|
return self.dev.pos()
|
||||||
|
|
||||||
def writable(self):
|
def writable(self):
|
||||||
return self._dev.isWritable()
|
return self.dev.isWritable()
|
||||||
|
|
||||||
def readinto(self, b):
|
|
||||||
self._check_open()
|
|
||||||
return self._dev.read(b, len(b))
|
|
||||||
|
|
||||||
def write(self, b):
|
def write(self, b):
|
||||||
self._check_open()
|
self._check_open()
|
||||||
num = self._dev.write(b)
|
self._check_writable()
|
||||||
|
num = self.dev.write(b)
|
||||||
if num == -1 or num < len(b):
|
if num == -1 or num < len(b):
|
||||||
raise OSError(self._dev.errorString())
|
raise OSError(self.dev.errorString())
|
||||||
return num
|
return num
|
||||||
|
|
||||||
def read(self, size):
|
def read(self, size=-1):
|
||||||
self._check_open()
|
self._check_open()
|
||||||
buf = bytes()
|
self._check_readable()
|
||||||
num = self._dev.read(buf, size)
|
if size < 0:
|
||||||
if num == -1:
|
buf = self.dev.readAll()
|
||||||
raise OSError(self._dev.errorString())
|
else:
|
||||||
return num
|
buf = self.dev.read(size)
|
||||||
|
if buf is None:
|
||||||
|
raise OSError(self.dev.errorString())
|
||||||
|
return buf
|
||||||
|
|
||||||
|
|
||||||
class QtValueError(ValueError):
|
class QtValueError(ValueError):
|
||||||
|
|
||||||
"""Exception which gets raised by ensure_valid."""
|
"""Exception which gets raised by ensure_valid."""
|
||||||
|
|
||||||
def __init__(self, obj):
|
def __init__(self, obj, null=False):
|
||||||
try:
|
try:
|
||||||
self.reason = obj.errorString()
|
self.reason = obj.errorString()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
self.reason = None
|
self.reason = None
|
||||||
err = "{} is not valid".format(obj)
|
if null:
|
||||||
|
err = "{} is null".format(obj)
|
||||||
|
else:
|
||||||
|
err = "{} is not valid".format(obj)
|
||||||
if self.reason:
|
if self.reason:
|
||||||
err += ": {}".format(self.reason)
|
err += ": {}".format(self.reason)
|
||||||
super().__init__(err)
|
super().__init__(err)
|
||||||
|
@ -94,6 +94,8 @@ def _is_url_naive(urlstr):
|
|||||||
True if the URL really is a URL, False otherwise.
|
True if the URL really is a URL, False otherwise.
|
||||||
"""
|
"""
|
||||||
url = qurl_from_user_input(urlstr)
|
url = qurl_from_user_input(urlstr)
|
||||||
|
assert url.isValid()
|
||||||
|
|
||||||
if not utils.raises(ValueError, ipaddress.ip_address, urlstr):
|
if not utils.raises(ValueError, ipaddress.ip_address, urlstr):
|
||||||
# Valid IPv4/IPv6 address
|
# Valid IPv4/IPv6 address
|
||||||
return True
|
return True
|
||||||
@ -104,9 +106,7 @@ def _is_url_naive(urlstr):
|
|||||||
if not QHostAddress(urlstr).isNull():
|
if not QHostAddress(urlstr).isNull():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not url.isValid():
|
if '.' in url.host():
|
||||||
return False
|
|
||||||
elif '.' in url.host():
|
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
@ -122,9 +122,7 @@ def _is_url_dns(urlstr):
|
|||||||
True if the URL really is a URL, False otherwise.
|
True if the URL really is a URL, False otherwise.
|
||||||
"""
|
"""
|
||||||
url = qurl_from_user_input(urlstr)
|
url = qurl_from_user_input(urlstr)
|
||||||
if not url.isValid():
|
assert url.isValid()
|
||||||
log.url.debug("Invalid URL -> False")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if (utils.raises(ValueError, ipaddress.ip_address, urlstr) and
|
if (utils.raises(ValueError, ipaddress.ip_address, urlstr) and
|
||||||
not QHostAddress(urlstr).isNull()):
|
not QHostAddress(urlstr).isNull()):
|
||||||
@ -246,16 +244,13 @@ def is_url(urlstr):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
if not qurl_userinput.isValid():
|
if not qurl_userinput.isValid():
|
||||||
|
# This will also catch URLs containing spaces.
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if _has_explicit_scheme(qurl):
|
if _has_explicit_scheme(qurl):
|
||||||
# URLs with explicit schemes are always URLs
|
# URLs with explicit schemes are always URLs
|
||||||
log.url.debug("Contains explicit scheme")
|
log.url.debug("Contains explicit scheme")
|
||||||
url = True
|
url = True
|
||||||
elif ' ' in urlstr:
|
|
||||||
# A URL will never contain a space
|
|
||||||
log.url.debug("Contains space -> no URL")
|
|
||||||
url = False
|
|
||||||
elif qurl_userinput.host() in ('localhost', '127.0.0.1', '::1'):
|
elif qurl_userinput.host() in ('localhost', '127.0.0.1', '::1'):
|
||||||
log.url.debug("Is localhost.")
|
log.url.debug("Is localhost.")
|
||||||
url = True
|
url = True
|
||||||
@ -274,7 +269,7 @@ def is_url(urlstr):
|
|||||||
else:
|
else:
|
||||||
raise ValueError("Invalid autosearch value")
|
raise ValueError("Invalid autosearch value")
|
||||||
log.url.debug("url = {}".format(url))
|
log.url.debug("url = {}".format(url))
|
||||||
return url and qurl_userinput.isValid()
|
return url
|
||||||
|
|
||||||
|
|
||||||
def qurl_from_user_input(urlstr):
|
def qurl_from_user_input(urlstr):
|
||||||
|
@ -130,7 +130,7 @@ def _module_versions():
|
|||||||
try:
|
try:
|
||||||
import sipconfig # pylint: disable=import-error,unused-variable
|
import sipconfig # pylint: disable=import-error,unused-variable
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
lines.append('SIP: ?')
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
lines.append('SIP: {}'.format(
|
lines.append('SIP: {}'.format(
|
||||||
|
@ -28,6 +28,7 @@ import sys
|
|||||||
import glob
|
import glob
|
||||||
import subprocess
|
import subprocess
|
||||||
import platform
|
import platform
|
||||||
|
import filecmp
|
||||||
|
|
||||||
|
|
||||||
class Error(Exception):
|
class Error(Exception):
|
||||||
@ -58,6 +59,22 @@ def get_ignored_files(directory, files):
|
|||||||
return filtered
|
return filtered
|
||||||
|
|
||||||
|
|
||||||
|
def needs_update(source, dest):
|
||||||
|
"""Check if a file to be linked/copied needs to be updated."""
|
||||||
|
if os.path.islink(dest):
|
||||||
|
# No need to delete a link and relink -> skip this
|
||||||
|
return False
|
||||||
|
elif os.path.isdir(dest):
|
||||||
|
diffs = filecmp.dircmp(source, dest)
|
||||||
|
ignored = get_ignored_files(source, diffs.left_only)
|
||||||
|
has_new_files = set(ignored) != set(diffs.left_only)
|
||||||
|
return (has_new_files or diffs.right_only or
|
||||||
|
diffs.common_funny or diffs.diff_files or
|
||||||
|
diffs.funny_files)
|
||||||
|
else:
|
||||||
|
return not filecmp.cmp(source, dest)
|
||||||
|
|
||||||
|
|
||||||
def link_pyqt(sys_path, venv_path):
|
def link_pyqt(sys_path, venv_path):
|
||||||
"""Symlink the systemwide PyQt/sip into the venv.
|
"""Symlink the systemwide PyQt/sip into the venv.
|
||||||
|
|
||||||
@ -70,28 +87,47 @@ def link_pyqt(sys_path, venv_path):
|
|||||||
if not globbed_sip:
|
if not globbed_sip:
|
||||||
raise Error("Did not find sip in {}!".format(sys_path))
|
raise Error("Did not find sip in {}!".format(sys_path))
|
||||||
|
|
||||||
files = ['PyQt5']
|
files = [('PyQt5', True), ('sipconfig.py', False)]
|
||||||
files += [os.path.basename(e) for e in globbed_sip]
|
files += [(os.path.basename(e), True) for e in globbed_sip]
|
||||||
for fn in files:
|
for fn, required in files:
|
||||||
source = os.path.join(sys_path, fn)
|
source = os.path.join(sys_path, fn)
|
||||||
dest = os.path.join(venv_path, fn)
|
dest = os.path.join(venv_path, fn)
|
||||||
|
|
||||||
if not os.path.exists(source):
|
if not os.path.exists(source):
|
||||||
raise FileNotFoundError(source)
|
if required:
|
||||||
|
raise FileNotFoundError(source)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
if os.path.exists(dest):
|
if os.path.exists(dest):
|
||||||
if os.path.isdir(dest) and not os.path.islink(dest):
|
if needs_update(source, dest):
|
||||||
shutil.rmtree(dest)
|
remove(dest)
|
||||||
else:
|
else:
|
||||||
os.unlink(dest)
|
continue
|
||||||
if os.name == 'nt':
|
|
||||||
if os.path.isdir(source):
|
copy_or_link(source, dest)
|
||||||
shutil.copytree(source, dest, ignore=get_ignored_files,
|
|
||||||
copy_function=verbose_copy)
|
|
||||||
else:
|
def copy_or_link(source, dest):
|
||||||
print('{} -> {}'.format(source, dest))
|
"""Copy or symlink source to dest."""
|
||||||
shutil.copy(source, dest)
|
if os.name == 'nt':
|
||||||
|
if os.path.isdir(source):
|
||||||
|
shutil.copytree(source, dest, ignore=get_ignored_files,
|
||||||
|
copy_function=verbose_copy)
|
||||||
else:
|
else:
|
||||||
print('{} -> {}'.format(source, dest))
|
print('{} -> {}'.format(source, dest))
|
||||||
os.symlink(source, dest)
|
shutil.copy(source, dest)
|
||||||
|
else:
|
||||||
|
print('{} -> {}'.format(source, dest))
|
||||||
|
os.symlink(source, dest)
|
||||||
|
|
||||||
|
|
||||||
|
def remove(filename):
|
||||||
|
"""Remove a given filename, regardless of whether it's a file or dir."""
|
||||||
|
if os.path.isdir(filename):
|
||||||
|
shutil.rmtree(filename)
|
||||||
|
else:
|
||||||
|
os.unlink(filename)
|
||||||
|
|
||||||
|
|
||||||
def get_python_lib(executable, venv=False):
|
def get_python_lib(executable, venv=False):
|
||||||
|
@ -184,7 +184,7 @@ def _get_command_doc_args(cmd, parser):
|
|||||||
yield "* +'{}'+: {}".format(name, parser.arg_descs[arg])
|
yield "* +'{}'+: {}".format(name, parser.arg_descs[arg])
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
raise KeyError("No description for arg {} of command "
|
raise KeyError("No description for arg {} of command "
|
||||||
"'{}'!".format(e, cmd.name))
|
"'{}'!".format(e, cmd.name)) from e
|
||||||
|
|
||||||
if cmd.opt_args:
|
if cmd.opt_args:
|
||||||
yield ""
|
yield ""
|
||||||
@ -193,9 +193,9 @@ def _get_command_doc_args(cmd, parser):
|
|||||||
try:
|
try:
|
||||||
yield '* +*{}*+, +*{}*+: {}'.format(short_flag, long_flag,
|
yield '* +*{}*+, +*{}*+: {}'.format(short_flag, long_flag,
|
||||||
parser.arg_descs[arg])
|
parser.arg_descs[arg])
|
||||||
except KeyError:
|
except KeyError as e:
|
||||||
raise KeyError("No description for arg {} of command "
|
raise KeyError("No description for arg {} of command "
|
||||||
"'{}'!".format(e, cmd.name))
|
"'{}'!".format(e, cmd.name)) from e
|
||||||
|
|
||||||
|
|
||||||
def _get_command_doc_count(cmd, parser):
|
def _get_command_doc_count(cmd, parser):
|
||||||
@ -213,9 +213,9 @@ def _get_command_doc_count(cmd, parser):
|
|||||||
yield "==== count"
|
yield "==== count"
|
||||||
try:
|
try:
|
||||||
yield parser.arg_descs[cmd.count_arg]
|
yield parser.arg_descs[cmd.count_arg]
|
||||||
except KeyError:
|
except KeyError as e:
|
||||||
raise KeyError("No description for count arg {!r} of command "
|
raise KeyError("No description for count arg {!r} of command "
|
||||||
"{!r}!".format(cmd.count_arg, cmd.name))
|
"{!r}!".format(cmd.count_arg, cmd.name)) from e
|
||||||
|
|
||||||
|
|
||||||
def _get_command_doc_notes(cmd):
|
def _get_command_doc_notes(cmd):
|
||||||
|
@ -157,14 +157,6 @@ class TestConfigParser:
|
|||||||
self.cfg.get('general', 'bar') # pylint: disable=bad-config-call
|
self.cfg.get('general', 'bar') # pylint: disable=bad-config-call
|
||||||
|
|
||||||
|
|
||||||
def keyconfig_deprecated_test_cases():
|
|
||||||
"""Generator yielding test cases (command, rgx) for TestKeyConfigParser."""
|
|
||||||
for sect in configdata.KEY_DATA.values():
|
|
||||||
for command in sect:
|
|
||||||
for rgx, _repl in configdata.CHANGED_KEY_COMMANDS:
|
|
||||||
yield (command, rgx)
|
|
||||||
|
|
||||||
|
|
||||||
class TestKeyConfigParser:
|
class TestKeyConfigParser:
|
||||||
|
|
||||||
"""Test config.parsers.keyconf.KeyConfigParser."""
|
"""Test config.parsers.keyconf.KeyConfigParser."""
|
||||||
@ -185,10 +177,13 @@ class TestKeyConfigParser:
|
|||||||
with pytest.raises(keyconf.KeyConfigError):
|
with pytest.raises(keyconf.KeyConfigError):
|
||||||
kcp._read_command(cmdline_test.cmd)
|
kcp._read_command(cmdline_test.cmd)
|
||||||
|
|
||||||
@pytest.mark.parametrize('command, rgx', keyconfig_deprecated_test_cases())
|
@pytest.mark.parametrize('rgx', [rgx for rgx, _repl
|
||||||
def test_default_config_no_deprecated(self, command, rgx):
|
in configdata.CHANGED_KEY_COMMANDS])
|
||||||
|
def test_default_config_no_deprecated(self, rgx):
|
||||||
"""Make sure the default config contains no deprecated commands."""
|
"""Make sure the default config contains no deprecated commands."""
|
||||||
assert rgx.match(command) is None
|
for sect in configdata.KEY_DATA.values():
|
||||||
|
for command in sect:
|
||||||
|
assert rgx.match(command) is None
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'old, new_expected',
|
'old, new_expected',
|
||||||
|
@ -72,7 +72,7 @@ class LineParserWrapper:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class TestableAppendLineParser(LineParserWrapper,
|
class AppendLineParserTestable(LineParserWrapper,
|
||||||
lineparsermod.AppendLineParser):
|
lineparsermod.AppendLineParser):
|
||||||
|
|
||||||
"""Wrapper over AppendLineParser to make it testable."""
|
"""Wrapper over AppendLineParser to make it testable."""
|
||||||
@ -80,14 +80,14 @@ class TestableAppendLineParser(LineParserWrapper,
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TestableLineParser(LineParserWrapper, lineparsermod.LineParser):
|
class LineParserTestable(LineParserWrapper, lineparsermod.LineParser):
|
||||||
|
|
||||||
"""Wrapper over LineParser to make it testable."""
|
"""Wrapper over LineParser to make it testable."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TestableLimitLineParser(LineParserWrapper,
|
class LimitLineParserTestable(LineParserWrapper,
|
||||||
lineparsermod.LimitLineParser):
|
lineparsermod.LimitLineParser):
|
||||||
|
|
||||||
"""Wrapper over LimitLineParser to make it testable."""
|
"""Wrapper over LimitLineParser to make it testable."""
|
||||||
@ -137,7 +137,7 @@ class TestAppendLineParser:
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def lineparser(self):
|
def lineparser(self):
|
||||||
"""Fixture to get an AppendLineParser for tests."""
|
"""Fixture to get an AppendLineParser for tests."""
|
||||||
lp = TestableAppendLineParser('this really', 'does not matter')
|
lp = AppendLineParserTestable('this really', 'does not matter')
|
||||||
lp.new_data = self.BASE_DATA
|
lp.new_data = self.BASE_DATA
|
||||||
lp.save()
|
lp.save()
|
||||||
return lp
|
return lp
|
||||||
@ -178,7 +178,7 @@ class TestAppendLineParser:
|
|||||||
|
|
||||||
def test_get_recent_none(self):
|
def test_get_recent_none(self):
|
||||||
"""Test get_recent with no data."""
|
"""Test get_recent with no data."""
|
||||||
linep = TestableAppendLineParser('this really', 'does not matter')
|
linep = AppendLineParserTestable('this really', 'does not matter')
|
||||||
assert linep.get_recent() == []
|
assert linep.get_recent() == []
|
||||||
|
|
||||||
def test_get_recent_little(self, lineparser):
|
def test_get_recent_little(self, lineparser):
|
||||||
|
9
tox.ini
9
tox.ini
@ -19,17 +19,18 @@ usedevelop = true
|
|||||||
setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/platforms
|
setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/platforms
|
||||||
passenv = DISPLAY XAUTHORITY HOME
|
passenv = DISPLAY XAUTHORITY HOME
|
||||||
deps =
|
deps =
|
||||||
|
-r{toxinidir}/requirements.txt
|
||||||
py==1.4.27
|
py==1.4.27
|
||||||
pytest==2.7.1
|
pytest==2.7.1
|
||||||
pytest-capturelog==0.7
|
pytest-capturelog==0.7
|
||||||
pytest-qt==1.3.0
|
pytest-qt==1.3.0
|
||||||
pytest-mock==0.5
|
pytest-mock==0.5
|
||||||
pytest-html==1.2
|
pytest-html==1.3.1
|
||||||
# We don't use {[testenv:mkvenv]commands} here because that seems to be broken
|
# We don't use {[testenv:mkvenv]commands} here because that seems to be broken
|
||||||
# on Ubuntu Trusty.
|
# on Ubuntu Trusty.
|
||||||
commands =
|
commands =
|
||||||
{envpython} scripts/link_pyqt.py --tox {envdir}
|
{envpython} scripts/link_pyqt.py --tox {envdir}
|
||||||
{envpython} -m py.test --strict {posargs}
|
{envpython} -m py.test --strict -rfEsw {posargs}
|
||||||
|
|
||||||
[testenv:coverage]
|
[testenv:coverage]
|
||||||
passenv = DISPLAY XAUTHORITY HOME
|
passenv = DISPLAY XAUTHORITY HOME
|
||||||
@ -40,7 +41,7 @@ deps =
|
|||||||
cov-core==1.15.0
|
cov-core==1.15.0
|
||||||
commands =
|
commands =
|
||||||
{[testenv:mkvenv]commands}
|
{[testenv:mkvenv]commands}
|
||||||
{envpython} -m py.test --strict --cov qutebrowser --cov-report term --cov-report html {posargs}
|
{envpython} -m py.test --strict -rfEswx -v --cov qutebrowser --cov-report term --cov-report html {posargs}
|
||||||
|
|
||||||
[testenv:misc]
|
[testenv:misc]
|
||||||
commands =
|
commands =
|
||||||
@ -96,7 +97,7 @@ commands =
|
|||||||
[testenv:check-manifest]
|
[testenv:check-manifest]
|
||||||
skip_install = true
|
skip_install = true
|
||||||
deps =
|
deps =
|
||||||
check-manifest==0.24
|
check-manifest==0.25
|
||||||
commands =
|
commands =
|
||||||
{[testenv:mkvenv]commands}
|
{[testenv:mkvenv]commands}
|
||||||
{envdir}/bin/check-manifest --ignore 'qutebrowser/git-commit-id,qutebrowser/html/doc,qutebrowser/html/doc/*,*/__pycache__'
|
{envdir}/bin/check-manifest --ignore 'qutebrowser/git-commit-id,qutebrowser/html/doc,qutebrowser/html/doc/*,*/__pycache__'
|
||||||
|
Loading…
Reference in New Issue
Block a user