Merge remote-tracking branch 'origin/master' into layout

Conflicts:
	qutebrowser/test/keyinput/test_basekeyparser.py
	qutebrowser/test/utils/test_standarddir.py
	test/browser/http/test_content_disposition.py
	test/config/test_configtypes.py
	test/misc/test_editor.py
	test/utils/test_debug.py
	test/utils/test_utils.py
	tox.ini
This commit is contained in:
Bruno Oliveira 2015-04-04 14:29:49 -03:00
commit 4fa2294805
89 changed files with 899 additions and 582 deletions

View File

@ -278,7 +278,7 @@ There are currently these object registries, also called 'scopes':
`cookie-jar`, etc.) `cookie-jar`, etc.)
* The `tab` scope with objects which are per-tab (`hintmanager`, `webview`, * The `tab` scope with objects which are per-tab (`hintmanager`, `webview`,
etc.). Passing this scope to `objreg.get()` selects the object in the currently etc.). Passing this scope to `objreg.get()` selects the object in the currently
focused tab by default. A tab can be explicitely selected by passing focused tab by default. A tab can be explicitly selected by passing
+tab=_tab-id_, window=_win-id_+ to it. +tab=_tab-id_, window=_win-id_+ to it.
A new object can be registered by using A new object can be registered by using
@ -373,7 +373,7 @@ The types of the function arguments are inferred based on their default values,
e.g. an argument `foo=True` will be converted to a flag `-f`/`--foo` in e.g. an argument `foo=True` will be converted to a flag `-f`/`--foo` in
qutebrowser's commandline. qutebrowser's commandline.
This behaviour can be overridden using Python's This behavior can be overridden using Python's
http://legacy.python.org/dev/peps/pep-3107/[function annotations]. The http://legacy.python.org/dev/peps/pep-3107/[function annotations]. The
annotation should always be a `dict`, like this: annotation should always be a `dict`, like this:
@ -447,7 +447,7 @@ This option controls Valgrind's detection of self-modifying code. If no
checking is done, if a program executes some code, then overwrites it with new checking is done, if a program executes some code, then overwrites it with new
code, and executes the new code, Valgrind will continue to execute the code, and executes the new code, Valgrind will continue to execute the
translations it made for the old code. This will likely lead to incorrect translations it made for the old code. This will likely lead to incorrect
behaviour and/or crashes. behavior and/or crashes.
... ...

View File

@ -157,6 +157,7 @@ Contributors, sorted by the number of commits in descending order:
* Helen Sherwood-Taylor * Helen Sherwood-Taylor
* HalosGhost * HalosGhost
* Gregor Pohl * Gregor Pohl
* Franz Fellner
* Eivind Uggedal * Eivind Uggedal
* Andreas Fischer * Andreas Fischer
// QUTE_AUTHORS_END // QUTE_AUTHORS_END

View File

@ -35,7 +35,7 @@
|<<report,report>>|Report a bug in qutebrowser. |<<report,report>>|Report a bug in qutebrowser.
|<<restart,restart>>|Restart qutebrowser while keeping existing tabs open. |<<restart,restart>>|Restart qutebrowser while keeping existing tabs open.
|<<save,save>>|Save configs and state. |<<save,save>>|Save configs and state.
|<<search,search>>|Search for a text on the current page. |<<search,search>>|Search for a text on the current page. With no text, clear results.
|<<session-delete,session-delete>>|Delete a session. |<<session-delete,session-delete>>|Delete a session.
|<<session-load,session-load>>|Load a session. |<<session-load,session-load>>|Load a session.
|<<session-save,session-save>>|Save a session. |<<session-save,session-save>>|Save a session.
@ -394,9 +394,9 @@ Save configs and state.
[[search]] [[search]]
=== search === search
Syntax: +:search [*--reverse*] 'text'+ Syntax: +:search [*--reverse*] ['text']+
Search for a text on the current page. Search for a text on the current page. With no text, clear results.
==== positional arguments ==== positional arguments
* +'text'+: The text to search for. * +'text'+: The text to search for.
@ -406,16 +406,20 @@ Search for a text on the current page.
[[session-delete]] [[session-delete]]
=== session-delete === session-delete
Syntax: +:session-delete 'name'+ Syntax: +:session-delete [*--force*] 'name'+
Delete a session. Delete a session.
==== positional arguments ==== positional arguments
* +'name'+: The name of the session. * +'name'+: The name of the session.
==== optional arguments
* +*-f*+, +*--force*+: Force deleting internal sessions (starting with an underline).
[[session-load]] [[session-load]]
=== session-load === session-load
Syntax: +:session-load [*--clear*] 'name'+ Syntax: +:session-load [*--clear*] [*--force*] 'name'+
Load a session. Load a session.
@ -424,10 +428,12 @@ Load a session.
==== optional arguments ==== optional arguments
* +*-c*+, +*--clear*+: Close all existing windows. * +*-c*+, +*--clear*+: Close all existing windows.
* +*-f*+, +*--force*+: Force loading internal sessions (starting with an underline).
[[session-save]] [[session-save]]
=== session-save === session-save
Syntax: +:session-save [*--quiet*] ['name']+ Syntax: +:session-save [*--quiet*] [*--force*] ['name']+
Save a session. Save a session.
@ -436,6 +442,7 @@ Save a session.
==== optional arguments ==== optional arguments
* +*-q*+, +*--quiet*+: Don't show confirmation message. * +*-q*+, +*--quiet*+: Don't show confirmation message.
* +*-f*+, +*--force*+: Force saving internal sessions (starting with an underline).
[[set]] [[set]]
=== set === set
@ -456,13 +463,16 @@ If the option name ends with '?', the value of the option is shown instead. If t
[[set-cmd-text]] [[set-cmd-text]]
=== set-cmd-text === set-cmd-text
Syntax: +:set-cmd-text 'text'+ Syntax: +:set-cmd-text [*--space*] 'text'+
Preset the statusbar to some text. Preset the statusbar to some text.
==== positional arguments ==== positional arguments
* +'text'+: The commandline to set. * +'text'+: The commandline to set.
==== optional arguments
* +*-s*+, +*--space*+: If given, a space is added to the end.
[[spawn]] [[spawn]]
=== spawn === spawn
Syntax: +:spawn [*--userscript*] 'args' ['args' ...]+ Syntax: +:spawn [*--userscript*] 'args' ['args' ...]+
@ -503,7 +513,7 @@ Close the current/[count]th tab.
==== optional arguments ==== optional arguments
* +*-l*+, +*--left*+: Force selecting the tab to the left of the current tab. * +*-l*+, +*--left*+: Force selecting the tab to the left of the current tab.
* +*-r*+, +*--right*+: Force selecting the tab to the right of the current tab. * +*-r*+, +*--right*+: Force selecting the tab to the right of the current tab.
* +*-o*+, +*--opposite*+: Force selecting the tab in the oppsite direction of what's configured in 'tabs->select-on-remove'. * +*-o*+, +*--opposite*+: Force selecting the tab in the opposite direction of what's configured in 'tabs->select-on-remove'.
==== count ==== count
@ -643,7 +653,6 @@ How many steps to zoom out.
|<<completion-item-prev,completion-item-prev>>|Select the previous completion item. |<<completion-item-prev,completion-item-prev>>|Select the previous completion item.
|<<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.
|<<fooled,fooled>>|Turn off april's fools.
|<<leave-mode,leave-mode>>|Leave the mode we're currently in. |<<leave-mode,leave-mode>>|Leave the mode we're currently in.
|<<open-editor,open-editor>>|Open an external editor with the currently selected form field. |<<open-editor,open-editor>>|Open an external editor with the currently selected form field.
|<<prompt-accept,prompt-accept>>|Accept the current prompt. |<<prompt-accept,prompt-accept>>|Accept the current prompt.
@ -701,10 +710,6 @@ Enter a key mode.
=== follow-hint === follow-hint
Follow the currently selected hint. Follow the currently selected hint.
[[fooled]]
=== fooled
Turn off april's fools.
[[leave-mode]] [[leave-mode]]
=== leave-mode === leave-mode
Leave the mode we're currently in. Leave the mode we're currently in.
@ -875,6 +880,7 @@ These commands are mainly intended for debugging. They are hidden if qutebrowser
|<<debug-console,debug-console>>|Show the debugging console. |<<debug-console,debug-console>>|Show the debugging console.
|<<debug-crash,debug-crash>>|Crash for debugging purposes. |<<debug-crash,debug-crash>>|Crash for debugging purposes.
|<<debug-pyeval,debug-pyeval>>|Evaluate a python string and display the results as a web page. |<<debug-pyeval,debug-pyeval>>|Evaluate a python string and display the results as a web page.
|<<debug-trace,debug-trace>>|Trace executed code via hunter.
|============== |==============
[[debug-all-objects]] [[debug-all-objects]]
=== debug-all-objects === debug-all-objects
@ -906,3 +912,12 @@ Evaluate a python string and display the results as a webpage.
==== positional arguments ==== positional arguments
* +'s'+: The string to evaluate. * +'s'+: The string to evaluate.
[[debug-trace]]
=== debug-trace
Syntax: +:debug-trace ['expr']+
Trace executed code via hunter.
==== positional arguments
* +'expr'+: What to trace, passed to hunter.

View File

@ -93,7 +93,7 @@
|<<tabs-background-tabs,background-tabs>>|Whether to open new tabs (middleclick/ctrl+click) in background. |<<tabs-background-tabs,background-tabs>>|Whether to open new tabs (middleclick/ctrl+click) in background.
|<<tabs-select-on-remove,select-on-remove>>|Which tab to select when the focused tab is removed. |<<tabs-select-on-remove,select-on-remove>>|Which tab to select when the focused tab is removed.
|<<tabs-new-tab-position,new-tab-position>>|How new tabs are positioned. |<<tabs-new-tab-position,new-tab-position>>|How new tabs are positioned.
|<<tabs-new-tab-position-explicit,new-tab-position-explicit>>|How new tabs opened explicitely are positioned. |<<tabs-new-tab-position-explicit,new-tab-position-explicit>>|How new tabs opened explicitly are positioned.
|<<tabs-last-close,last-close>>|Behaviour when the last tab is closed. |<<tabs-last-close,last-close>>|Behaviour when the last tab is closed.
|<<tabs-hide-auto,hide-auto>>|Hide the tab bar if only one tab is open. |<<tabs-hide-auto,hide-auto>>|Hide the tab bar if only one tab is open.
|<<tabs-hide-always,hide-always>>|Always hide the tab bar. |<<tabs-hide-always,hide-always>>|Always hide the tab bar.
@ -834,7 +834,7 @@ Default: +pass:[right]+
[[tabs-new-tab-position-explicit]] [[tabs-new-tab-position-explicit]]
=== new-tab-position-explicit === new-tab-position-explicit
How new tabs opened explicitely are positioned. How new tabs opened explicitly are positioned.
Valid values: Valid values:
@ -1208,7 +1208,7 @@ Whether to accept cookies.
Valid values: Valid values:
* +default+: Default QtWebKit behaviour. * +default+: Default QtWebKit behavior.
* +never+: Don't accept cookies at all. * +never+: Don't accept cookies at all.
Default: +pass:[default]+ Default: +pass:[default]+
@ -1332,7 +1332,7 @@ Default: +pass:[\bprev(ious)?\b,\bback\b,\bolder\b,\b[&lt;←≪]\b,\b(&lt;&lt;|
== searchengines == searchengines
Definitions of search engines which can be used via the address bar. Definitions of search engines which can be used via the address bar.
The searchengine named `DEFAULT` is used when `general -> auto-search` is true and something else than a URL was entered to be opened. Other search engines can be used via the bang-syntax, e.g. `:open qutebrowser !google`. The string `{}` will be replaced by the search term, use `{{` and `}}` for literal `{`/`}` signs. The searchengine named `DEFAULT` is used when `general -> auto-search` is true and something else than a URL was entered to be opened. Other search engines can be used by prepending the search engine name to the search term, e.g. `:open google qutebrowser`. The string `{}` will be replaced by the search term, use `{{` and `}}` for literal `{`/`}` signs.
== aliases == aliases
Aliases for commands. Aliases for commands.

View File

@ -30,12 +30,16 @@ import base64
import functools import functools
import traceback import traceback
import faulthandler import faulthandler
import datetime import json
from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox
from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon
from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QTimer, QUrl, from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QTimer, QUrl,
QObject, Qt, QSocketNotifier) QObject, Qt, QSocketNotifier)
try:
import hunter
except ImportError:
hunter = None
import qutebrowser import qutebrowser
import qutebrowser.resources # pylint: disable=unused-import import qutebrowser.resources # pylint: disable=unused-import
@ -50,7 +54,7 @@ from qutebrowser.misc import (crashdialog, readline, ipc, earlyinit,
from qutebrowser.misc import utilcmds # pylint: disable=unused-import from qutebrowser.misc import utilcmds # pylint: disable=unused-import
from qutebrowser.keyinput import modeman from qutebrowser.keyinput import modeman
from qutebrowser.utils import (log, version, message, utils, qtutils, urlutils, from qutebrowser.utils import (log, version, message, utils, qtutils, urlutils,
debug, objreg, usertypes, standarddir) objreg, usertypes, standarddir)
# We import utilcmds to run the cmdutils.register decorators. # We import utilcmds to run the cmdutils.register decorators.
@ -157,15 +161,6 @@ class Application(QApplication):
if self._crashdlg is not None: if self._crashdlg is not None:
self._crashdlg.raise_() self._crashdlg.raise_()
state_config = objreg.get('state-config')
try:
fooled = state_config['general']['fooled']
except KeyError:
fooled = False
if datetime.date.today() == datetime.date(2015, 4, 1) and not fooled:
message.info('current', "Happy April's fools! Use :fooled to turn "
"this off.")
def __repr__(self): def __repr__(self):
return utils.get_repr(self) return utils.get_repr(self)
@ -312,6 +307,9 @@ class Application(QApplication):
del state_config['general']['session'] del state_config['general']['session']
except KeyError: except KeyError:
pass pass
# If this was a _restart session, delete it.
if name == '_restart':
session_manager.delete('_restart')
def _get_window(self, via_ipc, force_window=False, force_tab=False): def _get_window(self, via_ipc, force_window=False, force_tab=False):
"""Helper function for process_pos_args to get a window id. """Helper function for process_pos_args to get a window id.
@ -433,10 +431,6 @@ class Application(QApplication):
window='last-focused') window='last-focused')
tabbed_browser.tabopen( tabbed_browser.tabopen(
QUrl('http://www.qutebrowser.org/quickstart.html')) QUrl('http://www.qutebrowser.org/quickstart.html'))
try:
state_config.add_section('general')
except configparser.DuplicateSectionError:
pass
state_config['general']['quickstart-done'] = '1' state_config['general']['quickstart-done'] = '1'
def _setup_signals(self): def _setup_signals(self):
@ -557,19 +551,11 @@ class Application(QApplication):
if self.geometry is not None: if self.geometry is not None:
state_config = objreg.get('state-config') state_config = objreg.get('state-config')
geom = base64.b64encode(self.geometry).decode('ASCII') geom = base64.b64encode(self.geometry).decode('ASCII')
try:
state_config.add_section('geometry')
except configparser.DuplicateSectionError:
pass
state_config['geometry']['mainwindow'] = geom state_config['geometry']['mainwindow'] = geom
def _save_version(self): def _save_version(self):
"""Save the current version to the state config.""" """Save the current version to the state config."""
state_config = objreg.get('state-config') state_config = objreg.get('state-config')
try:
state_config.add_section('general')
except configparser.DuplicateSectionError:
pass
state_config['general']['version'] = qutebrowser.__version__ state_config['general']['version'] = qutebrowser.__version__
def _destroy_crashlogfile(self): def _destroy_crashlogfile(self):
@ -656,7 +642,8 @@ class Application(QApplication):
self._args.debug, pages, cmd_history, exc, objects) self._args.debug, pages, cmd_history, exc, objects)
ret = self._crashdlg.exec_() ret = self._crashdlg.exec_()
if ret == QDialog.Accepted: # restore if ret == QDialog.Accepted: # restore
self.restart(shutdown=False, pages=pages) self._do_restart(pages)
# We might risk a segfault here, but that's better than continuing to # We might risk a segfault here, but that's better than continuing to
# run in some undefined state, so we only do the most needed shutdown # run in some undefined state, so we only do the most needed shutdown
# here. # here.
@ -664,11 +651,12 @@ class Application(QApplication):
self._destroy_crashlogfile() self._destroy_crashlogfile()
sys.exit(1) sys.exit(1)
def _get_restart_args(self, pages): def _get_restart_args(self, pages=(), session=None):
"""Get the current working directory and args to relaunch qutebrowser. """Get the current working directory and args to relaunch qutebrowser.
Args: Args:
pages: The pages to re-open. pages: The pages to re-open.
session: The session to load, or None.
Return: Return:
An (args, cwd) tuple. An (args, cwd) tuple.
@ -691,42 +679,82 @@ class Application(QApplication):
# cwd=None and see if that works out. # cwd=None and see if that works out.
# See https://github.com/The-Compiler/qutebrowser/issues/323 # See https://github.com/The-Compiler/qutebrowser/issues/323
cwd = None cwd = None
for arg in sys.argv[1:]:
if arg.startswith('-'):
# We only want to preserve options on a restart.
args.append(arg)
# Add all open pages so they get reopened. # Add all open pages so they get reopened.
page_args = [] page_args = []
for win in pages: for win in pages:
page_args.extend(win) page_args.extend(win)
page_args.append('') page_args.append('')
if page_args:
args.extend(page_args[:-1]) # Serialize the argparse namespace into json and pass that to the new
# process via --json-args.
# We do this as there's no way to "unparse" the namespace while
# ignoring some arguments.
argdict = vars(self._args)
argdict['session'] = None
argdict['url'] = []
argdict['command'] = page_args[:-1]
argdict['json_args'] = None
# Ensure the given session (or none at all) gets opened.
if session is None:
argdict['session'] = None
argdict['override_restore'] = True
else:
argdict['session'] = session
argdict['override_restore'] = False
# Dump the data
data = json.dumps(argdict)
args += ['--json-args', data]
log.destroy.debug("args: {}".format(args)) log.destroy.debug("args: {}".format(args))
log.destroy.debug("cwd: {}".format(cwd)) log.destroy.debug("cwd: {}".format(cwd))
return args, cwd return args, cwd
@cmdutils.register(instance='app', ignore_args=True) @cmdutils.register(instance='app')
def restart(self, shutdown=True, pages=None): def restart(self):
"""Restart qutebrowser while keeping existing tabs open.""" """Restart qutebrowser while keeping existing tabs open."""
if pages is None: ok = self._do_restart(session='_restart')
pages = self._recover_pages() if ok:
self.shutdown()
def _do_restart(self, pages=(), session=None):
"""Inner logic to restart qutebrowser.
The "better" way to restart is to pass a session (_restart usually) as
that'll save the complete state.
However we don't do that (and pass a list of pages instead) when we
restart because of an exception, as that's a lot simpler and we don't
want to risk anything going wrong.
Args:
pages: A list of URLs to open.
session: The session to load, or None.
Return:
True if the restart succeeded, False otherwise.
"""
log.destroy.debug("sys.executable: {}".format(sys.executable)) log.destroy.debug("sys.executable: {}".format(sys.executable))
log.destroy.debug("sys.path: {}".format(sys.path)) log.destroy.debug("sys.path: {}".format(sys.path))
log.destroy.debug("sys.argv: {}".format(sys.argv)) log.destroy.debug("sys.argv: {}".format(sys.argv))
log.destroy.debug("frozen: {}".format(hasattr(sys, 'frozen'))) log.destroy.debug("frozen: {}".format(hasattr(sys, 'frozen')))
# Save the session if one is given.
if session is not None:
session_manager = objreg.get('session-manager')
session_manager.save(session)
# Open a new process and immediately shutdown the existing one # Open a new process and immediately shutdown the existing one
try: try:
args, cwd = self._get_restart_args(pages) args, cwd = self._get_restart_args(pages, session)
if cwd is None: if cwd is None:
subprocess.Popen(args) subprocess.Popen(args)
else: else:
subprocess.Popen(args, cwd=cwd) subprocess.Popen(args, cwd=cwd)
except OSError: except OSError:
log.destroy.exception("Failed to restart") log.destroy.exception("Failed to restart")
return False
else: else:
if shutdown: return True
self.shutdown()
@cmdutils.register(instance='app', maxsplit=0, debug=True) @cmdutils.register(instance='app', maxsplit=0, debug=True)
def debug_pyeval(self, s): def debug_pyeval(self, s):
@ -843,8 +871,8 @@ class Application(QApplication):
deferrer = True deferrer = True
if deferrer: if deferrer:
# If shutdown was called while we were asking a question, we're in # If shutdown was called while we were asking a question, we're in
# a still sub-eventloop (which gets quitted now) and not in the # a still sub-eventloop (which gets quit now) and not in the main
# main one. # one.
# This means we need to defer the real shutdown to when we're back # This means we need to defer the real shutdown to when we're back
# in the real main event loop, or we'll get a segfault. # in the real main event loop, or we'll get a segfault.
log.destroy.debug("Deferring real shutdown because question was " log.destroy.debug("Deferring real shutdown because question was "
@ -894,7 +922,7 @@ class Application(QApplication):
qInstallMessageHandler(None) qInstallMessageHandler(None)
# Now we can hopefully quit without segfaults # Now we can hopefully quit without segfaults
log.destroy.debug("Deferring QApplication::exit...") log.destroy.debug("Deferring QApplication::exit...")
# We use a singleshot timer to exit here to minimize the likelyhood of # We use a singleshot timer to exit here to minimize the likelihood of
# segfaults. # segfaults.
QTimer.singleShot(0, functools.partial(self.exit, status)) QTimer.singleShot(0, functools.partial(self.exit, status))
@ -924,6 +952,10 @@ class Application(QApplication):
"""Extend QApplication::exit to log the event.""" """Extend QApplication::exit to log the event."""
log.destroy.debug("Now calling QApplication::exit.") log.destroy.debug("Now calling QApplication::exit.")
if self._args.debug_exit: if self._args.debug_exit:
if hunter is None:
print("Not logging late shutdown because hunter could not be "
"imported!", file=sys.stderr)
else:
print("Now logging late shutdown.", file=sys.stderr) print("Now logging late shutdown.", file=sys.stderr)
debug.trace_lines(True) hunter.trace()
super().exit(status) super().exit(status)

View File

@ -227,7 +227,7 @@ class CommandDispatcher:
Args: Args:
left: Force selecting the tab to the left of the current tab. left: Force selecting the tab to the left of the current tab.
right: Force selecting the tab to the right of the current tab. right: Force selecting the tab to the right of the current tab.
opposite: Force selecting the tab in the oppsite direction of opposite: Force selecting the tab in the opposite direction of
what's configured in 'tabs->select-on-remove'. what's configured in 'tabs->select-on-remove'.
Return: Return:
@ -259,7 +259,7 @@ class CommandDispatcher:
Args: Args:
left: Force selecting the tab to the left of the current tab. left: Force selecting the tab to the left of the current tab.
right: Force selecting the tab to the right of the current tab. right: Force selecting the tab to the right of the current tab.
opposite: Force selecting the tab in the oppsite direction of opposite: Force selecting the tab in the opposite direction of
what's configured in 'tabs->select-on-remove'. what's configured in 'tabs->select-on-remove'.
count: The tab index to close, or None count: The tab index to close, or None
""" """

View File

@ -185,7 +185,7 @@ class DownloadItem(QObject):
done: Whether the download is finished. done: Whether the download is finished.
stats: A DownloadItemStats object. stats: A DownloadItemStats object.
index: The index of the download in the view. index: The index of the download in the view.
successful: Whether the download has completed sucessfully. successful: Whether the download has completed successfully.
error_msg: The current error message, or None error_msg: The current error message, or None
autoclose: Whether to close the associated file if the download is autoclose: Whether to close the associated file if the download is
done. done.
@ -204,7 +204,7 @@ class DownloadItem(QObject):
data_changed: The downloads metadata changed. data_changed: The downloads metadata changed.
finished: The download was finished. finished: The download was finished.
cancelled: The download was cancelled. cancelled: The download was cancelled.
error: An error with the download occured. error: An error with the download occurred.
arg: The error message as string. arg: The error message as string.
redirected: Signal emitted when a download was redirected. redirected: Signal emitted when a download was redirected.
arg 0: The new QNetworkRequest. arg 0: The new QNetworkRequest.

View File

@ -123,7 +123,7 @@ class DownloadView(QListView):
Return: Return:
A list of either: A list of either:
- (QAction, callable) tuples. - (QAction, callable) tuples.
- (None, None) for a seperator - (None, None) for a separator
""" """
actions = [] actions = []
if item is None: if item is None:

View File

@ -455,7 +455,7 @@ class HintManager(QObject):
"""Yank an element to the clipboard or primary selection. """Yank an element to the clipboard or primary selection.
Args: Args:
url: The URL to open as a QURL. url: The URL to open as a QUrl.
context: The HintContext to use. context: The HintContext to use.
""" """
sel = context.target == Target.yank_primary sel = context.target == Target.yank_primary
@ -816,7 +816,7 @@ class HintManager(QObject):
'<font color="{}">{}</font>{}'.format( '<font color="{}">{}</font>{}'.format(
match_color, matched, rest)) match_color, matched, rest))
if self._is_hidden(elems.label): if self._is_hidden(elems.label):
# hidden element which matches again -> unhide it # hidden element which matches again -> show it
self._show_elem(elems.label) self._show_elem(elems.label)
else: else:
# element doesn't match anymore -> hide it # element doesn't match anymore -> hide it
@ -835,7 +835,7 @@ class HintManager(QObject):
if (filterstr is None or if (filterstr is None or
str(elems.elem).lower().startswith(filterstr)): str(elems.elem).lower().startswith(filterstr)):
if self._is_hidden(elems.label): if self._is_hidden(elems.label):
# hidden element which matches again -> unhide it # hidden element which matches again -> show it
self._show_elem(elems.label) self._show_elem(elems.label)
else: else:
# element doesn't match anymore -> hide it # element doesn't match anymore -> hide it

View File

@ -132,6 +132,8 @@ class WebHistory(QWebHistoryInterface):
Args: Args:
url_string: An url as string to add to the history. url_string: An url as string to add to the history.
""" """
if not url_string:
return
if not config.get('general', 'private-browsing'): if not config.get('general', 'private-browsing'):
entry = HistoryEntry(time.time(), url_string) entry = HistoryEntry(time.time(), url_string)
self.item_about_to_be_added.emit(entry) self.item_about_to_be_added.emit(entry)

View File

@ -176,7 +176,7 @@ class NetworkManager(QNetworkAccessManager):
if answer is not None: if answer is not None:
# Since the answer could be something else than (user, password) # Since the answer could be something else than (user, password)
# pylint seems to think we're unpacking a non-sequence. However we # pylint seems to think we're unpacking a non-sequence. However we
# *did* explicitely ask for a tuple, so it *will* always be one. # *did* explicitly ask for a tuple, so it *will* always be one.
user, password = answer user, password = answer
authenticator.setUser(user) authenticator.setUser(user)
authenticator.setPassword(password) authenticator.setPassword(password)

View File

@ -43,9 +43,19 @@ class QuickmarkManager(QObject):
marks: An OrderedDict of all quickmarks. marks: An OrderedDict of all quickmarks.
_lineparser: The LineParser used for the quickmarks, or None _lineparser: The LineParser used for the quickmarks, or None
(when qutebrowser is started with -c ''). (when qutebrowser is started with -c '').
Signals:
changed: Emitted when anything changed.
added: Emitted when a new quickmark was added.
arg 0: The name of the quickmark.
arg 1: The URL of the quickmark, as string.
removed: Emitted when an existing quickmark was removed.
arg 0: The name of the quickmark.
""" """
changed = pyqtSignal() changed = pyqtSignal()
added = pyqtSignal(str, str)
removed = pyqtSignal(str)
def __init__(self, parent=None): def __init__(self, parent=None):
"""Initialize and read quickmarks.""" """Initialize and read quickmarks."""
@ -117,6 +127,7 @@ class QuickmarkManager(QObject):
"""Really set the quickmark.""" """Really set the quickmark."""
self.marks[name] = url self.marks[name] = url
self.changed.emit() self.changed.emit()
self.added.emit(name, url)
if name in self.marks: if name in self.marks:
message.confirm_async( message.confirm_async(
@ -138,6 +149,7 @@ class QuickmarkManager(QObject):
raise cmdexc.CommandError("Quickmark '{}' not found!".format(name)) raise cmdexc.CommandError("Quickmark '{}' not found!".format(name))
else: else:
self.changed.emit() self.changed.emit()
self.removed.emit(name)
def get(self, name): def get(self, name):
"""Get the URL of the quickmark named name as a QUrl.""" """Get the URL of the quickmark named name as a QUrl."""

View File

@ -133,7 +133,7 @@ def serialize(items):
Return: Return:
A (stream, data, user_data) tuple. A (stream, data, user_data) tuple.
stream: The resetted QDataStream. stream: The reseted QDataStream.
data: The QByteArray with the raw data. data: The QByteArray with the raw data.
user_data: A list with each item's user data. user_data: A list with each item's user data.

View File

@ -41,7 +41,7 @@ class BrowserPage(QWebPage):
"""Our own QWebPage with advanced features. """Our own QWebPage with advanced features.
Attributes: Attributes:
error_occured: Whether an error occured while loading. error_occurred: Whether an error occurred while loading.
open_target: Where to open the next navigation request. open_target: Where to open the next navigation request.
("normal", "tab", "tab_bg") ("normal", "tab", "tab_bg")
_hint_target: Override for open_target while hinting, or None. _hint_target: Override for open_target while hinting, or None.
@ -69,7 +69,7 @@ class BrowserPage(QWebPage):
QWebPage.ChooseMultipleFilesExtension: self._handle_multiple_files, QWebPage.ChooseMultipleFilesExtension: self._handle_multiple_files,
} }
self._ignore_load_started = False self._ignore_load_started = False
self.error_occured = False self.error_occurred = False
self.open_target = usertypes.ClickTarget.normal self.open_target = usertypes.ClickTarget.normal
self._hint_target = None self._hint_target = None
self._networkmanager = networkmanager.NetworkManager( self._networkmanager = networkmanager.NetworkManager(
@ -147,7 +147,7 @@ class BrowserPage(QWebPage):
else: else:
error_str = info.errorString error_str = info.errorString
if error_str == networkmanager.HOSTBLOCK_ERROR_STRING: if error_str == networkmanager.HOSTBLOCK_ERROR_STRING:
# We don't set error_occured in this case. # We don't set error_occurred in this case.
error_str = "Request blocked by host blocker." error_str = "Request blocked by host blocker."
main_frame = info.frame.page().mainFrame() main_frame = info.frame.page().mainFrame()
if info.frame != main_frame: if info.frame != main_frame:
@ -160,7 +160,7 @@ class BrowserPage(QWebPage):
return False return False
else: else:
self._ignore_load_started = True self._ignore_load_started = True
self.error_occured = True self.error_occurred = True
log.webview.error("Error while loading {}: {}".format( log.webview.error("Error while loading {}: {}".format(
urlstr, error_str)) urlstr, error_str))
log.webview.debug("Error domain: {}, error code: {}".format( log.webview.debug("Error domain: {}, error code: {}".format(
@ -248,7 +248,7 @@ class BrowserPage(QWebPage):
frame.setScrollPosition, cur_data['scroll-pos'])) frame.setScrollPosition, cur_data['scroll-pos']))
def display_content(self, reply, mimetype): def display_content(self, reply, mimetype):
"""Display a QNetworkReply with an explicitely set mimetype.""" """Display a QNetworkReply with an explicitly set mimetype."""
self.mainFrame().setContent(reply.readAll(), mimetype, reply.url()) self.mainFrame().setContent(reply.readAll(), mimetype, reply.url())
reply.deleteLater() reply.deleteLater()
@ -312,11 +312,11 @@ class BrowserPage(QWebPage):
@pyqtSlot() @pyqtSlot()
def on_load_started(self): def on_load_started(self):
"""Reset error_occured when loading of a new page started.""" """Reset error_occurred when loading of a new page started."""
if self._ignore_load_started: if self._ignore_load_started:
self._ignore_load_started = False self._ignore_load_started = False
else: else:
self.error_occured = False self.error_occurred = False
@pyqtSlot('QWebFrame', 'QWebPage::Feature') @pyqtSlot('QWebFrame', 'QWebPage::Feature')
def on_feature_permission_requested(self, frame, feature): def on_feature_permission_requested(self, frame, feature):
@ -393,8 +393,8 @@ class BrowserPage(QWebPage):
# With Qt 5.2.1 (Ubuntu Trusty) we get this when closing a tab: # With Qt 5.2.1 (Ubuntu Trusty) we get this when closing a tab:
# RuntimeError: wrapped C/C++ object of type BrowserPage has # RuntimeError: wrapped C/C++ object of type BrowserPage has
# been deleted # been deleted
# Since the information here isn't that important for closing # Since the information here isn't that important for closing web
# webviews anyways, we ignore this error. # views anyways, we ignore this error.
return return
data = { data = {
'zoom': frame.zoomFactor(), 'zoom': frame.zoomFactor(),

View File

@ -52,7 +52,7 @@ class WebView(QWebView):
hintmanager: The HintManager instance for this view. hintmanager: The HintManager instance for this view.
progress: loading progress of this page. progress: loading progress of this page.
scroll_pos: The current scroll position as (x%, y%) tuple. scroll_pos: The current scroll position as (x%, y%) tuple.
statusbar_message: The current javscript statusbar message. statusbar_message: The current javascript statusbar message.
inspector: The QWebInspector used for this webview. inspector: The QWebInspector used for this webview.
load_status: loading status of this page (index into LoadStatus) load_status: loading status of this page (index into LoadStatus)
viewing_source: Whether the webview is currently displaying source viewing_source: Whether the webview is currently displaying source
@ -63,7 +63,7 @@ class WebView(QWebView):
tab_id: The tab ID of the view. tab_id: The tab ID of the view.
win_id: The window ID of the view. win_id: The window ID of the view.
_cur_url: The current URL (accessed via cur_url property). _cur_url: The current URL (accessed via cur_url property).
_has_ssl_errors: Whether SSL errors occured during loading. _has_ssl_errors: Whether SSL errors occurred during loading.
_zoom: A NeighborList with the zoom levels. _zoom: A NeighborList with the zoom levels.
_old_scroll_pos: The old scroll position. _old_scroll_pos: The old scroll position.
_check_insertmode: If True, in mouseReleaseEvent we should check if we _check_insertmode: If True, in mouseReleaseEvent we should check if we
@ -393,7 +393,7 @@ class WebView(QWebView):
true when the QWebPage has an ErrorPageExtension implemented. true when the QWebPage has an ErrorPageExtension implemented.
See https://github.com/The-Compiler/qutebrowser/issues/84 See https://github.com/The-Compiler/qutebrowser/issues/84
""" """
ok = not self.page().error_occured ok = not self.page().error_occurred
if ok and not self._has_ssl_errors: if ok and not self._has_ssl_errors:
self._set_load_status(LoadStatus.success) self._set_load_status(LoadStatus.success)
elif ok: elif ok:

View File

@ -32,7 +32,7 @@ class CommandError(Exception):
class CommandMetaError(Exception): class CommandMetaError(Exception):
"""Common base class for exceptions occuring before a command is run.""" """Common base class for exceptions occurring before a command is run."""
class NoSuchCommandError(CommandMetaError): class NoSuchCommandError(CommandMetaError):

View File

@ -101,7 +101,7 @@ class register: # pylint: disable=invalid-name
_instance: The object from the object registry to be used as "self". _instance: The object from the object registry to be used as "self".
_scope: The scope to get _instance for. _scope: The scope to get _instance for.
_name: The name (as string) or names (as list) of the command. _name: The name (as string) or names (as list) of the command.
_maxsplit: The maxium amounts of splits to do for the commandline, or _maxsplit: The maximum amounts of splits to do for the commandline, or
None. None.
_hide: Whether to hide the command or not. _hide: Whether to hide the command or not.
_completion: Which completion to use for arguments, as a list of _completion: Which completion to use for arguments, as a list of
@ -151,7 +151,7 @@ class register: # pylint: disable=invalid-name
def _get_names(self, func): def _get_names(self, func):
"""Get the name(s) which should be used for the current command. """Get the name(s) which should be used for the current command.
If the name hasn't been overridden explicitely, the function name is If the name hasn't been overridden explicitly, the function name is
transformed. transformed.
If it has been set, it can either be a string which is If it has been set, it can either be a string which is

View File

@ -160,7 +160,7 @@ class Command:
return type_conv return type_conv
def _get_nameconv(self, param, annotation_info): def _get_nameconv(self, param, annotation_info):
"""Get a dict with a name conversion for the paraeter. """Get a dict with a name conversion for the parameter.
Args: Args:
param: The inspect.Parameter to handle. param: The inspect.Parameter to handle.

View File

@ -19,8 +19,6 @@
"""Module containing command managers (SearchRunner and CommandRunner).""" """Module containing command managers (SearchRunner and CommandRunner)."""
import re
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QUrl from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QUrl
from PyQt5.QtWebKitWidgets import QWebPage from PyQt5.QtWebKitWidgets import QWebPage
@ -79,8 +77,8 @@ class SearchRunner(QObject):
@pyqtSlot(str) @pyqtSlot(str)
@cmdutils.register(instance='search-runner', scope='window', maxsplit=0) @cmdutils.register(instance='search-runner', scope='window', maxsplit=0)
def search(self, text, reverse=False): def search(self, text="", reverse=False):
"""Search for a text on the current page. """Search for a text on the current page. With no text, clear results.
Args: Args:
text: The text to search for. text: The text to search for.
@ -266,16 +264,8 @@ class CommandRunner(QObject):
else: else:
self._args = [] self._args = []
maxsplit = i + self._cmd.maxsplit + flag_arg_count maxsplit = i + self._cmd.maxsplit + flag_arg_count
args = split.simple_split(argstr, keep=keep, self._args = split.simple_split(argstr, keep=keep,
maxsplit=maxsplit) maxsplit=maxsplit)
for s in args:
# remove quotes and replace \" by "
if s == '""' or s == "''":
s = ''
else:
s = re.sub(r"""(^|[^\\])["']""", r'\1', s)
s = re.sub(r"""\\(["'])""", r'\1', s)
self._args.append(s)
break break
else: else:
# If there are only flags, we got it right on the first try # If there are only flags, we got it right on the first try

View File

@ -52,7 +52,7 @@ class _QtFIFOReader(QObject):
@pyqtSlot() @pyqtSlot()
def read_line(self): def read_line(self):
"""(Try to) read a line from the fifo.""" """(Try to) read a line from the FIFO."""
log.procs.debug("QSocketNotifier triggered!") log.procs.debug("QSocketNotifier triggered!")
self._notifier.setEnabled(False) self._notifier.setEnabled(False)
for line in self.fifo: for line in self.fifo:
@ -60,7 +60,7 @@ class _QtFIFOReader(QObject):
self._notifier.setEnabled(True) self._notifier.setEnabled(True)
def cleanup(self): def cleanup(self):
"""Clean up so the fifo can be closed.""" """Clean up so the FIFO can be closed."""
self._notifier.setEnabled(False) self._notifier.setEnabled(False)

View File

@ -196,6 +196,12 @@ class Completer(QObject):
data = model.data(indexes[0]) data = model.data(indexes[0])
if data is None: if data is None:
return return
parts = self.split()
try:
needs_quoting = cmdutils.cmd_dict[parts[0]].maxsplit is None
except KeyError:
needs_quoting = True
if needs_quoting:
data = self._quote(data) data = self._quote(data)
if model.count() == 1 and config.get('completion', 'quick-complete'): if model.count() == 1 and config.get('completion', 'quick-complete'):
# If we only have one item, we want to apply it immediately # If we only have one item, we want to apply it immediately
@ -245,7 +251,7 @@ class Completer(QObject):
if self._cmd.prefix() != ':': if self._cmd.prefix() != ':':
# This is a search or gibberish, so we don't need to complete # This is a search or gibberish, so we don't need to complete
# anything (yet) # anything (yet)
# FIXME complete searchs # FIXME complete searches
# https://github.com/The-Compiler/qutebrowser/issues/32 # https://github.com/The-Compiler/qutebrowser/issues/32
completion.hide() completion.hide()
return return

View File

@ -151,6 +151,9 @@ class CompletionView(QTreeView):
idx = self.selectionModel().currentIndex() idx = self.selectionModel().currentIndex()
if not idx.isValid(): if not idx.isValid():
# No item selected yet # No item selected yet
if upwards:
return self.model().last_item()
else:
return self.model().first_item() return self.model().first_item()
while True: while True:
idx = self.indexAbove(idx) if upwards else self.indexBelow(idx) idx = self.indexAbove(idx) if upwards else self.indexBelow(idx)

View File

@ -121,4 +121,5 @@ class SessionCompletionModel(base.BaseCompletionModel):
super().__init__(parent) super().__init__(parent)
cat = self.new_category("Sessions") cat = self.new_category("Sessions")
for name in objreg.get('session-manager').list_sessions(): for name in objreg.get('session-manager').list_sessions():
if not name.startswith('_'):
self.new_item(cat, name) self.new_item(cat, name)

View File

@ -21,7 +21,7 @@
import datetime import datetime
from PyQt5.QtCore import pyqtSlot from PyQt5.QtCore import pyqtSlot, Qt
from qutebrowser.utils import objreg, utils from qutebrowser.utils import objreg, utils
from qutebrowser.completion.models import base from qutebrowser.completion.models import base
@ -42,20 +42,21 @@ class UrlCompletionModel(base.BaseCompletionModel):
self._quickmark_cat = self.new_category("Quickmarks") self._quickmark_cat = self.new_category("Quickmarks")
self._history_cat = self.new_category("History") self._history_cat = self.new_category("History")
quickmarks = objreg.get('quickmark-manager').marks.items() quickmark_manager = objreg.get('quickmark-manager')
self._history = objreg.get('web-history') quickmarks = quickmark_manager.marks.items()
for qm_name, qm_url in quickmarks: for qm_name, qm_url in quickmarks:
self.new_item(self._quickmark_cat, qm_url, qm_name) self._add_quickmark_entry(qm_name, qm_url)
quickmark_manager.added.connect(self.on_quickmark_added)
quickmark_manager.removed.connect(self.on_quickmark_removed)
self._history = objreg.get('web-history')
max_history = config.get('completion', 'web-history-max-items') max_history = config.get('completion', 'web-history-max-items')
history = utils.newest_slice(self._history, max_history) history = utils.newest_slice(self._history, max_history)
for entry in history: for entry in history:
self._add_history_entry(entry) self._add_history_entry(entry)
self._history.item_about_to_be_added.connect( self._history.item_about_to_be_added.connect(
self.on_history_item_added) self.on_history_item_added)
objreg.get('config').changed.connect(self.reformat_timestamps) objreg.get('config').changed.connect(self.reformat_timestamps)
def _fmt_atime(self, atime): def _fmt_atime(self, atime):
@ -71,6 +72,15 @@ class UrlCompletionModel(base.BaseCompletionModel):
self._fmt_atime(entry.atime), sort=int(entry.atime), self._fmt_atime(entry.atime), sort=int(entry.atime),
userdata=entry.url) userdata=entry.url)
def _add_quickmark_entry(self, name, url):
"""Add a new quickmark entry to the completion.
Args:
name: The name of the new quickmark.
url: The URL of the new quickmark.
"""
self.new_item(self._quickmark_cat, url, name)
@config.change_filter('completion', 'timestamp-format') @config.change_filter('completion', 'timestamp-format')
def reformat_timestamps(self): def reformat_timestamps(self):
"""Reformat the timestamps if the config option was changed.""" """Reformat the timestamps if the config option was changed."""
@ -93,3 +103,26 @@ class UrlCompletionModel(base.BaseCompletionModel):
break break
else: else:
self._add_history_entry(entry) self._add_history_entry(entry)
@pyqtSlot(str, str)
def on_quickmark_added(self, name, url):
"""Called when a quickmark has been added by the user.
Args:
name: The name of the new quickmark.
url: The url of the new quickmark, as string.
"""
self._add_quickmark_entry(name, url)
@pyqtSlot(str)
def on_quickmark_removed(self, name):
"""Called when a quickmark has been removed by the user.
Args:
name: The name of the quickmark which has been removed.
"""
for i in range(self._quickmark_cat.rowCount()):
name_item = self._quickmark_cat.child(i, 1)
if name_item.data(Qt.DisplayRole) == name:
self._quickmark_cat.removeRow(i)
break

View File

@ -20,8 +20,8 @@
"""Configuration storage and config-related utilities. """Configuration storage and config-related utilities.
This borrows a lot of ideas from configparser, but also has some things that This borrows a lot of ideas from configparser, but also has some things that
are fundamentally different. This is why nothing inherts from configparser, but are fundamentally different. This is why nothing inherits from configparser,
we borrow some methods and classes from there where it makes sense. but we borrow some methods and classes from there where it makes sense.
""" """
import os import os
@ -144,7 +144,7 @@ def _init_main_config():
for sect in config_obj.sections.values(): for sect in config_obj.sections.values():
for opt in sect.values.values(): for opt in sect.values.values():
if opt.values['conf'] is None: if opt.values['conf'] is None:
# Option added to builtin defaults but not in user's # Option added to built-in defaults but not in user's
# config yet # config yet
save_manager.save('config', explicit=True, force=True) save_manager.save('config', explicit=True, force=True)
return return
@ -171,14 +171,22 @@ def _init_key_config():
save_manager = objreg.get('save-manager') save_manager = objreg.get('save-manager')
filename = os.path.join(standarddir.config(), 'keys.conf') filename = os.path.join(standarddir.config(), 'keys.conf')
save_manager.add_saveable( save_manager.add_saveable(
'key-config', key_config.save, key_config.changed, 'key-config', key_config.save, key_config.config_dirty,
config_opt=('general', 'auto-save-config'), filename=filename) config_opt=('general', 'auto-save-config'), filename=filename,
dirty=key_config.is_dirty)
def _init_misc(): def _init_misc():
"""Initialize misc. config-related files.""" """Initialize misc. config-related files."""
save_manager = objreg.get('save-manager') save_manager = objreg.get('save-manager')
state_config = ini.ReadWriteConfigParser(standarddir.data(), 'state') state_config = ini.ReadWriteConfigParser(standarddir.data(), 'state')
for sect in ('general', 'geometry'):
try:
state_config.add_section(sect)
except configparser.DuplicateSectionError:
pass
# See commit a98060e020a4ba83b663813a4b9404edb47f28ad.
state_config['general'].pop('fooled', None)
objreg.register('state-config', state_config) objreg.register('state-config', state_config)
save_manager.add_saveable('state-config', state_config.save) save_manager.add_saveable('state-config', state_config.save)
@ -262,8 +270,8 @@ class ConfigManager(QObject):
('completion', 'history-length'): 'cmd-history-max-items', ('completion', 'history-length'): 'cmd-history-max-items',
} }
DELETED_OPTIONS = [ DELETED_OPTIONS = [
('colors', 'tab.seperator'), ('colors', 'tab.separator'),
('colors', 'tabs.seperator'), ('colors', 'tabs.separator'),
('colors', 'completion.item.bg'), ('colors', 'completion.item.bg'),
] ]
@ -476,7 +484,7 @@ class ConfigManager(QObject):
def items(self, sectname, raw=True): def items(self, sectname, raw=True):
"""Get a list of (optname, value) tuples for a section. """Get a list of (optname, value) tuples for a section.
Implemented for configparser interpolation compatbility. Implemented for configparser interpolation compatibility
Args: Args:
sectname: The name of the section to get. sectname: The name of the section to get.
@ -539,7 +547,7 @@ class ConfigManager(QObject):
The value of the option. The value of the option.
""" """
if not self._initialized: if not self._initialized:
raise Exception("get got called before initialisation was " raise Exception("get got called before initialization was "
"complete!") "complete!")
try: try:
sect = self.sections[sectname] sect = self.sections[sectname]

View File

@ -80,10 +80,10 @@ SECTION_DESC = {
"bar.\n" "bar.\n"
"The searchengine named `DEFAULT` is used when " "The searchengine named `DEFAULT` is used when "
"`general -> auto-search` is true and something else than a URL was " "`general -> auto-search` is true and something else than a URL was "
"entered to be opened. Other search engines can be used via the " "entered to be opened. Other search engines can be used by prepending "
"bang-syntax, e.g. `:open qutebrowser !google`. The string `{}` will " "the search engine name to the search term, e.g. "
"be replaced by the search term, use `{{` and `}}` for literal " "`:open google qutebrowser`. The string `{}` will be replaced by the "
"`{`/`}` signs."), "search term, use `{{` and `}}` for literal `{`/`}` signs."),
'aliases': ( 'aliases': (
"Aliases for commands.\n" "Aliases for commands.\n"
"By default, no aliases are defined. Example which adds a new command " "By default, no aliases are defined. Example which adds a new command "
@ -414,7 +414,7 @@ DATA = collections.OrderedDict([
('new-tab-position-explicit', ('new-tab-position-explicit',
SettingValue(typ.NewTabPosition(), 'last'), SettingValue(typ.NewTabPosition(), 'last'),
"How new tabs opened explicitely are positioned."), "How new tabs opened explicitly are positioned."),
('last-close', ('last-close',
SettingValue(typ.LastClose(), 'ignore'), SettingValue(typ.LastClose(), 'ignore'),
@ -975,7 +975,7 @@ KEY_FIRST_COMMENT = """
# * Shift: `Shift` # * Shift: `Shift`
# #
# For simple keys (no `<>`-signs), a capital letter means the key is pressed # For simple keys (no `<>`-signs), a capital letter means the key is pressed
# with Shift. For special keys (with `<>`-signs), you need to explicitely add # with Shift. For special keys (with `<>`-signs), you need to explicitly add
# `Shift-` to match a key pressed with shift. You can bind multiple commands # `Shift-` to match a key pressed with shift. You can bind multiple commands
# by separating them with `;;`. # by separating them with `;;`.
""" """
@ -1029,14 +1029,14 @@ KEY_DATA = collections.OrderedDict([
('normal', collections.OrderedDict([ ('normal', collections.OrderedDict([
('search ""', ['<Escape>']), ('search ""', ['<Escape>']),
('set-cmd-text ":open "', ['o']), ('set-cmd-text -s :open', ['o']),
('set-cmd-text ":open {url}"', ['go']), ('set-cmd-text :open {url}', ['go']),
('set-cmd-text ":open -t "', ['O']), ('set-cmd-text -s :open -t', ['O']),
('set-cmd-text ":open -t {url}"', ['gO']), ('set-cmd-text :open -t {url}', ['gO']),
('set-cmd-text ":open -b "', ['xo']), ('set-cmd-text -s :open -b', ['xo']),
('set-cmd-text ":open -b {url}"', ['xO']), ('set-cmd-text :open -b {url}', ['xO']),
('set-cmd-text ":open -w "', ['wo']), ('set-cmd-text -s :open -w', ['wo']),
('set-cmd-text ":open -w {url}"', ['wO']), ('set-cmd-text :open -w {url}', ['wO']),
('open -t', ['ga']), ('open -t', ['ga']),
('tab-close', ['d', '<Ctrl-W>']), ('tab-close', ['d', '<Ctrl-W>']),
('tab-close -o', ['D']), ('tab-close -o', ['D']),
@ -1179,3 +1179,16 @@ KEY_DATA = collections.OrderedDict([
('rl-backward-delete-char', ['<Ctrl-H>']), ('rl-backward-delete-char', ['<Ctrl-H>']),
])), ])),
]) ])
# A list of (regex, replacement) tuples of changed key commands.
CHANGED_KEY_COMMANDS = [
(re.compile(r'^open -([twb]) about:blank$'), r'open -\1'),
(re.compile(r'^download-page$'), r'download'),
(re.compile(r'^cancel-download$'), r'download-cancel'),
(re.compile(r'^search ""$'), r'search'),
(re.compile(r"^search ''$"), r'search'),
(re.compile(r"""^set-cmd-text ['"](.*) ['"]$"""), r'set-cmd-text -s \1'),
(re.compile(r"""^set-cmd-text ['"](.*)['"]$"""), r'set-cmd-text \1'),
]

View File

@ -32,9 +32,9 @@ class ValidationError(Error):
"""Raised when a value for a config type was invalid. """Raised when a value for a config type was invalid.
Attributes: Attributes:
section: Section in which the error occured (added when catching and section: Section in which the error occurred (added when catching and
re-raising the exception). re-raising the exception).
option: Option in which the error occured. option: Option in which the error occurred.
""" """
def __init__(self, value, msg): def __init__(self, value, msg):

View File

@ -1328,7 +1328,7 @@ class AcceptCookies(BaseType):
"""Whether to accept a cookie.""" """Whether to accept a cookie."""
valid_values = ValidValues(('default', "Default QtWebKit behaviour."), valid_values = ValidValues(('default', "Default QtWebKit behavior."),
('never', "Don't accept cookies at all.")) ('never', "Don't accept cookies at all."))

View File

@ -60,7 +60,7 @@ class ReadConfigParser(configparser.ConfigParser):
class ReadWriteConfigParser(ReadConfigParser): class ReadWriteConfigParser(ReadConfigParser):
"""ConfigParser subclass used for auxillary config files.""" """ConfigParser subclass used for auxiliary config files."""
def save(self): def save(self):
"""Save the config file.""" """Save the config file."""

View File

@ -34,7 +34,7 @@ class KeyConfigError(Exception):
"""Raised on errors with the key config. """Raised on errors with the key config.
Attributes: Attributes:
lineno: The config line in which the exception occured. lineno: The config line in which the exception occurred.
""" """
def __init__(self, msg=None): def __init__(self, msg=None):
@ -55,13 +55,16 @@ class KeyConfigParser(QObject):
_configfile: The filename of the config or None. _configfile: The filename of the config or None.
_cur_section: The section currently being processed by _read(). _cur_section: The section currently being processed by _read().
_cur_command: The command currently being processed by _read(). _cur_command: The command currently being processed by _read().
is_dirty: Whether the config is currently dirty.
Signals: Signals:
changed: Emitted when the config has changed. changed: Emitted when the internal data has changed.
arg: Name of the mode which was changed. arg: Name of the mode which was changed.
config_dirty: Emitted when the config should be re-saved.
""" """
changed = pyqtSignal(str) changed = pyqtSignal(str)
config_dirty = pyqtSignal()
def __init__(self, configdir, fname, parent=None): def __init__(self, configdir, fname, parent=None):
"""Constructor. """Constructor.
@ -71,6 +74,7 @@ class KeyConfigParser(QObject):
fname: The filename of the config. fname: The filename of the config.
""" """
super().__init__(parent) super().__init__(parent)
self.is_dirty = False
self._cur_section = None self._cur_section = None
self._cur_command = None self._cur_command = None
# Mapping of section name(s) to key binding -> command dicts. # Mapping of section name(s) to key binding -> command dicts.
@ -165,6 +169,7 @@ class KeyConfigParser(QObject):
raise cmdexc.CommandError(e) raise cmdexc.CommandError(e)
for m in mode.split(','): for m in mode.split(','):
self.changed.emit(m) self.changed.emit(m)
self._mark_config_dirty()
@cmdutils.register(instance='key-config') @cmdutils.register(instance='key-config')
def unbind(self, key, mode=None): def unbind(self, key, mode=None):
@ -194,6 +199,7 @@ class KeyConfigParser(QObject):
else: else:
for m in mode.split(','): for m in mode.split(','):
self.changed.emit(m) self.changed.emit(m)
self._mark_config_dirty()
def _normalize_sectname(self, s): def _normalize_sectname(self, s):
"""Normalize a section string like 'foo, bar,baz' to 'bar,baz,foo'.""" """Normalize a section string like 'foo, bar,baz' to 'bar,baz,foo'."""
@ -246,6 +252,11 @@ class KeyConfigParser(QObject):
for sectname in self.keybindings: for sectname in self.keybindings:
self.changed.emit(sectname) self.changed.emit(sectname)
def _mark_config_dirty(self):
"""Mark the config as dirty."""
self.is_dirty = True
self.config_dirty.emit()
def _read_command(self, line): def _read_command(self, line):
"""Read a command from a line.""" """Read a command from a line."""
if self._cur_section is None: if self._cur_section is None:
@ -255,6 +266,11 @@ class KeyConfigParser(QObject):
command = line.split(maxsplit=1)[0] command = line.split(maxsplit=1)[0]
if command not in cmdutils.cmd_dict: if command not in cmdutils.cmd_dict:
raise KeyConfigError("Invalid command '{}'!".format(command)) raise KeyConfigError("Invalid command '{}'!".format(command))
for rgx, repl in configdata.CHANGED_KEY_COMMANDS:
if rgx.match(line):
line = rgx.sub(repl, line)
self._mark_config_dirty()
break
self._cur_command = line self._cur_command = line
def _read_keybinding(self, line): def _read_keybinding(self, line):

View File

@ -94,7 +94,7 @@ class ColorDict(dict):
log.style.exception("No color defined for {}!") log.style.exception("No color defined for {}!")
return '' return ''
if isinstance(val, QColor): if isinstance(val, QColor):
# This could happen when accidentaly declarding something as # This could happen when accidentally declaring something as
# QtColor instead of Color in the config, and it'd go unnoticed as # QtColor instead of Color in the config, and it'd go unnoticed as
# the CSS is invalid then. # the CSS is invalid then.
raise TypeError("QColor passed to ColorDict!") raise TypeError("QColor passed to ColorDict!")

View File

@ -26,7 +26,7 @@ class SettingValue:
"""Base class for setting values. """Base class for setting values.
Intended to be subclassed by config value "types". Intended to be sub-classed by config value "types".
Attributes: Attributes:
typ: A BaseType subclass instance. typ: A BaseType subclass instance.

View File

@ -24,12 +24,9 @@ Module attributes:
constants. constants.
""" """
import base64
import datetime
import os.path import os.path
from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtCore import QUrl
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import standarddir, objreg, log, utils, debug from qutebrowser.utils import standarddir, objreg, log, utils, debug
@ -194,22 +191,6 @@ class Setter(Base):
self._setter(*args) self._setter(*args)
class AprilSetter(Setter):
"""Set something... unless it's the 1st of April."""
def _set(self, value, qws=None):
state_config = objreg.get('state-config')
try:
fooled = state_config['general']['fooled']
except KeyError:
fooled = False
if datetime.date.today() == datetime.date(2015, 4, 1) and not fooled:
pass
else:
super()._set(value, qws)
class NullStringSetter(Setter): class NullStringSetter(Setter):
"""A setter for settings requiring a null QString as default. """A setter for settings requiring a null QString as default.
@ -336,7 +317,7 @@ MAPPINGS = {
'frame-flattening': 'frame-flattening':
Attribute(QWebSettings.FrameFlatteningEnabled), Attribute(QWebSettings.FrameFlatteningEnabled),
'user-stylesheet': 'user-stylesheet':
AprilSetter(getter=QWebSettings.userStyleSheetUrl, Setter(getter=QWebSettings.userStyleSheetUrl,
setter=QWebSettings.setUserStyleSheetUrl), setter=QWebSettings.setUserStyleSheetUrl),
'css-media-type': 'css-media-type':
NullStringSetter(getter=QWebSettings.cssMediaType, NullStringSetter(getter=QWebSettings.cssMediaType,
@ -399,21 +380,6 @@ def init():
QWebSettings.setOfflineStoragePath( QWebSettings.setOfflineStoragePath(
os.path.join(standarddir.data(), 'offline-storage')) os.path.join(standarddir.data(), 'offline-storage'))
state_config = objreg.get('state-config')
try:
fooled = state_config['general']['fooled']
except KeyError:
fooled = False
if datetime.date.today() == datetime.date(2015, 4, 1) and not fooled:
value = """
html {
-webkit-transform:rotate(3deg) scale(0.99);
}
"""
data = base64.b64encode(value.encode('utf-8')).decode('ascii')
url = QUrl("data:text/css;charset=utf-8;base64,{}".format(data))
QWebSettings.globalSettings().setUserStyleSheetUrl(url)
for sectname, section in MAPPINGS.items(): for sectname, section in MAPPINGS.items():
for optname, mapping in section.items(): for optname, mapping in section.items():
default = mapping.save_default() default = mapping.save_default()

View File

@ -57,7 +57,7 @@ class BaseKeyParser(QObject):
_warn_on_keychains: Whether a warning should be logged when binding _warn_on_keychains: Whether a warning should be logged when binding
keychains in a section which does not support them. keychains in a section which does not support them.
_keystring: The currently entered key sequence _keystring: The currently entered key sequence
_ambigious_timer: Timer for delayed execution with ambigious bindings. _ambiguous_timer: Timer for delayed execution with ambiguous bindings.
_modename: The name of the input mode associated with this keyparser. _modename: The name of the input mode associated with this keyparser.
_supports_count: Whether count is supported _supports_count: Whether count is supported
_supports_chains: Whether keychains are supported _supports_chains: Whether keychains are supported
@ -78,8 +78,8 @@ class BaseKeyParser(QObject):
supports_chains=False): supports_chains=False):
super().__init__(parent) super().__init__(parent)
self._win_id = win_id self._win_id = win_id
self._ambigious_timer = usertypes.Timer(self, 'ambigious-match') self._ambiguous_timer = usertypes.Timer(self, 'ambiguous-match')
self._ambigious_timer.setSingleShot(True) self._ambiguous_timer.setSingleShot(True)
self._modename = None self._modename = None
self._keystring = '' self._keystring = ''
if supports_count is None: if supports_count is None:
@ -248,11 +248,11 @@ class BaseKeyParser(QObject):
def _stop_timers(self): def _stop_timers(self):
"""Stop a delayed execution if any is running.""" """Stop a delayed execution if any is running."""
if self._ambigious_timer.isActive() and self.do_log: if self._ambiguous_timer.isActive() and self.do_log:
log.keyboard.debug("Stopping delayed execution.") log.keyboard.debug("Stopping delayed execution.")
self._ambigious_timer.stop() self._ambiguous_timer.stop()
try: try:
self._ambigious_timer.timeout.disconnect() self._ambiguous_timer.timeout.disconnect()
except TypeError: except TypeError:
# no connections # no connections
pass pass
@ -274,10 +274,10 @@ class BaseKeyParser(QObject):
# execute in `time' ms # execute in `time' ms
self._debug_log("Scheduling execution of {} in {}ms".format( self._debug_log("Scheduling execution of {} in {}ms".format(
binding, time)) binding, time))
self._ambigious_timer.setInterval(time) self._ambiguous_timer.setInterval(time)
self._ambigious_timer.timeout.connect( self._ambiguous_timer.timeout.connect(
functools.partial(self.delayed_exec, binding, count)) functools.partial(self.delayed_exec, binding, count))
self._ambigious_timer.start() self._ambiguous_timer.start()
def delayed_exec(self, command, count): def delayed_exec(self, command, count):
"""Execute a delayed command. """Execute a delayed command.

View File

@ -32,6 +32,33 @@ from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.utils import usertypes, log, objreg, utils from qutebrowser.utils import usertypes, log, objreg, utils
class KeyEvent:
"""A small wrapper over a QKeyEvent storing its data.
This is needed because Qt apparently mutates existing events with new data.
It doesn't store the modifiers because they can be different for a key
press/release.
Attributes:
key: A Qt.Key member (QKeyEvent::key).
text: A string (QKeyEvent::text).
"""
def __init__(self, keyevent):
self.key = keyevent.key()
self.text = keyevent.text()
def __repr__(self):
return utils.get_repr(self, key=self.key, text=self.text)
def __eq__(self, other):
return self.key == other.key and self.text == other.text
def __hash__(self):
return hash((self.key, self.text))
class NotInModeError(Exception): class NotInModeError(Exception):
"""Exception raised when we want to leave a mode we're not in.""" """Exception raised when we want to leave a mode we're not in."""
@ -95,7 +122,7 @@ def maybe_leave(win_id, mode, reason=None):
class EventFilter(QObject): class EventFilter(QObject):
"""Event filter which passes the event to the corrent ModeManager.""" """Event filter which passes the event to the current ModeManager."""
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
@ -143,7 +170,7 @@ class ModeManager(QObject):
_win_id: The window ID of this ModeManager _win_id: The window ID of this ModeManager
_handlers: A dictionary of modes and their handlers. _handlers: A dictionary of modes and their handlers.
_forward_unbound_keys: If we should forward unbound keys. _forward_unbound_keys: If we should forward unbound keys.
_releaseevents_to_pass: A list of keys where the keyPressEvent was _releaseevents_to_pass: A set of KeyEvents where the keyPressEvent was
passed through, so the release event should as passed through, so the release event should as
well. well.
@ -166,7 +193,7 @@ class ModeManager(QObject):
self._handlers = {} self._handlers = {}
self.passthrough = [] self.passthrough = []
self.mode = usertypes.KeyMode.normal self.mode = usertypes.KeyMode.normal
self._releaseevents_to_pass = [] self._releaseevents_to_pass = set()
self._forward_unbound_keys = config.get( self._forward_unbound_keys = config.get(
'input', 'forward-unbound-keys') 'input', 'forward-unbound-keys')
objreg.get('config').changed.connect(self.set_forward_unbound_keys) objreg.get('config').changed.connect(self.set_forward_unbound_keys)
@ -207,7 +234,7 @@ class ModeManager(QObject):
filter_this = True filter_this = True
if not filter_this: if not filter_this:
self._releaseevents_to_pass.append(event) self._releaseevents_to_pass.add(KeyEvent(event))
if curmode != usertypes.KeyMode.insert: if curmode != usertypes.KeyMode.insert:
log.modes.debug("handled: {}, forward-unbound-keys: {}, " log.modes.debug("handled: {}, forward-unbound-keys: {}, "
@ -228,10 +255,9 @@ class ModeManager(QObject):
True if event should be filtered, False otherwise. True if event should be filtered, False otherwise.
""" """
# handle like matching KeyPress # handle like matching KeyPress
if event in self._releaseevents_to_pass: keyevent = KeyEvent(event)
# remove all occurences if keyevent in self._releaseevents_to_pass:
self._releaseevents_to_pass = [ self._releaseevents_to_pass.remove(keyevent)
e for e in self._releaseevents_to_pass if e != event]
filter_this = False filter_this = False
else: else:
filter_this = True filter_this = True

View File

@ -43,7 +43,7 @@ class MainWindow(QWidget):
"""The main window of qutebrowser. """The main window of qutebrowser.
Adds all needed components to a vbox, initializes subwidgets and connects Adds all needed components to a vbox, initializes sub-widgets and connects
signals. signals.
Attributes: Attributes:
@ -242,6 +242,8 @@ class MainWindow(QWidget):
tabs.current_tab_changed.connect(status.percentage.on_tab_changed) tabs.current_tab_changed.connect(status.percentage.on_tab_changed)
tabs.cur_scroll_perc_changed.connect(status.percentage.set_perc) tabs.cur_scroll_perc_changed.connect(status.percentage.set_perc)
tabs.tab_index_changed.connect(status.tabindex.on_tab_index_changed)
tabs.current_tab_changed.connect(status.txt.on_tab_changed) tabs.current_tab_changed.connect(status.txt.on_tab_changed)
tabs.cur_statusbar_message.connect(status.txt.on_statusbar_message) tabs.cur_statusbar_message.connect(status.txt.on_statusbar_message)
tabs.cur_load_started.connect(status.txt.on_load_started) tabs.cur_load_started.connect(status.txt.on_load_started)

View File

@ -28,7 +28,8 @@ from PyQt5.QtWidgets import QWidget, QHBoxLayout, QStackedLayout, QSizePolicy
from qutebrowser.config import config, style from qutebrowser.config import config, style
from qutebrowser.utils import usertypes, log, objreg, utils from qutebrowser.utils import usertypes, log, objreg, utils
from qutebrowser.mainwindow.statusbar import (command, progress, keystring, from qutebrowser.mainwindow.statusbar import (command, progress, keystring,
percentage, url, prompt) percentage, url, prompt,
tabindex)
from qutebrowser.mainwindow.statusbar import text as textwidget from qutebrowser.mainwindow.statusbar import text as textwidget
@ -174,6 +175,9 @@ class StatusBar(QWidget):
self.percentage = percentage.Percentage() self.percentage = percentage.Percentage()
self._hbox.addWidget(self.percentage) self._hbox.addWidget(self.percentage)
self.tabindex = tabindex.TabIndex()
self._hbox.addWidget(self.tabindex)
# We add a parent to Progress here because it calls self.show() based # We add a parent to Progress here because it calls self.show() based
# on some signals, and if that happens before it's added to the layout, # on some signals, and if that happens before it's added to the layout,
# it will quickly blink up as independent window. # it will quickly blink up as independent window.

View File

@ -98,7 +98,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
@cmdutils.register(instance='status-command', name='set-cmd-text', @cmdutils.register(instance='status-command', name='set-cmd-text',
scope='window', maxsplit=0) scope='window', maxsplit=0)
def set_cmd_text_command(self, text): def set_cmd_text_command(self, text, space=False):
"""Preset the statusbar to some text. """Preset the statusbar to some text.
// //
@ -108,6 +108,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
Args: Args:
text: The commandline to set. text: The commandline to set.
space: If given, a space is added to the end.
""" """
tabbed_browser = objreg.get('tabbed-browser', scope='window', tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id) window=self._win_id)
@ -127,7 +128,9 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
# I'm not sure what's the best thing to do here # I'm not sure what's the best thing to do here
# https://github.com/The-Compiler/qutebrowser/issues/123 # https://github.com/The-Compiler/qutebrowser/issues/123
text = text.replace('{url}', url) text = text.replace('{url}', url)
if not text[0] in modeparsers.STARTCHARS: if space:
text += ' '
if not text or text[0] not in modeparsers.STARTCHARS:
raise cmdexc.CommandError( raise cmdexc.CommandError(
"Invalid command text '{}'.".format(text)) "Invalid command text '{}'.".format(text))
self.set_cmd_text(text) self.set_cmd_text(text)
@ -179,7 +182,7 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
def on_mode_left(self, mode): def on_mode_left(self, mode):
"""Clear up when command mode was left. """Clear up when command mode was left.
- Clear the statusbar text if it's explicitely unfocused. - Clear the statusbar text if it's explicitly unfocused.
- Clear completion selection - Clear completion selection
- Hide completion - Hide completion

View File

@ -0,0 +1,34 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""TabIndex displayed in the statusbar."""
from PyQt5.QtCore import pyqtSlot
from qutebrowser.mainwindow.statusbar import textbase
class TabIndex(textbase.TextBase):
"""Shows current tab index and number of tabs in the statusbar."""
@pyqtSlot(int, int)
def on_tab_index_changed(self, current, count):
"""Update tab index when tab changed."""
self.setText('[{}/{}]'.format(current + 1, count))

View File

@ -32,7 +32,7 @@ class TextBase(QLabel):
Unlike QLabel, the text will get elided. Unlike QLabel, the text will get elided.
Eliding is loosly based on Eliding is loosely based on
http://gedgedev.blogspot.ch/2010/12/elided-labels-in-qt.html http://gedgedev.blogspot.ch/2010/12/elided-labels-in-qt.html
Attributes: Attributes:
@ -64,7 +64,7 @@ class TextBase(QLabel):
This update the elided text after setting the text, and also works This update the elided text after setting the text, and also works
around a weird QLabel redrawing bug where it doesn't redraw correctly around a weird QLabel redrawing bug where it doesn't redraw correctly
when the text is empty -- we explicitely need to call repaint() to when the text is empty -- we explicitly need to call repaint() to
resolve this. resolve this.
More info: More info:

View File

@ -40,7 +40,7 @@ class UrlText(textbase.TextBase):
_normal_url: The normal URL to be displayed as a UrlType instance. _normal_url: The normal URL to be displayed as a UrlType instance.
_normal_url_type: The type of the normal URL as a UrlType instance. _normal_url_type: The type of the normal URL as a UrlType instance.
_hover_url: The URL we're currently hovering over. _hover_url: The URL we're currently hovering over.
_ssl_errors: Whether SSL errors occured while loading. _ssl_errors: Whether SSL errors occurred while loading.
Class attributes: Class attributes:
_urltype: The URL type to show currently (normal/ok/error/warn/hover). _urltype: The URL type to show currently (normal/ok/error/warn/hover).

View File

@ -48,12 +48,12 @@ class TabbedBrowser(tabwidget.TabWidget):
"""A TabWidget with QWebViews inside. """A TabWidget with QWebViews inside.
Provides methods to manage tabs, convenience methods to interact with the Provides methods to manage tabs, convenience methods to interact with the
current tab (cur_*) and filters signals to re-emit them when they occured current tab (cur_*) and filters signals to re-emit them when they occurred
in the currently visible tab. in the currently visible tab.
For all tab-specific signals (cur_*) emitted by a tab, this happens: For all tab-specific signals (cur_*) emitted by a tab, this happens:
- the signal gets filtered with _filter_signals and self.cur_* gets - the signal gets filtered with _filter_signals and self.cur_* gets
emitted if the signal occured in the current tab. emitted if the signal occurred in the current tab.
Attributes: Attributes:
_win_id: The window ID this tabbedbrowser is associated with. _win_id: The window ID this tabbedbrowser is associated with.
@ -331,7 +331,7 @@ class TabbedBrowser(tabwidget.TabWidget):
url: The URL to open as QUrl or None for an empty tab. url: The URL to open as QUrl or None for an empty tab.
background: Whether to open the tab in the background. background: Whether to open the tab in the background.
if None, the background-tabs setting decides. if None, the background-tabs setting decides.
explicit: Whether the tab was opened explicitely. explicit: Whether the tab was opened explicitly.
If this is set, the new position might be different. With If this is set, the new position might be different. With
the default settings we handle it like Chromium does: the default settings we handle it like Chromium does:
- Tabs from clicked links etc. are to the right of - Tabs from clicked links etc. are to the right of
@ -368,7 +368,7 @@ class TabbedBrowser(tabwidget.TabWidget):
"""Get the index of a tab to insert. """Get the index of a tab to insert.
Args: Args:
explicit: Whether the tab was opened explicitely. explicit: Whether the tab was opened explicitly.
Return: Return:
The index of the new tab. The index of the new tab.
@ -590,7 +590,7 @@ class TabbedBrowser(tabwidget.TabWidget):
except TabDeletedError: except TabDeletedError:
# We can get signals for tabs we already deleted... # We can get signals for tabs we already deleted...
return return
if tab.page().error_occured: if tab.page().error_occurred:
color = config.get('colors', 'tabs.indicator.error') color = config.get('colors', 'tabs.indicator.error')
else: else:
start = config.get('colors', 'tabs.indicator.start') start = config.get('colors', 'tabs.indicator.start')

View File

@ -26,7 +26,7 @@ Module attributes:
import functools import functools
from PyQt5.QtCore import pyqtSlot, Qt, QSize, QRect, QPoint, QTimer from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QSize, QRect, QPoint, QTimer
from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle, from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle,
QStyle, QStylePainter, QStyleOptionTab) QStyle, QStylePainter, QStyleOptionTab)
from PyQt5.QtGui import QIcon, QPalette, QColor from PyQt5.QtGui import QIcon, QPalette, QColor
@ -41,7 +41,15 @@ PM_TabBarPadding = QStyle.PM_CustomBase
class TabWidget(QTabWidget): class TabWidget(QTabWidget):
"""The tabwidget used for TabbedBrowser.""" """The tab widget used for TabbedBrowser.
Signals:
tab_index_changed: Emitted when the current tab was changed.
arg 0: The index of the tab which is now focused.
arg 1: The total count of tabs.
"""
tab_index_changed = pyqtSignal(int, int)
def __init__(self, win_id, parent=None): def __init__(self, win_id, parent=None):
super().__init__(parent) super().__init__(parent)
@ -50,6 +58,7 @@ class TabWidget(QTabWidget):
bar.tabCloseRequested.connect(self.tabCloseRequested) bar.tabCloseRequested.connect(self.tabCloseRequested)
bar.tabMoved.connect(functools.partial( bar.tabMoved.connect(functools.partial(
QTimer.singleShot, 0, self.update_tab_titles)) QTimer.singleShot, 0, self.update_tab_titles))
bar.currentChanged.connect(self.emit_tab_index_changed)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.setDocumentMode(True) self.setDocumentMode(True)
self.setElideMode(Qt.ElideRight) self.setElideMode(Qt.ElideRight)
@ -65,10 +74,10 @@ class TabWidget(QTabWidget):
self.setMovable(config.get('tabs', 'movable')) self.setMovable(config.get('tabs', 'movable'))
self.setTabsClosable(False) self.setTabsClosable(False)
position = config.get('tabs', 'position') position = config.get('tabs', 'position')
selection_behaviour = config.get('tabs', 'select-on-remove') selection_behavior = config.get('tabs', 'select-on-remove')
self.setTabPosition(position) self.setTabPosition(position)
tabbar.vertical = position in (QTabWidget.West, QTabWidget.East) tabbar.vertical = position in (QTabWidget.West, QTabWidget.East)
tabbar.setSelectionBehaviorOnRemove(selection_behaviour) tabbar.setSelectionBehaviorOnRemove(selection_behavior)
tabbar.refresh() tabbar.refresh()
def set_tab_indicator_color(self, idx, color): def set_tab_indicator_color(self, idx, color):
@ -84,7 +93,7 @@ class TabWidget(QTabWidget):
def set_page_title(self, idx, title): def set_page_title(self, idx, title):
"""Set the tab title user data.""" """Set the tab title user data."""
self.tabBar().set_tab_data(idx, 'page-title', title.replace('&', '&&')) self.tabBar().set_tab_data(idx, 'page-title', title)
self.update_tab_title(idx) self.update_tab_title(idx)
def page_title(self, idx): def page_title(self, idx):
@ -94,7 +103,7 @@ class TabWidget(QTabWidget):
def update_tab_title(self, idx): def update_tab_title(self, idx):
"""Update the tab text for the given tab.""" """Update the tab text for the given tab."""
widget = self.widget(idx) widget = self.widget(idx)
page_title = self.page_title(idx) page_title = self.page_title(idx).replace('&', '&&')
fields = {} fields = {}
if widget.load_status == webview.LoadStatus.loading: if widget.load_status == webview.LoadStatus.loading:
@ -184,6 +193,11 @@ class TabWidget(QTabWidget):
self.set_page_title(new_idx, text) self.set_page_title(new_idx, text)
return new_idx return new_idx
@pyqtSlot(int)
def emit_tab_index_changed(self, index):
"""Emit the tab_index_changed signal if the current tab changed."""
self.tab_index_changed.emit(index, self.count())
class TabBar(QTabBar): class TabBar(QTabBar):
@ -264,7 +278,7 @@ class TabBar(QTabBar):
Args: Args:
idx: The tab index to get the title for. idx: The tab index to get the title for.
handle_unset: Whether to return an emtpy string on KeyError. handle_unset: Whether to return an empty string on KeyError.
""" """
try: try:
return self.tab_data(idx, 'page-title') return self.tab_data(idx, 'page-title')

View File

@ -149,7 +149,7 @@ class ConsoleWidget(QWidget):
_output: The output widget in the console. _output: The output widget in the console.
_vbox: The layout which contains everything. _vbox: The layout which contains everything.
_more: A flag which is set when more input is expected. _more: A flag which is set when more input is expected.
_buffer: The buffer for multiline commands. _buffer: The buffer for multi-line commands.
_interpreter: The InteractiveInterpreter to execute code with. _interpreter: The InteractiveInterpreter to execute code with.
""" """
@ -201,7 +201,7 @@ class ConsoleWidget(QWidget):
# printed elsewhere (e.g. by exec). Other Python GUI shells do the # printed elsewhere (e.g. by exec). Other Python GUI shells do the
# same. # same.
# - We disable our exception hook, so exceptions from the console get # - We disable our exception hook, so exceptions from the console get
# printed and don't ooen a crashdialog. # printed and don't open a crashdialog.
with utils.fake_io(self.write), utils.disabled_excepthook(): with utils.fake_io(self.write), utils.disabled_excepthook():
self._more = self._interpreter.runsource(source, '<console>') self._more = self._interpreter.runsource(source, '<console>')
self.write(self._curprompt()) self.write(self._curprompt())

View File

@ -82,7 +82,7 @@ def get_fatal_crash_dialog(debug, data):
else: else:
title = "qutebrowser was restarted after a fatal crash!" title = "qutebrowser was restarted after a fatal crash!"
text = ("<b>qutebrowser was restarted after a fatal crash!</b><br/>" text = ("<b>qutebrowser was restarted after a fatal crash!</b><br/>"
"Unfortunately, this crash occured in Qt (the library " "Unfortunately, this crash occurred in Qt (the library "
"qutebrowser uses), and your version ({}) is outdated - " "qutebrowser uses), and your version ({}) is outdated - "
"Qt 5.4 or later is recommended. Unfortuntately Debian and " "Qt 5.4 or later is recommended. Unfortuntately Debian and "
"Ubuntu don't ship a newer version (yet?)...".format( "Ubuntu don't ship a newer version (yet?)...".format(
@ -403,12 +403,12 @@ class ExceptionCrashDialog(_CrashDialog):
class FatalCrashDialog(_CrashDialog): class FatalCrashDialog(_CrashDialog):
"""Dialog which gets shown when a fatal error occured. """Dialog which gets shown when a fatal error occurred.
Attributes: Attributes:
_log: The log text to display. _log: The log text to display.
_type: The type of error which occured. _type: The type of error which occurred.
_func: The function (top of the stack) in which the error occured. _func: The function (top of the stack) in which the error occurred.
_chk_history: A checkbox for the user to decide if page history should _chk_history: A checkbox for the user to decide if page history should
be sent. be sent.
""" """

View File

@ -20,6 +20,13 @@
At this point we can be sure we have all python 3.4 features available. At this point we can be sure we have all python 3.4 features available.
""" """
try:
# Importing hunter to register its atexit handler early so it gets called
# late.
import hunter # pylint: disable=import-error,unused-import
except ImportError:
hunter = None
import os import os
import sys import sys
import faulthandler import faulthandler
@ -32,7 +39,7 @@ try:
except ImportError: except ImportError:
tkinter = None tkinter = None
# NOTE: No qutebrowser or PyQt import should be done here, as some early # NOTE: No qutebrowser or PyQt import should be done here, as some early
# initialisation needs to take place before that! # initialization needs to take place before that!
def _missing_str(name, *, windows=None, pip=None): def _missing_str(name, *, windows=None, pip=None):
@ -90,7 +97,7 @@ def _die(message, exception=None):
def init_faulthandler(fileobj=sys.__stderr__): def init_faulthandler(fileobj=sys.__stderr__):
"""Enable faulthandler module if available. """Enable faulthandler module if available.
This print a nice traceback on segfauls. This print a nice traceback on segfaults.
We use sys.__stderr__ instead of sys.stderr here so this will still work We use sys.__stderr__ instead of sys.stderr here so this will still work
when sys.stderr got replaced, e.g. by "Python Tools for Visual Studio". when sys.stderr got replaced, e.g. by "Python Tools for Visual Studio".
@ -156,7 +163,7 @@ def fix_harfbuzz(args):
elif args.harfbuzz in ('old', 'new'): elif args.harfbuzz in ('old', 'new'):
# forced harfbuzz variant # forced harfbuzz variant
# FIXME looking at the Qt code, 'new' isn't a valid value, but leaving # FIXME looking at the Qt code, 'new' isn't a valid value, but leaving
# it empty and using new yields different behaviour... # it empty and using new yields different behavior...
# (probably irrelevant when workaround gets removed) # (probably irrelevant when workaround gets removed)
log.init.debug("Using {} harfbuzz engine (forced)".format( log.init.debug("Using {} harfbuzz engine (forced)".format(
args.harfbuzz)) args.harfbuzz))
@ -256,7 +263,7 @@ def init_log(args):
def earlyinit(args): def earlyinit(args):
"""Do all needed early initialisation. """Do all needed early initialization.
Note that it's vital the other earlyinit functions get called in the right Note that it's vital the other earlyinit functions get called in the right
order! order!
@ -265,7 +272,7 @@ def earlyinit(args):
args: The argparse namespace. args: The argparse namespace.
""" """
# First we initialize the faulthandler as early as possible, so we # First we initialize the faulthandler as early as possible, so we
# theoretically could catch segfaults occuring later during earlyinit. # theoretically could catch segfaults occurring later during earlyinit.
init_faulthandler() init_faulthandler()
# Here we check if QtCore is available, and if not, print a message to the # Here we check if QtCore is available, and if not, print a message to the
# console or via Tk. # console or via Tk.

View File

@ -127,7 +127,7 @@ class _CommandValidator(QValidator):
Args: Args:
string: The string to validate. string: The string to validate.
pos: The current curser position. pos: The current cursor position.
Return: Return:
A tuple (status, string, pos) as a QValidator should. A tuple (status, string, pos) as a QValidator should.

View File

@ -22,7 +22,7 @@
import os.path import os.path
import collections import collections
from PyQt5.QtCore import pyqtSlot, QObject from PyQt5.QtCore import pyqtSlot, QObject, QTimer
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.commands import cmdutils from qutebrowser.commands import cmdutils
@ -39,7 +39,8 @@ class Saveable:
_save_handler: The function to call to save this Saveable. _save_handler: The function to call to save this Saveable.
_save_on_exit: Whether to always save this saveable on exit. _save_on_exit: Whether to always save this saveable on exit.
_config_opt: A (section, option) tuple of a config option which decides _config_opt: A (section, option) tuple of a config option which decides
whether to autosave or not. None if no such option exists. whether to auto-save or not. None if no such option
exists.
_filename: The filename of the underlying file. _filename: The filename of the underlying file.
""" """
@ -77,7 +78,7 @@ class Saveable:
Args: Args:
is_exit: Whether we're currently exiting qutebrowser. is_exit: Whether we're currently exiting qutebrowser.
explicit: Whether the user explicitely requested this save. explicit: Whether the user explicitly requested this save.
silent: Don't write informations to log. silent: Don't write informations to log.
force: Force saving, no matter what. force: Force saving, no matter what.
""" """
@ -119,7 +120,7 @@ class SaveManager(QObject):
return utils.get_repr(self, saveables=self.saveables) return utils.get_repr(self, saveables=self.saveables)
def init_autosave(self): def init_autosave(self):
"""Initialize autosaving. """Initialize auto-saving.
We don't do this in __init__ because the config needs to be initialized We don't do this in __init__ because the config needs to be initialized
first, but the config needs the save manager. first, but the config needs the save manager.
@ -129,7 +130,7 @@ class SaveManager(QObject):
@config.change_filter('general', 'auto-save-interval') @config.change_filter('general', 'auto-save-interval')
def set_autosave_interval(self): def set_autosave_interval(self):
"""Set the autosave interval.""" """Set the auto-save interval."""
interval = config.get('general', 'auto-save-interval') interval = config.get('general', 'auto-save-interval')
if interval == 0: if interval == 0:
self._save_timer.stop() self._save_timer.stop()
@ -138,22 +139,26 @@ class SaveManager(QObject):
self._save_timer.start() self._save_timer.start()
def add_saveable(self, name, save, changed=None, config_opt=None, def add_saveable(self, name, save, changed=None, config_opt=None,
filename=None): filename=None, dirty=False):
"""Add a new saveable. """Add a new saveable.
Args: Args:
name: The name to use. name: The name to use.
save: The function to call to save this saveable. save: The function to call to save this saveable.
changed: The signal emitted when this saveable changed. changed: The signal emitted when this saveable changed.
config_opt: A (section, option) tuple deciding whether to autosave config_opt: A (section, option) tuple deciding whether to auto-save
or not. or not.
filename: The filename of the underlying file, so we can force filename: The filename of the underlying file, so we can force
saving if it doesn't exist. saving if it doesn't exist.
dirty: Whether the saveable is already dirty.
""" """
if name in self.saveables: if name in self.saveables:
raise ValueError("Saveable {} already registered!".format(name)) raise ValueError("Saveable {} already registered!".format(name))
self.saveables[name] = Saveable(name, save, changed, config_opt, saveable = Saveable(name, save, changed, config_opt, filename)
filename) self.saveables[name] = saveable
if dirty:
saveable.mark_dirty()
QTimer.singleShot(0, saveable.save)
def save(self, name, is_exit=False, explicit=False, silent=False, def save(self, name, is_exit=False, explicit=False, silent=False,
force=False): force=False):
@ -161,7 +166,7 @@ class SaveManager(QObject):
Args: Args:
is_exit: Whether we're currently exiting qutebrowser. is_exit: Whether we're currently exiting qutebrowser.
explicit: Whether this save operation was triggered explicitely. explicit: Whether this save operation was triggered explicitly.
silent: Don't write informations to log. Used to reduce log spam silent: Don't write informations to log. Used to reduce log spam
when autosaving. when autosaving.
force: Force saving, no matter what. force: Force saving, no matter what.

View File

@ -21,7 +21,6 @@
import os import os
import os.path import os.path
import configparser
from PyQt5.QtCore import pyqtSignal, QUrl, QObject, QPoint, QTimer from PyQt5.QtCore import pyqtSignal, QUrl, QObject, QPoint, QTimer
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
@ -185,10 +184,6 @@ class SessionManager(QObject):
self.update_completion.emit() self.update_completion.emit()
if load_next_time: if load_next_time:
state_config = objreg.get('state-config') state_config = objreg.get('state-config')
try:
state_config.add_section('general')
except configparser.DuplicateSectionError:
pass
state_config['general']['session'] = name state_config['general']['session'] = name
def save_last_window_session(self): def save_last_window_session(self):
@ -266,13 +261,18 @@ class SessionManager(QObject):
@cmdutils.register(completion=[usertypes.Completion.sessions], @cmdutils.register(completion=[usertypes.Completion.sessions],
instance='session-manager') instance='session-manager')
def session_load(self, name, clear=False): def session_load(self, name, clear=False, force=False):
"""Load a session. """Load a session.
Args: Args:
name: The name of the session. name: The name of the session.
clear: Close all existing windows. clear: Close all existing windows.
force: Force loading internal sessions (starting with an
underline).
""" """
if name.startswith('_') and not force:
raise cmdexc.CommandError("{!r} is an internal session, use "
"--force to load anyways.".format(name))
old_windows = list(objreg.window_registry.values()) old_windows = list(objreg.window_registry.values())
try: try:
self.load(name) self.load(name)
@ -290,14 +290,18 @@ class SessionManager(QObject):
completion=[usertypes.Completion.sessions], completion=[usertypes.Completion.sessions],
instance='session-manager') instance='session-manager')
def session_save(self, win_id: {'special': 'win_id'}, name='default', def session_save(self, win_id: {'special': 'win_id'}, name='default',
quiet=False): quiet=False, force=False):
"""Save a session. """Save a session.
Args: Args:
win_id: The current window ID. win_id: The current window ID.
name: The name of the session. name: The name of the session.
quiet: Don't show confirmation message. quiet: Don't show confirmation message.
force: Force saving internal sessions (starting with an underline).
""" """
if name.startswith('_') and not force:
raise cmdexc.CommandError("{!r} is an internal session, use "
"--force to save anyways.".format(name))
try: try:
self.save(name) self.save(name)
except SessionError as e: except SessionError as e:
@ -310,12 +314,18 @@ class SessionManager(QObject):
@cmdutils.register(completion=[usertypes.Completion.sessions], @cmdutils.register(completion=[usertypes.Completion.sessions],
instance='session-manager') instance='session-manager')
def session_delete(self, name): def session_delete(self, name, force=False):
"""Delete a session. """Delete a session.
Args: Args:
name: The name of the session. name: The name of the session.
force: Force deleting internal sessions (starting with an
underline).
""" """
if name.startswith('_') and not force:
raise cmdexc.CommandError("{!r} is an internal session, use "
"--force to delete anyways.".format(
name))
try: try:
self.delete(name) self.delete(name)
except OSError as e: except OSError as e:

View File

@ -190,7 +190,7 @@ def simple_split(s, keep=False, maxsplit=None):
whitespace = '\n\t ' whitespace = '\n\t '
if maxsplit == 0: if maxsplit == 0:
# re.split with maxsplit=0 splits everything, while str.split splits # re.split with maxsplit=0 splits everything, while str.split splits
# nothing (which is the behaviour we want). # nothing (which is the behavior we want).
if keep: if keep:
return [s] return [s]
else: else:

View File

@ -19,11 +19,14 @@
"""Misc. utility commands exposed to the user.""" """Misc. utility commands exposed to the user."""
import configparser
import functools import functools
import types import types
from PyQt5.QtCore import QCoreApplication from PyQt5.QtCore import QCoreApplication
try:
import hunter
except ImportError:
hunter = None
from qutebrowser.utils import log, objreg, usertypes from qutebrowser.utils import log, objreg, usertypes
from qutebrowser.commands import cmdutils, runners, cmdexc from qutebrowser.commands import cmdutils, runners, cmdexc
@ -119,14 +122,17 @@ def debug_console():
con_widget.show() con_widget.show()
@cmdutils.register(hide=True) @cmdutils.register(debug=True, maxsplit=0)
def fooled(): def debug_trace(expr=""):
"""Turn off april's fools.""" """Trace executed code via hunter.
from qutebrowser.config import websettings
state_config = objreg.get('state-config') Args:
expr: What to trace, passed to hunter.
"""
if hunter is None:
raise cmdexc.CommandError("You need to install 'hunter' to use this "
"command!")
try: try:
state_config.add_section('general') eval('hunter.trace({})'.format(expr))
except configparser.DuplicateSectionError: except Exception as e:
pass raise cmdexc.CommandError("{}: {}".format(e.__class__.__name__, e))
state_config['general']['fooled'] = '1'
websettings.update_settings('ui', 'user-stylesheet')

View File

@ -20,6 +20,7 @@
"""Early initialization and main entry point.""" """Early initialization and main entry point."""
import sys import sys
import json
import qutebrowser import qutebrowser
try: try:
@ -58,6 +59,7 @@ def get_argparser():
parser.add_argument('-R', '--override-restore', help="Don't restore a " parser.add_argument('-R', '--override-restore', help="Don't restore a "
"session even if one would be restored.", "session even if one would be restored.",
action='store_true') action='store_true')
parser.add_argument('--json-args', help=argparse.SUPPRESS)
debug = parser.add_argument_group('debug arguments') debug = parser.add_argument_group('debug arguments')
debug.add_argument('-l', '--loglevel', dest='loglevel', debug.add_argument('-l', '--loglevel', dest='loglevel',
@ -118,6 +120,13 @@ def main():
"""Main entry point for qutebrowser.""" """Main entry point for qutebrowser."""
parser = get_argparser() parser = get_argparser()
args = parser.parse_args() args = parser.parse_args()
if args.json_args is not None:
# Restoring after a restart.
# When restarting, we serialize the argparse namespace into json, and
# construct a "fake" argparse.Namespace here based on the data loaded
# from json.
data = json.loads(args.json_args)
args = argparse.Namespace(**data)
earlyinit.earlyinit(args) earlyinit.earlyinit(args)
# We do this imports late as earlyinit needs to be run first (because of # We do this imports late as earlyinit needs to be run first (because of
# the harfbuzz fix and version checking). # the harfbuzz fix and version checking).
@ -133,7 +142,7 @@ def main():
""" """
return app.exec_() return app.exec_()
# We set qApp explicitely here to reduce the risk of segfaults while # We set qApp explicitly here to reduce the risk of segfaults while
# quitting. # quitting.
# See https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/561303/comments/7 # See https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/561303/comments/7
# While this is a workaround for PyQt4 which should be fixed in PyQt, it # While this is a workaround for PyQt4 which should be fixed in PyQt, it

View File

@ -20,7 +20,6 @@
"""Utilities used for debugging.""" """Utilities used for debugging."""
import re import re
import sys
import inspect import inspect
import functools import functools
import datetime import datetime
@ -87,36 +86,6 @@ def log_signals(obj):
connect_log_slot(obj) connect_log_slot(obj)
def trace_lines(do_trace):
"""Turn on/off printing each executed line.
Args:
do_trace: Whether to start tracing (True) or stop it (False).
"""
def trace(frame, event, arg):
"""Trace function passed to sys.settrace.
Return:
Itself, so tracing continues.
"""
if sys is not None:
loc = '{}:{}'.format(frame.f_code.co_filename, frame.f_lineno)
if arg is not None:
arg = utils.compact_text(str(arg), 200)
else:
arg = ''
print("{:11} {:80} {}".format(event, loc, arg), file=sys.stderr)
return trace
else:
# When tracing while shutting down, it seems sys can be None
# sometimes... if that's the case, we stop tracing.
return None
if do_trace:
sys.settrace(trace)
else:
sys.settrace(None)
def qenum_key(base, value, add_base=False, klass=None): def qenum_key(base, value, add_base=False, klass=None):
"""Convert a Qt Enum value to its key as a string. """Convert a Qt Enum value to its key as a string.

View File

@ -49,7 +49,7 @@ class Loader(jinja2.BaseLoader):
def _guess_autoescape(template_name): def _guess_autoescape(template_name):
"""Turn autoescape on/off based on the filetype. """Turn auto-escape on/off based on the file type.
Based on http://jinja.pocoo.org/docs/dev/api/#autoescaping Based on http://jinja.pocoo.org/docs/dev/api/#autoescaping
""" """

View File

@ -97,7 +97,12 @@ def on_focus_changed():
delta = datetime.datetime.now() - msg.time delta = datetime.datetime.now() - msg.time
log.misc.debug("Handling queued {} for window {}, delta {}".format( log.misc.debug("Handling queued {} for window {}, delta {}".format(
msg.method_name, msg.win_id, delta)) msg.method_name, msg.win_id, delta))
try:
bridge = _get_bridge(msg.win_id) bridge = _get_bridge(msg.win_id)
except objreg.RegistryUnavailableError:
# Non-mainwindow window focused.
_QUEUED.append(msg)
return
if delta.total_seconds() < 1: if delta.total_seconds() < 1:
text = msg.text text = msg.text
else: else:

View File

@ -77,7 +77,7 @@ def _from_args(typ, args):
Return: Return:
A (override, path) tuple. A (override, path) tuple.
override: boolean, if the user did override the path override: boolean, if the user did override the path
path: The overriden path, or None to turn off storage. path: The overridden path, or None to turn off storage.
""" """
typ_to_argparse_arg = { typ_to_argparse_arg = {
QStandardPaths.ConfigLocation: 'confdir' QStandardPaths.ConfigLocation: 'confdir'

View File

@ -40,7 +40,7 @@ def enum(name, items, start=1, is_int=False):
Args: Args:
name: Name of the enum name: Name of the enum
items: Iterable of ttems to be sequentally enumerated. items: Iterable of items to be sequentially enumerated.
start: The number to use for the first value. start: The number to use for the first value.
We use 1 as default so enum members are always True. We use 1 as default so enum members are always True.
is_init: True if the enum should be a Python IntEnum is_init: True if the enum should be a Python IntEnum
@ -309,7 +309,7 @@ class Question(QObject):
@pyqtSlot() @pyqtSlot()
def done(self): def done(self):
"""Must be called when the queston was answered completely.""" """Must be called when the question was answered completely."""
self.answered.emit(self.answer) self.answered.emit(self.answer)
if self.mode == PromptMode.yesno: if self.mode == PromptMode.yesno:
if self.answer: if self.answer:

View File

@ -95,7 +95,7 @@ def read_file(filename, binary=False):
def actute_warning(): def actute_warning():
"""Display a warning about the dead_actute issue if needed.""" """Display a warning about the dead_actute issue if needed."""
# WORKAROUND (remove this when we bump the requirements to 5.3.0) # WORKAROUND (remove this when we bump the requirements to 5.3.0)
# Non linux OS' aren't affected # Non Linux OS' aren't affected
if not sys.platform.startswith('linux'): if not sys.platform.startswith('linux'):
return return
# If no compose file exists for some reason, we're not affected # If no compose file exists for some reason, we're not affected
@ -435,7 +435,7 @@ class prevent_exceptions: # pylint: disable=invalid-name
silently ignores them. silently ignores them.
We used to re-raise the exception with a single-shot QTimer in a similar We used to re-raise the exception with a single-shot QTimer in a similar
case, but that lead to a strange proble with a KeyError with some random case, but that lead to a strange problem with a KeyError with some random
jinja template stuff as content. For now, we only log it, so it doesn't jinja template stuff as content. For now, we only log it, so it doesn't
pass 100% silently. pass 100% silently.

View File

@ -21,20 +21,29 @@
"""Various small code checkers.""" """Various small code checkers."""
import os import os
import re
import sys import sys
import os.path import os.path
import argparse import argparse
import subprocess import subprocess
import tokenize import tokenize
import traceback import traceback
import collections
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from scripts import utils from scripts import utils
def _py_files(target):
"""Iterate over all python files and yield filenames."""
for (dirpath, _dirnames, filenames) in os.walk(target):
for name in (e for e in filenames if e.endswith('.py')):
yield os.path.join(dirpath, name)
def check_git(): def check_git():
"""Check for uncommited git files..""" """Check for uncommitted git files.."""
if not os.path.isdir(".git"): if not os.path.isdir(".git"):
print("No .git dir, ignoring") print("No .git dir, ignoring")
print() print()
@ -55,13 +64,44 @@ def check_git():
return status return status
def check_spelling(target):
"""Check commonly misspelled words."""
# Words which I often misspell
words = {'behaviour', 'quitted', 'likelyhood', 'sucessfully',
'occur[^r .]', 'seperator', 'explicitely', 'resetted',
'auxillary', 'accidentaly', 'ambigious', 'loosly',
'initialis', 'convienence', 'similiar', 'uncommited',
'reproducable'}
# Words which look better when splitted, but might need some fine tuning.
words |= {'keystrings', 'webelements', 'mouseevent', 'keysequence',
'normalmode', 'eventloops', 'sizehint', 'statemachine',
'metaobject', 'logrecord', 'monkeypatch', 'filetype'}
seen = collections.defaultdict(list)
try:
ok = True
for fn in _py_files(target):
with tokenize.open(fn) as f:
if fn == os.path.join('scripts', 'misc_checks.py'):
continue
for line in f:
for w in words:
if re.search(w, line) and fn not in seen[w]:
print("Found '{}' in {}!".format(w, fn))
seen[w].append(fn)
print()
return ok
except Exception:
traceback.print_exc()
return None
def check_vcs_conflict(target): def check_vcs_conflict(target):
"""Check VCS conflict markers.""" """Check VCS conflict markers."""
try: try:
ok = True ok = True
for (dirpath, _dirnames, filenames) in os.walk(target): for fn in _py_files(target):
for name in (e for e in filenames if e.endswith('.py')):
fn = os.path.join(dirpath, name)
with tokenize.open(fn) as f: with tokenize.open(fn) as f:
for line in f: for line in f:
if any(line.startswith(c * 7) for c in '<>=|'): if any(line.startswith(c * 7) for c in '<>=|'):
@ -77,7 +117,7 @@ def check_vcs_conflict(target):
def main(): def main():
"""Main entry point.""" """Main entry point."""
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('checker', choices=('git', 'vcs'), parser.add_argument('checker', choices=('git', 'vcs', 'spelling'),
help="Which checker to run.") help="Which checker to run.")
parser.add_argument('target', help="What to check", nargs='*') parser.add_argument('target', help="What to check", nargs='*')
args = parser.parse_args() args = parser.parse_args()
@ -91,6 +131,13 @@ def main():
if not ok: if not ok:
is_ok = False is_ok = False
return 0 if is_ok else 1 return 0 if is_ok else 1
elif args.checker == 'spelling':
is_ok = True
for target in args.target:
ok = check_spelling(target)
if not ok:
is_ok = False
return 0 if is_ok else 1
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -74,7 +74,7 @@ def main():
('http://www.binpress.com/', False), ('http://www.binpress.com/', False),
('http://david.li/flow/', False), ('http://david.li/flow/', False),
('https://imzdl.com/', False), ('https://imzdl.com/', False),
# not reproducable # not reproducible
# https://bugreports.qt-project.org/browse/QTBUG-39847 # https://bugreports.qt-project.org/browse/QTBUG-39847
('http://www.20min.ch/', True), ('http://www.20min.ch/', True),
# HarfBuzz, https://bugreports.qt-project.org/browse/QTBUG-39278 # HarfBuzz, https://bugreports.qt-project.org/browse/QTBUG-39278

View File

@ -227,6 +227,8 @@ def _format_action_args(action):
def _format_action(action): def _format_action(action):
"""Get an invocation string/help from an argparse action.""" """Get an invocation string/help from an argparse action."""
if action.help == argparse.SUPPRESS:
return None
if not action.option_strings: if not action.option_strings:
invocation = '*{}*::'.format(_get_action_metavar(action)) invocation = '*{}*::'.format(_get_action_metavar(action))
else: else:
@ -396,7 +398,9 @@ def regenerate_manpage(filename):
if group.description is not None: if group.description is not None:
groupdata.append(group.description) groupdata.append(group.description)
for action in group._group_actions: for action in group._group_actions:
groupdata.append(_format_action(action)) action_data = _format_action(action)
if action_data is not None:
groupdata.append(action_data)
groups.append('\n'.join(groupdata)) groups.append('\n'.join(groupdata))
options = '\n'.join(groups) options = '\n'.join(groups)
# epilog # epilog

View File

@ -61,7 +61,7 @@ term_attributes = {
def _esc(code): def _esc(code):
"""Get an ANSII color code based on a color number.""" """Get an ANSI color code based on a color number."""
return '\033[{}m'.format(code) return '\033[{}m'.format(code)

View File

@ -124,7 +124,7 @@ class TestInline:
variation of the test checks whether whatever handles PDF display variation of the test checks whether whatever handles PDF display
receives the filename information, and acts upon it (this was tested receives the filename information, and acts upon it (this was tested
with the latest Acrobat Reader plugin, or, in the case of Chrome, using with the latest Acrobat Reader plugin, or, in the case of Chrome, using
the builtin PDF handler). the built-in PDF handler).
""" """
header_checker.check_filename('inline; filename="foo.pdf"', "foo.pdf", header_checker.check_filename('inline; filename="foo.pdf"', "foo.pdf",
expected_inline=True) expected_inline=True)

View File

@ -39,8 +39,8 @@ def get_webelem(geometry=None, frame=None, null=False, visibility='',
geometry: The geometry of the QWebElement as QRect. geometry: The geometry of the QWebElement as QRect.
frame: The QWebFrame the element is in. frame: The QWebFrame the element is in.
null: Whether the element is null or not. null: Whether the element is null or not.
visibility: The CSS visibility style property calue. visibility: The CSS visibility style property value.
display: The CSS display style property calue. display: The CSS display style property value.
attributes: Boolean HTML attributes to be added. attributes: Boolean HTML attributes to be added.
tagname: The tag name. tagname: The tag name.
classes: HTML classes to be added. classes: HTML classes to be added.

View File

@ -1810,7 +1810,7 @@ class TestSearchEngineUrl:
self.t.validate('http://example.com/?q={}') self.t.validate('http://example.com/?q={}')
def test_validate_invalid_url(self): def test_validate_invalid_url(self):
"""Test validate with an invalud URL.""" """Test validate with an invalid URL."""
with pytest.raises(configexc.ValidationError): with pytest.raises(configexc.ValidationError):
self.t.validate(':{}') self.t.validate(':{}')

View File

@ -210,11 +210,11 @@ class TestKeyChain:
self.kp.execute.assert_called_once_with('ba', self.kp.Type.chain, None) self.kp.execute.assert_called_once_with('ba', self.kp.Type.chain, None)
assert self.kp._keystring == '' assert self.kp._keystring == ''
def test_ambigious_keychain(self, fake_keyevent_factory, mocker, stubs): def test_ambiguous_keychain(self, fake_keyevent_factory, mocker, stubs):
"""Test ambigious keychain.""" """Test ambigious keychain."""
mocker.patch('qutebrowser.keyinput.basekeyparser.config', mocker.patch('qutebrowser.keyinput.basekeyparser.config',
new=stubs.ConfigStub(CONFIG)) new=stubs.ConfigStub(CONFIG))
timer = self.kp._ambigious_timer timer = self.kp._ambiguous_timer
assert not timer.isActive() assert not timer.isActive()
# We start with 'a' where the keychain gives us an ambigious result. # We start with 'a' where the keychain gives us an ambigious result.
# Then we check if the timer has been set up correctly # Then we check if the timer has been set up correctly

View File

@ -52,7 +52,7 @@ def qt_message_handler(msg_type, context, msg):
QtFatalMsg: logging.CRITICAL, QtFatalMsg: logging.CRITICAL,
} }
level = qt_to_logging[msg_type] level = qt_to_logging[msg_type]
# There's very similiar code in utils.log, but we want it duplicated here # There's very similar code in utils.log, but we want it duplicated here
# for the tests. # for the tests.
if context.function is None: if context.function is None:
func = 'none' func = 'none'

View File

@ -146,20 +146,20 @@ class SimpleSplitTests(unittest.TestCase):
} }
def test_str_split(self): def test_str_split(self):
"""Test if the behaviour matches str.split.""" """Test if the behavior matches str.split."""
for test in self.TESTS: for test in self.TESTS:
with self.subTest(string=test): with self.subTest(string=test):
self.assertEqual(split.simple_split(test), self.assertEqual(split.simple_split(test),
test.rstrip().split()) test.rstrip().split())
def test_str_split_maxsplit_1(self): def test_str_split_maxsplit_1(self):
"""Test if the behaviour matches str.split with maxsplit=1.""" """Test if the behavior matches str.split with maxsplit=1."""
string = "foo bar baz" string = "foo bar baz"
self.assertEqual(split.simple_split(string, maxsplit=1), self.assertEqual(split.simple_split(string, maxsplit=1),
string.rstrip().split(maxsplit=1)) string.rstrip().split(maxsplit=1))
def test_str_split_maxsplit_0(self): def test_str_split_maxsplit_0(self):
"""Test if the behaviour matches str.split with maxsplit=0.""" """Test if the behavior matches str.split with maxsplit=0."""
string = " foo bar baz " string = " foo bar baz "
self.assertEqual(split.simple_split(string, maxsplit=0), self.assertEqual(split.simple_split(string, maxsplit=0),
string.rstrip().split(maxsplit=0)) string.rstrip().split(maxsplit=0))

View File

@ -0,0 +1,45 @@
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
#
# 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 qutebrowser.utils.debug.log_time."""
import logging
import re
import time
from qutebrowser.utils import debug
def test_log_time(caplog):
"""Test if log_time logs properly."""
logger_name = 'qt-tests'
with caplog.atLevel(logging.DEBUG, logger=logger_name):
with debug.log_time(logging.getLogger(logger_name), action='foobar'):
time.sleep(0.1)
records = caplog.records()
assert len(records) == 1
pattern = re.compile(r'^Foobar took ([\d.]*) seconds\.$')
match = pattern.match(records[0].msg)
assert match
duration = float(match.group(1))
assert 0.08 <= duration <= 0.12

View File

@ -0,0 +1,65 @@
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
#
# 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 qutebrowser.utils.debug.qenum_key."""
import pytest
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QStyle, QFrame
from qutebrowser.utils import debug
def test_no_metaobj():
"""Test with an enum with no meta-object."""
assert not hasattr(QStyle.PrimitiveElement, 'staticMetaObject')
key = debug.qenum_key(QStyle, QStyle.PE_PanelButtonCommand)
assert key == 'PE_PanelButtonCommand'
def test_metaobj():
"""Test with an enum with meta-object."""
assert hasattr(QFrame, 'staticMetaObject')
key = debug.qenum_key(QFrame, QFrame.Sunken)
assert key == 'Sunken'
def test_add_base():
"""Test with add_base=True."""
key = debug.qenum_key(QFrame, QFrame.Sunken, add_base=True)
assert key == 'QFrame.Sunken'
def test_int_noklass():
"""Test passing an int without explicit klass given."""
with pytest.raises(TypeError):
debug.qenum_key(QFrame, 42)
def test_int():
"""Test passing an int with explicit klass given."""
key = debug.qenum_key(QFrame, 0x0030, klass=QFrame.Shadow)
assert key == 'Sunken'
def test_unknown():
"""Test passing an unknown value."""
key = debug.qenum_key(QFrame, 0x1337, klass=QFrame.Shadow)
assert key == '0x1337'

View File

@ -0,0 +1,77 @@
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
#
# 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 qutebrowser.utils.debug.qflags_key.
https://github.com/The-Compiler/qutebrowser/issues/42
"""
import pytest
from PyQt5.QtCore import Qt
from qutebrowser.utils import debug
fixme = pytest.mark.xfail(reason="See issue #42", raises=AssertionError)
@fixme
def test_single():
"""Test with single value."""
flags = debug.qflags_key(Qt, Qt.AlignTop)
assert flags == 'AlignTop'
@fixme
def test_multiple():
"""Test with multiple values."""
flags = debug.qflags_key(Qt, Qt.AlignLeft | Qt.AlignTop)
assert flags == 'AlignLeft|AlignTop'
def test_combined():
"""Test with a combined value."""
flags = debug.qflags_key(Qt, Qt.AlignCenter)
assert flags == 'AlignHCenter|AlignVCenter'
@fixme
def test_add_base():
"""Test with add_base=True."""
flags = debug.qflags_key(Qt, Qt.AlignTop, add_base=True)
assert flags == 'Qt.AlignTop'
def test_int_noklass():
"""Test passing an int without explicit klass given."""
with pytest.raises(TypeError):
debug.qflags_key(Qt, 42)
@fixme
def test_int():
"""Test passing an int with explicit klass given."""
flags = debug.qflags_key(Qt, 0x0021, klass=Qt.Alignment)
assert flags == 'AlignLeft|AlignTop'
def test_unknown():
"""Test passing an unknown value."""
flags = debug.qflags_key(Qt, 0x1100, klass=Qt.Alignment)
assert flags == '0x0100|0x1000'

View File

@ -0,0 +1,52 @@
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
#
# 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/>.
"""Test signal debug output functions."""
import pytest
from qutebrowser.utils import debug
@pytest.fixture
def signal(stubs):
"""Fixture to provide a faked pyqtSignal."""
return stubs.FakeSignal()
def test_signal_name(signal):
"""Test signal_name()."""
assert debug.signal_name(signal) == 'fake'
def test_dbg_signal(signal):
"""Test dbg_signal()."""
assert debug.dbg_signal(signal, [23, 42]) == 'fake(23, 42)'
def test_dbg_signal_eliding(signal):
"""Test eliding in dbg_signal()."""
dbg_signal = debug.dbg_signal(signal, ['x' * 201])
assert dbg_signal == "fake('{}\u2026)".format('x' * 198)
def test_dbg_signal_newline(signal):
"""Test dbg_signal() with a newline."""
dbg_signal = debug.dbg_signal(signal, ['foo\nbar'])
assert dbg_signal == r"fake('foo\nbar')"

View File

@ -1,168 +0,0 @@
# Copyright 2014-2015 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
#
# 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 qutebrowser.utils.debug."""
import re
import time
import logging
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QStyle, QFrame
import pytest
from qutebrowser.utils import debug
class TestQEnumKey:
"""Tests for qenum_key."""
def test_no_metaobj(self):
"""Test with an enum with no metaobject."""
with pytest.raises(AttributeError):
# Make sure it doesn't have a meta object
# pylint: disable=pointless-statement,no-member
QStyle.PrimitiveElement.staticMetaObject
key = debug.qenum_key(QStyle, QStyle.PE_PanelButtonCommand)
assert key == 'PE_PanelButtonCommand'
def test_metaobj(self):
"""Test with an enum with metaobject."""
# pylint: disable=pointless-statement
QFrame.staticMetaObject # make sure it has a metaobject
key = debug.qenum_key(QFrame, QFrame.Sunken)
assert key == 'Sunken'
def test_add_base(self):
"""Test with add_base=True."""
key = debug.qenum_key(QFrame, QFrame.Sunken, add_base=True)
assert key == 'QFrame.Sunken'
def test_int_noklass(self):
"""Test passing an int without explicit klass given."""
with pytest.raises(TypeError):
debug.qenum_key(QFrame, 42)
def test_int(self):
"""Test passing an int with explicit klass given."""
key = debug.qenum_key(QFrame, 0x0030, klass=QFrame.Shadow)
assert key == 'Sunken'
def test_unknown(self):
"""Test passing an unknown value."""
key = debug.qenum_key(QFrame, 0x1337, klass=QFrame.Shadow)
assert key == '0x1337'
def test_reconverted(self):
"""Test passing a flag value which was re-converted to an enum."""
# FIXME maybe this should return the right thing anyways?
debug.qenum_key(Qt, Qt.Alignment(int(Qt.AlignLeft)))
class TestQFlagsKey:
"""Tests for qflags_key()."""
fail_issue42 = pytest.mark.xfail(
reason='https://github.com/The-Compiler/qutebrowser/issues/42')
@fail_issue42
def test_single(self):
"""Test with single value."""
flags = debug.qflags_key(Qt, Qt.AlignTop)
assert flags == 'AlignTop'
@fail_issue42
def test_multiple(self):
"""Test with multiple values."""
flags = debug.qflags_key(Qt, Qt.AlignLeft | Qt.AlignTop)
assert flags == 'AlignLeft|AlignTop'
def test_combined(self):
"""Test with a combined value."""
flags = debug.qflags_key(Qt, Qt.AlignCenter)
assert flags == 'AlignHCenter|AlignVCenter'
@fail_issue42
def test_add_base(self):
"""Test with add_base=True."""
flags = debug.qflags_key(Qt, Qt.AlignTop, add_base=True)
assert flags == 'Qt.AlignTop'
def test_int_noklass(self):
"""Test passing an int without explicit klass given."""
with pytest.raises(TypeError):
debug.qflags_key(Qt, 42)
@fail_issue42
def test_int(self):
"""Test passing an int with explicit klass given."""
flags = debug.qflags_key(Qt, 0x0021, klass=Qt.Alignment)
assert flags == 'AlignLeft|AlignTop'
def test_unknown(self):
"""Test passing an unknown value."""
flags = debug.qflags_key(Qt, 0x1100, klass=Qt.Alignment)
assert flags == '0x0100|0x1000'
class TestDebug:
"""Test signal debug output functions."""
@pytest.fixture
def signal(self, stubs):
return stubs.FakeSignal()
def test_signal_name(self, signal):
"""Test signal_name()."""
assert debug.signal_name(signal) == 'fake'
def test_dbg_signal(self, signal):
"""Test dbg_signal()."""
assert debug.dbg_signal(signal, [23, 42]) == 'fake(23, 42)'
def test_dbg_signal_eliding(self, signal):
"""Test eliding in dbg_signal()."""
assert debug.dbg_signal(signal, ['x' * 201]) == \
"fake('{}\u2026)".format('x' * 198)
def test_dbg_signal_newline(self, signal):
"""Test dbg_signal() with a newline."""
assert debug.dbg_signal(signal, ['foo\nbar']) == r"fake('foo\nbar')"
class TestLogTime:
"""Test log_time."""
def test_log_time(self, caplog):
"""Test if log_time logs properly."""
logger = logging.getLogger('qt-tests')
with caplog.atLevel(logging.DEBUG, logger.name):
with debug.log_time(logger, action='foobar'):
time.sleep(0.1)
assert len(caplog.records()) == 1
pattern = re.compile(r'^Foobar took ([\d.]*) seconds\.$')
match = pattern.match(caplog.records()[0].msg)
assert match
duration = float(match.group(1))
assert 0.09 <= duration <= 0.11

View File

@ -83,7 +83,7 @@ class CheckOverflowTests(unittest.TestCase):
def argparser_exit(status=0, message=None): # pylint: disable=unused-argument def argparser_exit(status=0, message=None): # pylint: disable=unused-argument
"""Function to monkeypatch .exit() of the argparser so it doesn't exit.""" """Function to monkey-patch .exit() of the argparser so it doesn't exit."""
raise Exception raise Exception

View File

@ -245,14 +245,14 @@ class BlockTests(unittest.TestCase):
mode=usertypes.NeighborList.Modes.block) mode=usertypes.NeighborList.Modes.block)
def test_first(self): def test_first(self):
"""Test ouf of bounds previtem().""" """Test out of bounds previtem()."""
self.nl.firstitem() self.nl.firstitem()
self.assertEqual(self.nl._idx, 0) self.assertEqual(self.nl._idx, 0)
self.assertEqual(self.nl.previtem(), 1) self.assertEqual(self.nl.previtem(), 1)
self.assertEqual(self.nl._idx, 0) self.assertEqual(self.nl._idx, 0)
def test_last(self): def test_last(self):
"""Test ouf of bounds nextitem().""" """Test out of bounds nextitem()."""
self.nl.lastitem() self.nl.lastitem()
self.assertEqual(self.nl._idx, 4) self.assertEqual(self.nl._idx, 4)
self.assertEqual(self.nl.nextitem(), 5) self.assertEqual(self.nl.nextitem(), 5)
@ -272,14 +272,14 @@ class WrapTests(unittest.TestCase):
[1, 2, 3, 4, 5], default=3, mode=usertypes.NeighborList.Modes.wrap) [1, 2, 3, 4, 5], default=3, mode=usertypes.NeighborList.Modes.wrap)
def test_first(self): def test_first(self):
"""Test ouf of bounds previtem().""" """Test out of bounds previtem()."""
self.nl.firstitem() self.nl.firstitem()
self.assertEqual(self.nl._idx, 0) self.assertEqual(self.nl._idx, 0)
self.assertEqual(self.nl.previtem(), 5) self.assertEqual(self.nl.previtem(), 5)
self.assertEqual(self.nl._idx, 4) self.assertEqual(self.nl._idx, 4)
def test_last(self): def test_last(self):
"""Test ouf of bounds nextitem().""" """Test out of bounds nextitem()."""
self.nl.lastitem() self.nl.lastitem()
self.assertEqual(self.nl._idx, 4) self.assertEqual(self.nl._idx, 4)
self.assertEqual(self.nl.nextitem(), 1) self.assertEqual(self.nl.nextitem(), 1)
@ -300,7 +300,7 @@ class RaiseTests(unittest.TestCase):
mode=usertypes.NeighborList.Modes.exception) mode=usertypes.NeighborList.Modes.exception)
def test_first(self): def test_first(self):
"""Test ouf of bounds previtem().""" """Test out of bounds previtem()."""
self.nl.firstitem() self.nl.firstitem()
self.assertEqual(self.nl._idx, 0) self.assertEqual(self.nl._idx, 0)
with self.assertRaises(IndexError): with self.assertRaises(IndexError):
@ -308,7 +308,7 @@ class RaiseTests(unittest.TestCase):
self.assertEqual(self.nl._idx, 0) self.assertEqual(self.nl._idx, 0)
def test_last(self): def test_last(self):
"""Test ouf of bounds nextitem().""" """Test out of bounds nextitem()."""
self.nl.lastitem() self.nl.lastitem()
self.assertEqual(self.nl._idx, 4) self.assertEqual(self.nl._idx, 4)
with self.assertRaises(IndexError): with self.assertRaises(IndexError):

View File

@ -19,6 +19,7 @@ setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envsitepackagesdir}/PyQt5/plugins/platform
deps = deps =
py==1.4.26 py==1.4.26
pytest==2.7.0 pytest==2.7.0
pytest-capturelog==0.7
pytest-qt==1.3.0 pytest-qt==1.3.0
pytest-mock==0.4.2 pytest-mock==0.4.2
# 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
@ -32,6 +33,7 @@ deps =
{[testenv:unittests]deps} {[testenv:unittests]deps}
coverage==3.7.1 coverage==3.7.1
pytest-cov==1.8.1 pytest-cov==1.8.1
pytest-capturelog==0.7
pytest-qt==1.3.0 pytest-qt==1.3.0
pytest-mock==0.4.2 pytest-mock==0.4.2
cov-core==1.15.0 cov-core==1.15.0
@ -43,6 +45,7 @@ commands =
commands = commands =
{envpython} scripts/misc_checks.py git {envpython} scripts/misc_checks.py git
{envpython} scripts/misc_checks.py vcs qutebrowser scripts {envpython} scripts/misc_checks.py vcs qutebrowser scripts
{envpython} scripts/misc_checks.py spelling qutebrowser scripts
[testenv:pylint] [testenv:pylint]
skip_install = true skip_install = true