From 69f729dbe5156330c4a866a78be6003ecb12c4d5 Mon Sep 17 00:00:00 2001 From: Austin Anderson Date: Sat, 9 May 2015 18:07:40 -0400 Subject: [PATCH 001/146] Added foreground color settings for statusbar messages. --- qutebrowser/config/configdata.py | 16 ++++++++++++++++ qutebrowser/mainwindow/statusbar/bar.py | 21 +++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index e0e613892..d5ec8e9bd 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -804,18 +804,34 @@ def data(readonly=False): SettingValue(typ.QssColor(), 'red'), "Background color of the statusbar if there was an error."), + ('statusbar.fg.error', + SettingValue(typ.QssColor(), 'white'), + "Foreground color of the statusbar if there was an error."), + ('statusbar.bg.warning', SettingValue(typ.QssColor(), 'darkorange'), "Background color of the statusbar if there is a warning."), + ('statusbar.fg.warning', + SettingValue(typ.QssColor(), 'white'), + "Foreground color of the statusbar if there is a warning."), + ('statusbar.bg.prompt', SettingValue(typ.QssColor(), 'darkblue'), "Background color of the statusbar if there is a prompt."), + ('statusbar.fg.prompt', + SettingValue(typ.QssColor(), 'white'), + "Foreground color of the statusbar if there is a prompt."), + ('statusbar.bg.insert', SettingValue(typ.QssColor(), 'darkgreen'), "Background color of the statusbar in insert mode."), + ('statusbar.fg.insert', + SettingValue(typ.QssColor(), 'white'), + "Foreground color of the statusbar in insert mode."), + ('statusbar.progress.bg', SettingValue(typ.QssColor(), 'white'), "Background color of the progress bar."), diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index a1c8aabd0..2a034223b 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -97,26 +97,47 @@ class StatusBar(QWidget): {{ color['statusbar.bg'] }} } + QWidget#StatusBar QLabel { + {{ color['statusbar.fg'] }} + } + QWidget#StatusBar[insert_active="true"] { {{ color['statusbar.bg.insert'] }} } + QWidget#StatusBar[insert_active="true"] QLabel { + {{ color['statusbar.fg.insert'] }} + } + QWidget#StatusBar[prompt_active="true"] { {{ color['statusbar.bg.prompt'] }} } + QWidget#StatusBar[prompt_active="true"] QLabel { + {{ color['statusbar.fg.prompt'] }} + } + QWidget#StatusBar[severity="error"] { {{ color['statusbar.bg.error'] }} } + QWidget#StatusBar[severity="error"] QLabel { + {{ color['statusbar.fg.error'] }} + } + QWidget#StatusBar[severity="warning"] { {{ color['statusbar.bg.warning'] }} } + QWidget#StatusBar[severity="warning"] QLabel { + {{ color['statusbar.fg.warning'] }} + } + QLabel, QLineEdit { {{ color['statusbar.fg'] }} {{ font['statusbar'] }} } + """ def __init__(self, win_id, parent=None): From 244d2753df3fd63af468e49d31d591e4a6a16342 Mon Sep 17 00:00:00 2001 From: Austin Anderson Date: Sun, 10 May 2015 15:33:58 -0400 Subject: [PATCH 002/146] Reordered fg/bg statusbar color options Options are now all fg, bg for each variant. --- qutebrowser/config/configdata.py | 56 ++++++++++++++++---------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index d5ec8e9bd..98787c284 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -792,46 +792,46 @@ def data(readonly=False): SettingValue(typ.QssColor(), '#ff4444'), "Foreground color of the matched text in the completion."), - ('statusbar.bg', - SettingValue(typ.QssColor(), 'black'), - "Foreground color of the statusbar."), - ('statusbar.fg', SettingValue(typ.QssColor(), 'white'), "Foreground color of the statusbar."), - ('statusbar.bg.error', - SettingValue(typ.QssColor(), 'red'), - "Background color of the statusbar if there was an error."), + ('statusbar.bg', + SettingValue(typ.QssColor(), 'black'), + "Foreground color of the statusbar."), ('statusbar.fg.error', SettingValue(typ.QssColor(), 'white'), "Foreground color of the statusbar if there was an error."), - ('statusbar.bg.warning', - SettingValue(typ.QssColor(), 'darkorange'), - "Background color of the statusbar if there is a warning."), + ('statusbar.bg.error', + SettingValue(typ.QssColor(), 'red'), + "Background color of the statusbar if there was an error."), ('statusbar.fg.warning', SettingValue(typ.QssColor(), 'white'), "Foreground color of the statusbar if there is a warning."), - ('statusbar.bg.prompt', - SettingValue(typ.QssColor(), 'darkblue'), - "Background color of the statusbar if there is a prompt."), + ('statusbar.bg.warning', + SettingValue(typ.QssColor(), 'darkorange'), + "Background color of the statusbar if there is a warning."), ('statusbar.fg.prompt', SettingValue(typ.QssColor(), 'white'), "Foreground color of the statusbar if there is a prompt."), - ('statusbar.bg.insert', - SettingValue(typ.QssColor(), 'darkgreen'), - "Background color of the statusbar in insert mode."), + ('statusbar.bg.prompt', + SettingValue(typ.QssColor(), 'darkblue'), + "Background color of the statusbar if there is a prompt."), ('statusbar.fg.insert', SettingValue(typ.QssColor(), 'white'), "Foreground color of the statusbar in insert mode."), + ('statusbar.bg.insert', + SettingValue(typ.QssColor(), 'darkgreen'), + "Background color of the statusbar in insert mode."), + ('statusbar.progress.bg', SettingValue(typ.QssColor(), 'white'), "Background color of the progress bar."), @@ -863,22 +863,22 @@ def data(readonly=False): SettingValue(typ.QtColor(), 'white'), "Foreground color of unselected odd tabs."), - ('tabs.fg.even', - SettingValue(typ.QtColor(), 'white'), - "Foreground color of unselected even tabs."), - - ('tabs.fg.selected', - SettingValue(typ.QtColor(), 'white'), - "Foreground color of selected tabs."), - ('tabs.bg.odd', SettingValue(typ.QtColor(), 'grey'), "Background color of unselected odd tabs."), + ('tabs.fg.even', + SettingValue(typ.QtColor(), 'white'), + "Foreground color of unselected even tabs."), + ('tabs.bg.even', SettingValue(typ.QtColor(), 'darkgrey'), "Background color of unselected even tabs."), + ('tabs.fg.selected', + SettingValue(typ.QtColor(), 'white'), + "Foreground color of selected tabs."), + ('tabs.bg.selected', SettingValue(typ.QtColor(), 'black'), "Background color of selected tabs."), @@ -907,10 +907,6 @@ def data(readonly=False): SettingValue(typ.CssColor(), 'black'), "Font color for hints."), - ('hints.fg.match', - SettingValue(typ.CssColor(), 'green'), - "Font color for the matched part of hints."), - ('hints.bg', SettingValue( typ.CssColor(), '-webkit-gradient(linear, left top, ' @@ -918,6 +914,10 @@ def data(readonly=False): 'color-stop(100%,#FFC542))'), "Background color for hints."), + ('hints.fg.match', + SettingValue(typ.CssColor(), 'green'), + "Font color for the matched part of hints."), + ('downloads.fg', SettingValue(typ.QtColor(), '#ffffff'), "Foreground color for downloads."), From 1a2a57d59eccee3de70899fdfac6db4c90dd1c61 Mon Sep 17 00:00:00 2001 From: Austin Anderson Date: Mon, 11 May 2015 22:27:21 -0400 Subject: [PATCH 003/146] Added command mode color configuration options. Including necessary tracker variable _command_active. --- qutebrowser/config/configdata.py | 8 ++++ qutebrowser/mainwindow/statusbar/bar.py | 49 +++++++++++++++++++++---- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 98787c284..9327dd377 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -832,6 +832,14 @@ def data(readonly=False): SettingValue(typ.QssColor(), 'darkgreen'), "Background color of the statusbar in insert mode."), + ('statusbar.fg.command', + SettingValue(typ.QssColor(), '${statusbar.fg}'), + "Foreground color of the statusbar in command mode."), + + ('statusbar.bg.command', + SettingValue(typ.QssColor(), '${statusbar.bg}'), + "Background color of the statusbar in command mode."), + ('statusbar.progress.bg', SettingValue(typ.QssColor(), 'white'), "Background color of the progress bar."), diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 2a034223b..98d2d05e0 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -77,6 +77,10 @@ class StatusBar(QWidget): For some reason we need to have this as class attribute so pyqtProperty works correctly. + _command_active: If we're currently in command mode. + + For some reason we need to have this as class attribute + so pyqtProperty works correctly. Signals: resized: Emitted when the statusbar has resized, so the completion widget can adjust its size to it. @@ -91,6 +95,7 @@ class StatusBar(QWidget): _severity = None _prompt_active = False _insert_active = False + _command_active = False STYLESHEET = """ QWidget#StatusBar { @@ -101,12 +106,8 @@ class StatusBar(QWidget): {{ color['statusbar.fg'] }} } - QWidget#StatusBar[insert_active="true"] { - {{ color['statusbar.bg.insert'] }} - } - - QWidget#StatusBar[insert_active="true"] QLabel { - {{ color['statusbar.fg.insert'] }} + QWidget#StatusBar QLineEdit { + {{ color['statusbar.fg.command'] }} } QWidget#StatusBar[prompt_active="true"] { @@ -117,6 +118,22 @@ class StatusBar(QWidget): {{ color['statusbar.fg.prompt'] }} } + QWidget#StatusBar[insert_active="true"] { + {{ color['statusbar.bg.insert'] }} + } + + QWidget#StatusBar[insert_active="true"] QLabel { + {{ color['statusbar.fg.insert'] }} + } + + QWidget#StatusBar[command_active="true"] QLabel { + {{ color['statusbar.fg.command'] }} + } + + QWidget#StatusBar[command_active="true"] { + {{ color['statusbar.bg.command'] }} + } + QWidget#StatusBar[severity="error"] { {{ color['statusbar.bg.error'] }} } @@ -134,7 +151,6 @@ class StatusBar(QWidget): } QLabel, QLineEdit { - {{ color['statusbar.fg'] }} {{ font['statusbar'] }} } @@ -269,6 +285,21 @@ class StatusBar(QWidget): self._prompt_active = val self.setStyleSheet(style.get_stylesheet(self.STYLESHEET)) + @pyqtProperty(bool) + def command_active(self): + """Getter for self.command_active, so it can be used as Qt property.""" + return self._command_active + + def _set_command_active(self, val): + """Setter for self._command_active. + + Re-set the stylesheet after setting the value, so everything gets + updated by Qt properly. + """ + log.statusbar.debug("Setting command_active to {}".format(val)) + self._command_active = val + self.setStyleSheet(style.get_stylesheet(self.STYLESHEET)) + @pyqtProperty(bool) def insert_active(self): """Getter for self.insert_active, so it can be used as Qt property.""" @@ -461,6 +492,8 @@ class StatusBar(QWidget): self._set_mode_text(mode.name) if mode == usertypes.KeyMode.insert: self._set_insert_active(True) + if mode == usertypes.KeyMode.command: + self._set_command_active(True) @pyqtSlot(usertypes.KeyMode, usertypes.KeyMode) def on_mode_left(self, old_mode, new_mode): @@ -474,6 +507,8 @@ class StatusBar(QWidget): self.txt.set_text(self.txt.Text.normal, '') if old_mode == usertypes.KeyMode.insert: self._set_insert_active(False) + if old_mode == usertypes.KeyMode.command: + self._set_command_active(False) @config.change_filter('ui', 'message-timeout') def set_pop_timer_interval(self): From 14c1332017f23a01f167b910b140d7291d2aa6dc Mon Sep 17 00:00:00 2001 From: Austin Anderson Date: Mon, 11 May 2015 22:28:12 -0400 Subject: [PATCH 004/146] Reordered statusbar stylesheet to match configuration ordering. --- qutebrowser/mainwindow/statusbar/bar.py | 32 ++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 98d2d05e0..53c991c86 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -110,6 +110,22 @@ class StatusBar(QWidget): {{ color['statusbar.fg.command'] }} } + QWidget#StatusBar[severity="error"] { + {{ color['statusbar.bg.error'] }} + } + + QWidget#StatusBar[severity="error"] QLabel { + {{ color['statusbar.fg.error'] }} + } + + QWidget#StatusBar[severity="warning"] { + {{ color['statusbar.bg.warning'] }} + } + + QWidget#StatusBar[severity="warning"] QLabel { + {{ color['statusbar.fg.warning'] }} + } + QWidget#StatusBar[prompt_active="true"] { {{ color['statusbar.bg.prompt'] }} } @@ -134,22 +150,6 @@ class StatusBar(QWidget): {{ color['statusbar.bg.command'] }} } - QWidget#StatusBar[severity="error"] { - {{ color['statusbar.bg.error'] }} - } - - QWidget#StatusBar[severity="error"] QLabel { - {{ color['statusbar.fg.error'] }} - } - - QWidget#StatusBar[severity="warning"] { - {{ color['statusbar.bg.warning'] }} - } - - QWidget#StatusBar[severity="warning"] QLabel { - {{ color['statusbar.fg.warning'] }} - } - QLabel, QLineEdit { {{ font['statusbar'] }} } From 0d66647918c4d27001752ba8ba08dd1bcd954f0d Mon Sep 17 00:00:00 2001 From: Austin Anderson Date: Mon, 11 May 2015 22:35:44 -0400 Subject: [PATCH 005/146] Set extra foreground colors to match the default by default. --- qutebrowser/config/configdata.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 9327dd377..b4bfbd900 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -801,7 +801,7 @@ def data(readonly=False): "Foreground color of the statusbar."), ('statusbar.fg.error', - SettingValue(typ.QssColor(), 'white'), + SettingValue(typ.QssColor(), '${statusbar.fg}'), "Foreground color of the statusbar if there was an error."), ('statusbar.bg.error', @@ -809,7 +809,7 @@ def data(readonly=False): "Background color of the statusbar if there was an error."), ('statusbar.fg.warning', - SettingValue(typ.QssColor(), 'white'), + SettingValue(typ.QssColor(), '${statusbar.fg}'), "Foreground color of the statusbar if there is a warning."), ('statusbar.bg.warning', @@ -817,7 +817,7 @@ def data(readonly=False): "Background color of the statusbar if there is a warning."), ('statusbar.fg.prompt', - SettingValue(typ.QssColor(), 'white'), + SettingValue(typ.QssColor(), '${statusbar.fg}'), "Foreground color of the statusbar if there is a prompt."), ('statusbar.bg.prompt', @@ -825,7 +825,7 @@ def data(readonly=False): "Background color of the statusbar if there is a prompt."), ('statusbar.fg.insert', - SettingValue(typ.QssColor(), 'white'), + SettingValue(typ.QssColor(), '${statusbar.fg}'), "Foreground color of the statusbar in insert mode."), ('statusbar.bg.insert', From 229733f1b03079dd2c1ad6c28a7d4fe518ba97e7 Mon Sep 17 00:00:00 2001 From: Austin Anderson Date: Mon, 11 May 2015 22:46:26 -0400 Subject: [PATCH 006/146] Properly distinguish between statusbar modes when styling line input. --- qutebrowser/mainwindow/statusbar/bar.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 53c991c86..aca145f6a 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -107,7 +107,7 @@ class StatusBar(QWidget): } QWidget#StatusBar QLineEdit { - {{ color['statusbar.fg.command'] }} + {{ color['statusbar.fg'] }} } QWidget#StatusBar[severity="error"] { @@ -134,6 +134,10 @@ class StatusBar(QWidget): {{ color['statusbar.fg.prompt'] }} } + QWidget#StatusBar[prompt_active="true"] QLineEdit { + {{ color['statusbar.fg.prompt'] }} + } + QWidget#StatusBar[insert_active="true"] { {{ color['statusbar.bg.insert'] }} } @@ -142,12 +146,16 @@ class StatusBar(QWidget): {{ color['statusbar.fg.insert'] }} } + QWidget#StatusBar[command_active="true"] { + {{ color['statusbar.bg.command'] }} + } + QWidget#StatusBar[command_active="true"] QLabel { {{ color['statusbar.fg.command'] }} } - QWidget#StatusBar[command_active="true"] { - {{ color['statusbar.bg.command'] }} + QWidget#StatusBar[command_active="true"] QLineEdit { + {{ color['statusbar.fg.command'] }} } QLabel, QLineEdit { From 05530944944b95e324303d6e0ae02ec5e98b09d5 Mon Sep 17 00:00:00 2001 From: Austin Anderson Date: Mon, 25 May 2015 19:20:33 -0400 Subject: [PATCH 007/146] Added explanation of *.system values to settings page. --- qutebrowser/config/configdata.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index b4bfbd900..ad321b73d 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -103,6 +103,10 @@ SECTION_DESC = { " * A gradient as explained in http://qt-project.org/doc/qt-4.8/" "stylesheet-reference.html#list-of-property-types[the Qt " "documentation] under ``Gradient''.\n\n" + "A *.system value determines the color system to use for color " + "interpolation between similarly-named *.start and *.stop entries, " + "regardless of how they are defined in the options. " + "Valid values are 'rgb', 'hsv', and 'hsl'.\n\n" "The `hints.*` values are a special case as they're real CSS " "colors, not Qt-CSS colors. There, for a gradient, you need to use " "`-webkit-gradient`, see https://www.webkit.org/blog/175/introducing-" From a8d2dbfdfb44d41f7f9c17c38c5251d26379f129 Mon Sep 17 00:00:00 2001 From: Austin Anderson Date: Mon, 25 May 2015 20:47:16 -0400 Subject: [PATCH 008/146] Added downloads bar fg customization, and refactored the download's color-picking. --- qutebrowser/browser/downloads.py | 20 ++++++++++++-------- qutebrowser/config/configdata.py | 20 ++++++++++++++++---- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index 6d12761f6..e30147e10 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -356,12 +356,16 @@ class DownloadItem(QObject): if reply.error() != QNetworkReply.NoError: QTimer.singleShot(0, lambda: self.error.emit(reply.errorString())) - def bg_color(self): - """Background color to be shown.""" - start = config.get('colors', 'downloads.bg.start') - stop = config.get('colors', 'downloads.bg.stop') - system = config.get('colors', 'downloads.bg.system') - error = config.get('colors', 'downloads.bg.error') + def get_status_color(self, position): + """Choose an appropriate color for presenting the download's status. + + Args: + position: The color type requested, can be 'fg' or 'bg'. + """ + start = config.get('colors', 'downloads.{}.start'.format(position)) + stop = config.get('colors', 'downloads.{}.stop'.format(position)) + system = config.get('colors', 'downloads.{}.system'.format(position)) + error = config.get('colors', 'downloads.{}.error'.format(position)) if self.error_msg is not None: assert not self.successful return error @@ -1020,9 +1024,9 @@ class DownloadManager(QAbstractListModel): if role == Qt.DisplayRole: data = str(item) elif role == Qt.ForegroundRole: - data = config.get('colors', 'downloads.fg') + data = item.get_status_color('fg') elif role == Qt.BackgroundRole: - data = item.bg_color() + data = item.get_status_color('bg') elif role == ModelRole.item: data = item elif role == Qt.ToolTipRole: diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index ad321b73d..db16a2bbf 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -930,26 +930,38 @@ def data(readonly=False): SettingValue(typ.CssColor(), 'green'), "Font color for the matched part of hints."), - ('downloads.fg', - SettingValue(typ.QtColor(), '#ffffff'), - "Foreground color for downloads."), - ('downloads.bg.bar', SettingValue(typ.QssColor(), 'black'), "Background color for the download bar."), + ('downloads.fg.start', + SettingValue(typ.QtColor(), '#0000aa'), + "Color gradient start for downloads."), + ('downloads.bg.start', SettingValue(typ.QtColor(), '#0000aa'), "Color gradient start for downloads."), + ('downloads.fg.stop', + SettingValue(typ.QtColor(), '#00aa00'), + "Color gradient end for downloads."), + ('downloads.bg.stop', SettingValue(typ.QtColor(), '#00aa00'), "Color gradient end for downloads."), + ('downloads.fg.system', + SettingValue(typ.ColorSystem(), 'rgb'), + "Color gradient interpolation system for downloads."), + ('downloads.bg.system', SettingValue(typ.ColorSystem(), 'rgb'), "Color gradient interpolation system for downloads."), + ('downloads.fg.error', + SettingValue(typ.QtColor(), 'red'), + "Foreground color for downloads with errors."), + ('downloads.bg.error', SettingValue(typ.QtColor(), 'red'), "Background color for downloads with errors."), From 27fdf4903a5bffec6af39032c07bd8f22f255ca6 Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Fri, 29 May 2015 18:36:39 +0200 Subject: [PATCH 009/146] Implement :jseval (Issue #334) TODO: - Tests - Doesn't show errors --- qutebrowser/misc/utilcmds.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index ea6fdf1cc..1c7e40d99 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -111,6 +111,19 @@ def message_warning(win_id, text): message.warning(win_id, text) +@cmdutils.register(maxsplit=0, no_cmd_split=True) +def jseval(s): + """Evaluate a JavaScript string. + + Args: + s: The string to evaluate. + """ + tabbed_browser = objreg.get('tabbed-browser', scope='window', + window='last-focused') + message_info(tabbed_browser.widget(0) + .page().mainFrame().evaluateJavaScript(s)) + + @cmdutils.register(debug=True) def debug_crash(typ: {'type': ('exception', 'segfault')}='exception'): """Crash for debugging purposes. From 7b5d2ace2485b91707ce03b79e76be6580c51d41 Mon Sep 17 00:00:00 2001 From: Austin Anderson Date: Sat, 30 May 2015 15:21:34 -0400 Subject: [PATCH 010/146] Added assertion for parameterized download color picker. --- qutebrowser/browser/downloads.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index e30147e10..f10cd7aad 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -362,6 +362,7 @@ class DownloadItem(QObject): Args: position: The color type requested, can be 'fg' or 'bg'. """ + assert position in ("fg", "bg") start = config.get('colors', 'downloads.{}.start'.format(position)) stop = config.get('colors', 'downloads.{}.stop'.format(position)) system = config.get('colors', 'downloads.{}.system'.format(position)) From fed2cdad4ef138f44171a699d39a3eb7211b87ba Mon Sep 17 00:00:00 2001 From: Austin Anderson Date: Sat, 30 May 2015 15:22:00 -0400 Subject: [PATCH 011/146] Cleaned up download configuration options. --- qutebrowser/config/configdata.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index db16a2bbf..67f847865 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -935,31 +935,32 @@ def data(readonly=False): "Background color for the download bar."), ('downloads.fg.start', - SettingValue(typ.QtColor(), '#0000aa'), - "Color gradient start for downloads."), + SettingValue(typ.QtColor(), 'white'), + "Color gradient start for download foreground text."), ('downloads.bg.start', SettingValue(typ.QtColor(), '#0000aa'), - "Color gradient start for downloads."), + "Color gradient start for download background."), ('downloads.fg.stop', - SettingValue(typ.QtColor(), '#00aa00'), - "Color gradient end for downloads."), + SettingValue(typ.QtColor(), '${downloads.fg.start}'), + "Color gradient end for download foreground text."), ('downloads.bg.stop', SettingValue(typ.QtColor(), '#00aa00'), - "Color gradient end for downloads."), + "Color gradient stop for download background."), ('downloads.fg.system', SettingValue(typ.ColorSystem(), 'rgb'), - "Color gradient interpolation system for downloads."), + "Color gradient interpolation system for download foreground" + "text."), ('downloads.bg.system', SettingValue(typ.ColorSystem(), 'rgb'), - "Color gradient interpolation system for downloads."), + "Color gradient interpolation system for download background."), ('downloads.fg.error', - SettingValue(typ.QtColor(), 'red'), + SettingValue(typ.QtColor(), 'white'), "Foreground color for downloads with errors."), ('downloads.bg.error', From 5c599879f819e600d11bcf197f0a902a812fdde6 Mon Sep 17 00:00:00 2001 From: Austin Anderson Date: Sat, 30 May 2015 16:03:36 -0400 Subject: [PATCH 012/146] Fixed a line-length error. --- qutebrowser/mainwindow/statusbar/bar.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 1e4ae3b84..9b1219b38 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -80,8 +80,8 @@ class StatusBar(QWidget): _command_active: If we're currently in command mode. - For some reason we need to have this as class attribute - so pyqtProperty works correctly. + For some reason we need to have this as class + attribute so pyqtProperty works correctly. _caret_mode: The current caret mode (off/on/selection). From 4d141f489f18d98e00df249a89356c87fef69361 Mon Sep 17 00:00:00 2001 From: Austin Anderson Date: Wed, 3 Jun 2015 08:42:13 -0400 Subject: [PATCH 013/146] Added pylint workaround directive to quash rebellion. --- qutebrowser/browser/downloads.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index 3a7c63fc0..22e8858af 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -362,6 +362,8 @@ class DownloadItem(QObject): Args: position: The color type requested, can be 'fg' or 'bg'. """ + # pylint: disable=bad-config-call + # WORKAROUND for https://bitbucket.org/logilab/astroid/issue/104/ assert position in ("fg", "bg") start = config.get('colors', 'downloads.{}.start'.format(position)) stop = config.get('colors', 'downloads.{}.stop'.format(position)) From 85eea17b183751a89ddc8bb4bb82a3a0ccdf5ba5 Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Wed, 3 Jun 2015 22:31:15 +0200 Subject: [PATCH 014/146] Try to get the error ... not sure about this ... source is undefined when you type stuff in the console, I *think* this is the only scenario? But maybe not? --- qutebrowser/browser/commands.py | 20 ++++++++++++++++++++ qutebrowser/browser/webpage.py | 11 +++++++++++ qutebrowser/misc/utilcmds.py | 13 ------------- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 0c01260d7..bc2159843 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1510,3 +1510,23 @@ class CommandDispatcher: view = self._current_widget() for _ in range(count): view.triggerPageAction(member) + + @cmdutils.register(instance='command-dispatcher', scope='window', + maxsplit=0, no_cmd_split=True) + def jseval(self, js): + """Evaluate a JavaScript string. + + Args: + s: The string to evaluate. + """ + tabbed_browser = objreg.get('tabbed-browser', scope='window', + window='last-focused') + out = tabbed_browser.widget(0).page().mainFrame().evaluateJavaScript( + 'window.__qute_jseval__ = true;\n' + js) + + if out is not None: + message.info(self._win_id, out) + elif tabbed_browser.widget(0).page().jseval_error: + message.error(self._win_id, + tabbed_browser.widget(0).page().jseval_error) + tabbed_browser.widget(0).page().jseval_error = None diff --git a/qutebrowser/browser/webpage.py b/qutebrowser/browser/webpage.py index 005a6e300..9e49032df 100644 --- a/qutebrowser/browser/webpage.py +++ b/qutebrowser/browser/webpage.py @@ -63,6 +63,7 @@ class BrowserPage(QWebPage): def __init__(self, win_id, tab_id, parent=None): super().__init__(parent) self._win_id = win_id + self.jseval_error = None self._is_shutting_down = False self._extension_handlers = { QWebPage.ErrorPageExtension: self._handle_errorpage, @@ -497,6 +498,16 @@ class BrowserPage(QWebPage): def javaScriptConsoleMessage(self, msg, line, source): """Override javaScriptConsoleMessage to use debug log.""" + + jseval = self.mainFrame().evaluateJavaScript('window.__qute_jseval__') + if jseval: + self.mainFrame().evaluateJavaScript('window.__qute_jseval__ = undefined;') + if source == 'undefined' and jseval: + self.mainFrame().evaluateJavaScript('window.__qute_jseval__ = false;') + print('jseval errror ->', jseval) + self.jseval_error = 'Error on line {}: {}'.format(line, msg) + print('other js error ->', msg, line, source) + if config.get('general', 'log-javascript-console'): log.js.debug("[{}:{}] {}".format(source, line, msg)) diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index 1c7e40d99..ea6fdf1cc 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -111,19 +111,6 @@ def message_warning(win_id, text): message.warning(win_id, text) -@cmdutils.register(maxsplit=0, no_cmd_split=True) -def jseval(s): - """Evaluate a JavaScript string. - - Args: - s: The string to evaluate. - """ - tabbed_browser = objreg.get('tabbed-browser', scope='window', - window='last-focused') - message_info(tabbed_browser.widget(0) - .page().mainFrame().evaluateJavaScript(s)) - - @cmdutils.register(debug=True) def debug_crash(typ: {'type': ('exception', 'segfault')}='exception'): """Crash for debugging purposes. From 94178c558abf0d5da0bdd6a5ec81348022d72a23 Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Fri, 5 Jun 2015 20:07:47 +0200 Subject: [PATCH 015/146] Well, getting the error doesn't work... --- doc/help/commands.asciidoc | 14 ++++++++++++++ qutebrowser/browser/commands.py | 28 ++++++++++++++++++---------- qutebrowser/browser/webpage.py | 11 ----------- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index c33bccaaf..287620867 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -20,6 +20,7 @@ |<>|Start hinting. |<>|Open main startpage in current tab. |<>|Toggle the web inspector. +|<>|Evaluate a JavaScript string. |<>|Execute a command after some time. |<>|Open typical prev/next links or navigate using the URL path. |<>|Open a URL in the current/[count]th tab. @@ -241,6 +242,19 @@ Open main startpage in current tab. === inspector Toggle the web inspector. +[[jseval]] +=== jseval +Syntax: +:jseval 'js_code'+ + +Evaluate a JavaScript string. + +==== positional arguments +* +'js_code'+: The string to evaluate. + +==== note +* This command does not split arguments after the last argument and handles quotes literally. +* With this command, +;;+ is interpreted literally instead of splitting off a second command. + [[later]] === later Syntax: +:later 'ms' 'command'+ diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index bc2159843..aa8430127 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1513,20 +1513,28 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window', maxsplit=0, no_cmd_split=True) - def jseval(self, js): + def jseval(self, js_code): """Evaluate a JavaScript string. Args: - s: The string to evaluate. + js_code: The string to evaluate. """ tabbed_browser = objreg.get('tabbed-browser', scope='window', window='last-focused') - out = tabbed_browser.widget(0).page().mainFrame().evaluateJavaScript( - 'window.__qute_jseval__ = true;\n' + js) + frame = tabbed_browser.widget(0).page().mainFrame() + out = frame.evaluateJavaScript(js_code) - if out is not None: - message.info(self._win_id, out) - elif tabbed_browser.widget(0).page().jseval_error: - message.error(self._win_id, - tabbed_browser.widget(0).page().jseval_error) - tabbed_browser.widget(0).page().jseval_error = None + if out is None: + # Getting the actual error (if any) seems to be difficult. The + # error does end up in BrowserPage.javaScriptConsoleMessage(), but + # distinguishing between :jseval errors and errors from the webpage + # is not trivial... + message.info(self._win_id, 'No output or error') + else: + # The output can be a string, number, dict, array, etc. But *don't* + # output too much data, as this will make qutebrowser hang + out = str(out) + if len(out) > 5000: + message.info(self._win_id, out[:5000] + ' [...trimmed...]') + else: + message.info(self._win_id, out) diff --git a/qutebrowser/browser/webpage.py b/qutebrowser/browser/webpage.py index 9e49032df..005a6e300 100644 --- a/qutebrowser/browser/webpage.py +++ b/qutebrowser/browser/webpage.py @@ -63,7 +63,6 @@ class BrowserPage(QWebPage): def __init__(self, win_id, tab_id, parent=None): super().__init__(parent) self._win_id = win_id - self.jseval_error = None self._is_shutting_down = False self._extension_handlers = { QWebPage.ErrorPageExtension: self._handle_errorpage, @@ -498,16 +497,6 @@ class BrowserPage(QWebPage): def javaScriptConsoleMessage(self, msg, line, source): """Override javaScriptConsoleMessage to use debug log.""" - - jseval = self.mainFrame().evaluateJavaScript('window.__qute_jseval__') - if jseval: - self.mainFrame().evaluateJavaScript('window.__qute_jseval__ = undefined;') - if source == 'undefined' and jseval: - self.mainFrame().evaluateJavaScript('window.__qute_jseval__ = false;') - print('jseval errror ->', jseval) - self.jseval_error = 'Error on line {}: {}'.format(line, msg) - print('other js error ->', msg, line, source) - if config.get('general', 'log-javascript-console'): log.js.debug("[{}:{}] {}".format(source, line, msg)) From b0880df695867bc60a8ac9edd79bd8fee671a393 Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Fri, 5 Jun 2015 23:24:47 +0200 Subject: [PATCH 016/146] Execute in the current tab, and not the first one --- qutebrowser/browser/commands.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index aa8430127..774cbaaa9 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1519,10 +1519,8 @@ class CommandDispatcher: Args: js_code: The string to evaluate. """ - tabbed_browser = objreg.get('tabbed-browser', scope='window', - window='last-focused') - frame = tabbed_browser.widget(0).page().mainFrame() - out = frame.evaluateJavaScript(js_code) + out = self._current_widget().page().mainFrame().evaluateJavaScript( + js_code) if out is None: # Getting the actual error (if any) seems to be difficult. The From 4204579c066684161cc13b16ab979e561fc592ae Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 19 May 2015 16:24:43 +0200 Subject: [PATCH 017/146] Add/improve tests for qutebrowser.utils.utils. --- qutebrowser/utils/utils.py | 8 +- tests/utils/test_utils.py | 668 ++++++++++++++++++++++++++++--------- 2 files changed, 513 insertions(+), 163 deletions(-) diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 920da944c..b113a2356 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -50,8 +50,6 @@ def elide(text, length): def compact_text(text, elidelength=None): """Remove leading whitespace and newlines from a text and maybe elide it. - FIXME: Add tests. - Args: text: The text to compact. elidelength: To how many chars to elide. @@ -105,12 +103,12 @@ def actute_warning(): try: if qtutils.version_check('5.3.0'): return - except ValueError: + except ValueError: # pragma: no cover pass try: with open('/usr/share/X11/locale/en_US.UTF-8/Compose', 'r', encoding='utf-8') as f: - for line in f: + for line in f: # pragma: no branch if '' in line: if sys.stdout is not None: sys.stdout.flush() @@ -118,7 +116,7 @@ def actute_warning(): "that is not a bug in qutebrowser! See " "https://bugs.freedesktop.org/show_bug.cgi?id=69476 " "for details.") - break + break # pragma: no branch except OSError: log.init.exception("Failed to read Compose file") diff --git a/tests/utils/test_utils.py b/tests/utils/test_utils.py index 588753587..52daab386 100644 --- a/tests/utils/test_utils.py +++ b/tests/utils/test_utils.py @@ -23,14 +23,22 @@ import sys import enum import datetime import os.path +import io +import logging +import functools from PyQt5.QtCore import Qt from PyQt5.QtGui import QColor import pytest +import qutebrowser +import qutebrowser.utils # for test_qualname from qutebrowser.utils import utils, qtutils +ELLIPSIS = '\u2026' + + class Color(QColor): """A QColor with a nicer repr().""" @@ -41,39 +49,193 @@ class Color(QColor): alpha=self.alpha()) +class TestCompactText: + + """Test compact_text.""" + + @pytest.mark.parametrize('text, expected', [ + ('foo\nbar', 'foobar'), + (' foo \n bar ', 'foobar'), + ('\nfoo\n', 'foo'), + ]) + def test_compact_text(self, text, expected): + """Test folding of newlines.""" + assert utils.compact_text(text) == expected + + @pytest.mark.parametrize('elidelength, text, expected', [ + (None, 'x' * 100, 'x' * 100), + (6, 'foobar', 'foobar'), + (5, 'foobar', 'foob' + ELLIPSIS), + (5, 'foo\nbar', 'foob' + ELLIPSIS), + (7, 'foo\nbar', 'foobar'), + ]) + def test_eliding(self, elidelength, text, expected): + """Test eliding.""" + assert utils.compact_text(text, elidelength) == expected + + class TestEliding: """Test elide.""" - ELLIPSIS = '\u2026' - def test_too_small(self): """Test eliding to 0 chars which should fail.""" with pytest.raises(ValueError): utils.elide('foo', 0) - def test_length_one(self): - """Test eliding to 1 char which should yield ...""" - assert utils.elide('foo', 1) == self.ELLIPSIS - - def test_fits(self): - """Test eliding with a string which fits exactly.""" - assert utils.elide('foo', 3) == 'foo' - - def test_elided(self): - """Test eliding with a string which should get elided.""" - assert utils.elide('foobar', 3) == 'fo' + self.ELLIPSIS + @pytest.mark.parametrize('text, length, expected', [ + ('foo', 1, ELLIPSIS), + ('foo', 3, 'foo'), + ('foobar', 3, 'fo' + ELLIPSIS), + ]) + def test_elided(self, text, length, expected): + assert utils.elide(text, length) == expected class TestReadFile: """Test read_file.""" + @pytest.fixture(autouse=True, params=[True, False]) + def freezer(self, request, monkeypatch): + if request.param: + monkeypatch.setattr(sys, 'frozen', True, raising=False) + monkeypatch.setattr('sys.executable', qutebrowser.__file__) + def test_readfile(self): """Read a test file.""" content = utils.read_file(os.path.join('utils', 'testfile')) assert content.splitlines()[0] == "Hello World!" + def test_readfile_binary(self): + """Read a test file in binary mode.""" + content = utils.read_file(os.path.join('utils', 'testfile'), + binary=True) + assert content.splitlines()[0] == b"Hello World!" + + +class Patcher: + + """Helper for TestActuteWarning. + + Attributes: + monkeypatch: The pytest monkeypatch fixture. + """ + + def __init__(self, monkeypatch): + self.monkeypatch = monkeypatch + + def patch_platform(self, platform='linux'): + """Patch sys.platform.""" + self.monkeypatch.setattr('sys.platform', platform) + + def patch_exists(self, exists=True): + """Patch os.path.exists.""" + self.monkeypatch.setattr('qutebrowser.utils.utils.os.path.exists', + lambda path: exists) + + def patch_version(self, version='5.2.0'): + """Patch Qt version.""" + self.monkeypatch.setattr( + 'qutebrowser.utils.utils.qtutils.qVersion', lambda: version) + + def patch_file(self, data): + """Patch open() to return the given data.""" + fake_file = io.StringIO(data) + self.monkeypatch.setattr(utils, 'open', + lambda filename, mode, encoding: fake_file, + raising=False) + + def patch_all(self, data): + """Patch everything so the issue would exist.""" + self.patch_platform() + self.patch_exists() + self.patch_version() + self.patch_file(data) + + +class TestActuteWarning: + + """Test actute_warning.""" + + @pytest.fixture + def patcher(self, monkeypatch): + """Fixture providing a Patcher helper.""" + return Patcher(monkeypatch) + + def test_non_linux(self, patcher, capsys): + """Test with a non-Linux OS.""" + patcher.patch_platform('toaster') + utils.actute_warning() + out, err = capsys.readouterr() + assert not out + assert not err + + def test_no_compose(self, patcher, capsys): + """Test with no compose file.""" + patcher.patch_platform() + patcher.patch_exists(False) + utils.actute_warning() + out, err = capsys.readouterr() + assert not out + assert not err + + def test_newer_qt(self, patcher, capsys): + """Test with compose file but newer Qt version.""" + patcher.patch_platform() + patcher.patch_exists() + patcher.patch_version('5.4') + utils.actute_warning() + out, err = capsys.readouterr() + assert not out + assert not err + + def test_no_match(self, patcher, capsys): + """Test with compose file and affected Qt but no match.""" + patcher.patch_all('foobar') + utils.actute_warning() + out, err = capsys.readouterr() + assert not out + assert not err + + def test_empty(self, patcher, capsys): + """Test with empty compose file.""" + patcher.patch_all(None) + utils.actute_warning() + out, err = capsys.readouterr() + assert not out + assert not err + + def test_match(self, patcher, capsys): + """Test with compose file and affected Qt and a match.""" + patcher.patch_all('foobar\n\nbaz') + utils.actute_warning() + out, err = capsys.readouterr() + assert out.startswith('Note: If you got a') + assert not err + + def test_match_stdout_none(self, monkeypatch, patcher, capsys): + """Test with a match and stdout being None.""" + patcher.patch_all('foobar\n\nbaz') + monkeypatch.setattr('sys.stdout', None) + utils.actute_warning() + + def test_unreadable(self, mocker, patcher, capsys, caplog): + """Test with an unreadable compose file.""" + patcher.patch_platform() + patcher.patch_exists() + patcher.patch_version() + mocker.patch('qutebrowser.utils.utils.open', side_effect=OSError, + create=True) + + with caplog.atLevel(logging.ERROR, 'init'): + utils.actute_warning() + + assert len(caplog.records()) == 1 + assert caplog.records()[0].message == 'Failed to read Compose file' + out, _err = capsys.readouterr() + assert not out + class TestInterpolateColor: @@ -164,62 +326,40 @@ class TestInterpolateColor: assert Color(color) == expected -class TestFormatSeconds: - - """Tests for format_seconds. - - Class attributes: - TESTS: A list of (input, output) tuples. - """ - - TESTS = [ - (-1, '-0:01'), - (0, '0:00'), - (59, '0:59'), - (60, '1:00'), - (60.4, '1:00'), - (61, '1:01'), - (-61, '-1:01'), - (3599, '59:59'), - (3600, '1:00:00'), - (3601, '1:00:01'), - (36000, '10:00:00'), - ] - - @pytest.mark.parametrize('seconds, out', TESTS) - def test_format_seconds(self, seconds, out): - """Test format_seconds with several tests.""" - assert utils.format_seconds(seconds) == out +@pytest.mark.parametrize('seconds, out', [ + (-1, '-0:01'), + (0, '0:00'), + (59, '0:59'), + (60, '1:00'), + (60.4, '1:00'), + (61, '1:01'), + (-61, '-1:01'), + (3599, '59:59'), + (3600, '1:00:00'), + (3601, '1:00:01'), + (36000, '10:00:00'), +]) +def test_format_seconds(seconds, out): + assert utils.format_seconds(seconds) == out -class TestFormatTimedelta: - - """Tests for format_timedelta. - - Class attributes: - TESTS: A list of (input, output) tuples. - """ - - TESTS = [ - (datetime.timedelta(seconds=-1), '-1s'), - (datetime.timedelta(seconds=0), '0s'), - (datetime.timedelta(seconds=59), '59s'), - (datetime.timedelta(seconds=120), '2m'), - (datetime.timedelta(seconds=60.4), '1m'), - (datetime.timedelta(seconds=63), '1m 3s'), - (datetime.timedelta(seconds=-64), '-1m 4s'), - (datetime.timedelta(seconds=3599), '59m 59s'), - (datetime.timedelta(seconds=3600), '1h'), - (datetime.timedelta(seconds=3605), '1h 5s'), - (datetime.timedelta(seconds=3723), '1h 2m 3s'), - (datetime.timedelta(seconds=3780), '1h 3m'), - (datetime.timedelta(seconds=36000), '10h'), - ] - - @pytest.mark.parametrize('td, out', TESTS) - def test_format_seconds(self, td, out): - """Test format_seconds with several tests.""" - assert utils.format_timedelta(td) == out +@pytest.mark.parametrize('td, out', [ + (datetime.timedelta(seconds=-1), '-1s'), + (datetime.timedelta(seconds=0), '0s'), + (datetime.timedelta(seconds=59), '59s'), + (datetime.timedelta(seconds=120), '2m'), + (datetime.timedelta(seconds=60.4), '1m'), + (datetime.timedelta(seconds=63), '1m 3s'), + (datetime.timedelta(seconds=-64), '-1m 4s'), + (datetime.timedelta(seconds=3599), '59m 59s'), + (datetime.timedelta(seconds=3600), '1h'), + (datetime.timedelta(seconds=3605), '1h 5s'), + (datetime.timedelta(seconds=3723), '1h 2m 3s'), + (datetime.timedelta(seconds=3780), '1h 3m'), + (datetime.timedelta(seconds=36000), '10h'), +]) +def test_format_timedelta(td, out): + assert utils.format_timedelta(td) == out class TestFormatSize: @@ -264,30 +404,24 @@ class TestKeyToString: """Test key_to_string.""" - def test_unicode_garbage_keys(self): + @pytest.mark.parametrize('key, expected', [ + (Qt.Key_Blue, 'Blue'), + (Qt.Key_Backtab, 'Tab'), + (Qt.Key_Escape, 'Escape'), + (Qt.Key_A, 'A'), + (Qt.Key_degree, '°'), + ]) + def test_normal(self, key, expected): """Test a special key where QKeyEvent::toString works incorrectly.""" - assert utils.key_to_string(Qt.Key_Blue) == 'Blue' + assert utils.key_to_string(key) == expected - def test_backtab(self): - """Test if backtab is normalized to tab correctly.""" - assert utils.key_to_string(Qt.Key_Backtab) == 'Tab' - - def test_escape(self): - """Test if escape is normalized to escape correctly.""" - assert utils.key_to_string(Qt.Key_Escape) == 'Escape' - - def test_letter(self): - """Test a simple letter key.""" + def test_missing(self, monkeypatch): + """Test with a missing key.""" + monkeypatch.delattr('qutebrowser.utils.utils.Qt.Key_Blue') + # We don't want to test the key which is actually missing - we only + # want to know if the mapping still behaves properly. assert utils.key_to_string(Qt.Key_A) == 'A' - def test_unicode(self): - """Test a printable unicode key.""" - assert utils.key_to_string(Qt.Key_degree) == '°' - - def test_special(self): - """Test a non-printable key handled by QKeyEvent::toString.""" - assert utils.key_to_string(Qt.Key_F1) == 'F1' - class TestKeyEventToString: @@ -323,26 +457,272 @@ class TestKeyEventToString: Qt.MetaModifier | Qt.ShiftModifier)) assert utils.keyevent_to_string(evt) == 'Ctrl+Alt+Meta+Shift+A' + def test_mac(self, monkeypatch, fake_keyevent_factory): + """Test with a simulated mac.""" + monkeypatch.setattr('sys.platform', 'darwin') + evt = fake_keyevent_factory(key=Qt.Key_A, modifiers=Qt.ControlModifier) + assert utils.keyevent_to_string(evt) == 'Meta+A' -class TestNormalize: - """Test normalize_keystr.""" +@pytest.mark.parametrize('orig, repl', [ + ('Control+x', 'ctrl+x'), + ('Windows+x', 'meta+x'), + ('Mod1+x', 'alt+x'), + ('Mod4+x', 'meta+x'), + ('Control--', 'ctrl+-'), + ('Windows++', 'meta++'), + ('ctrl-x', 'ctrl+x'), + ('control+x', 'ctrl+x') +]) +def test_normalize_keystr(orig, repl): + assert utils.normalize_keystr(orig) == repl - STRINGS = ( - ('Control+x', 'ctrl+x'), - ('Windows+x', 'meta+x'), - ('Mod1+x', 'alt+x'), - ('Mod4+x', 'meta+x'), - ('Control--', 'ctrl+-'), - ('Windows++', 'meta++'), - ('ctrl-x', 'ctrl+x'), - ('control+x', 'ctrl+x') - ) - @pytest.mark.parametrize('orig, repl', STRINGS) - def test_normalize(self, orig, repl): - """Test normalize with some strings.""" - assert utils.normalize_keystr(orig) == repl +class TestFakeIOStream: + + """Test FakeIOStream.""" + + def _write_func(self, text): + return text + + def test_flush(self): + """Smoke-test to see if flushing works.""" + s = utils.FakeIOStream(self._write_func) + s.flush() + + def test_isatty(self): + """Make sure isatty() is always false.""" + s = utils.FakeIOStream(self._write_func) + assert not s.isatty() + + def test_write(self): + """Make sure writing works.""" + s = utils.FakeIOStream(self._write_func) + assert s.write('echo') == 'echo' + + +class TestFakeIO: + + """Test FakeIO.""" + + @pytest.yield_fixture(autouse=True) + def restore_streams(self): + """Restore sys.stderr/sys.stdout after tests.""" + old_stdout = sys.stdout + old_stderr = sys.stderr + yield + sys.stdout = old_stdout + sys.stderr = old_stderr + + def test_normal(self, capsys): + """Test without changing sys.stderr/sys.stdout.""" + data = io.StringIO() + with utils.fake_io(data.write): + sys.stdout.write('hello\n') + sys.stderr.write('world\n') + + out, err = capsys.readouterr() + assert not out + assert not err + assert data.getvalue() == 'hello\nworld\n' + + sys.stdout.write('back to\n') + sys.stderr.write('normal\n') + out, err = capsys.readouterr() + assert out == 'back to\n' + assert err == 'normal\n' + + def test_stdout_replaced(self, capsys): + """Test with replaced stdout.""" + data = io.StringIO() + new_stdout = io.StringIO() + with utils.fake_io(data.write): + sys.stdout.write('hello\n') + sys.stderr.write('world\n') + sys.stdout = new_stdout + + out, err = capsys.readouterr() + assert not out + assert not err + assert data.getvalue() == 'hello\nworld\n' + + sys.stdout.write('still new\n') + sys.stderr.write('normal\n') + out, err = capsys.readouterr() + assert not out + assert err == 'normal\n' + assert new_stdout.getvalue() == 'still new\n' + + def test_stderr_replaced(self, capsys): + """Test with replaced stderr.""" + data = io.StringIO() + new_stderr = io.StringIO() + with utils.fake_io(data.write): + sys.stdout.write('hello\n') + sys.stderr.write('world\n') + sys.stderr = new_stderr + + out, err = capsys.readouterr() + assert not out + assert not err + assert data.getvalue() == 'hello\nworld\n' + + sys.stdout.write('normal\n') + sys.stderr.write('still new\n') + out, err = capsys.readouterr() + assert out == 'normal\n' + assert not err + assert new_stderr.getvalue() == 'still new\n' + + +class GotException(Exception): + + """Exception used for TestDisabledExcepthook.""" + + pass + + +def excepthook(_exc, _val, _tb): + return + + +def excepthook_2(_exc, _val, _tb): + return + + +class TestDisabledExcepthook: + + """Test disabled_excepthook. + + This doesn't test much as some things are untestable without triggering + the excepthook (which is hard to test). + """ + + @pytest.yield_fixture(autouse=True) + def restore_excepthook(self): + """Restore sys.excepthook and sys.__excepthook__ after tests.""" + old_excepthook = sys.excepthook + old_dunder_excepthook = sys.__excepthook__ + yield + sys.excepthook = old_excepthook + sys.__excepthook__ = old_dunder_excepthook + + def test_normal(self): + """Test without changing sys.excepthook.""" + sys.excepthook = excepthook + assert sys.excepthook is excepthook + with utils.disabled_excepthook(): + assert sys.excepthook is not excepthook + assert sys.excepthook is excepthook + + def test_changed(self): + """Test with changed sys.excepthook.""" + sys.excepthook = excepthook + with utils.disabled_excepthook(): + assert sys.excepthook is not excepthook + sys.excepthook = excepthook_2 + assert sys.excepthook is excepthook_2 + + +class TestPreventExceptions: + + """Test prevent_exceptions.""" + + @utils.prevent_exceptions(42) + def func_raising(self): + raise Exception + + def test_raising(self, caplog): + """Test with a raising function.""" + with caplog.atLevel(logging.ERROR, 'misc'): + ret = self.func_raising() + assert ret == 42 + assert len(caplog.records()) == 1 + expected = 'Error in test_utils.TestPreventExceptions.func_raising' + actual = caplog.records()[0].message + assert actual == expected + + @utils.prevent_exceptions(42) + def func_not_raising(self): + return 23 + + def test_not_raising(self, caplog): + """Test with a non-raising function.""" + with caplog.atLevel(logging.ERROR, 'misc'): + ret = self.func_not_raising() + assert ret == 23 + assert not caplog.records() + + @utils.prevent_exceptions(42, True) + def func_predicate_true(self): + raise Exception + + def test_predicate_true(self, caplog): + """Test with a True predicate.""" + with caplog.atLevel(logging.ERROR, 'misc'): + ret = self.func_predicate_true() + assert ret == 42 + assert len(caplog.records()) == 1 + + @utils.prevent_exceptions(42, False) + def func_predicate_false(self): + raise Exception + + def test_predicate_false(self, caplog): + """Test with a False predicate.""" + with caplog.atLevel(logging.ERROR, 'misc'): + with pytest.raises(Exception): + self.func_predicate_false() + assert not caplog.records() + + +class Obj: + + """Test object for test_get_repr().""" + + pass + + +@pytest.mark.parametrize('constructor, attrs, expected', [ + (False, {}, ''), + (False, {'foo': None}, ''), + (False, {'foo': "b'ar", 'baz': 2}, ''), + (True, {}, 'test_utils.Obj()'), + (True, {'foo': None}, 'test_utils.Obj(foo=None)'), + (True, {'foo': "te'st", 'bar': 2}, 'test_utils.Obj(bar=2, foo="te\'st")'), +]) +def test_get_repr(constructor, attrs, expected): + """Test get_repr().""" + assert utils.get_repr(Obj(), constructor, **attrs) == expected + + +class QualnameObj(): + + """Test object for test_qualname.""" + + def func(self): + """Test method for test_qualname.""" + pass + + +def qualname_func(_blah): + """Test function for test_qualname.""" + pass + + +@pytest.mark.parametrize('obj, expected', [ + (QualnameObj(), ''), # instance - unknown + (QualnameObj, 'test_utils.QualnameObj'), # class + (QualnameObj.func, 'test_utils.QualnameObj.func'), # unbound method + (QualnameObj().func, 'test_utils.QualnameObj.func'), # bound method + (qualname_func, 'test_utils.qualname_func'), # function + (functools.partial(qualname_func, True), 'test_utils.qualname_func'), + (qutebrowser, 'qutebrowser'), # module + (qutebrowser.utils, 'qutebrowser.utils'), # submodule + (utils, 'qutebrowser.utils.utils'), # submodule (from-import) +]) +def test_qualname(obj, expected): + assert utils.qualname(obj) == expected class TestIsEnum: @@ -412,20 +792,13 @@ class TestRaises: utils.raises(ValueError, self.do_raise) -class TestForceEncoding: - - """Test force_encoding.""" - - TESTS = [ - ('hello world', 'ascii', 'hello world'), - ('hellö wörld', 'utf-8', 'hellö wörld'), - ('hellö wörld', 'ascii', 'hell? w?rld'), - ] - - @pytest.mark.parametrize('inp, enc, expected', TESTS) - def test_fitting_ascii(self, inp, enc, expected): - """Test force_encoding will yield expected text.""" - assert utils.force_encoding(inp, enc) == expected +@pytest.mark.parametrize('inp, enc, expected', [ + ('hello world', 'ascii', 'hello world'), + ('hellö wörld', 'utf-8', 'hellö wörld'), + ('hellö wörld', 'ascii', 'hell? w?rld'), +]) +def test_force_encoding(inp, enc, expected): + assert utils.force_encoding(inp, enc) == expected class TestNewestSlice: @@ -437,44 +810,23 @@ class TestNewestSlice: with pytest.raises(ValueError): utils.newest_slice([], -2) - def test_count_minus_one(self): - """Test with a count of -1 (all elements).""" - items = range(20) - sliced = utils.newest_slice(items, -1) - assert list(sliced) == list(items) - - def test_count_zero(self): - """Test with a count of 0 (no elements).""" - items = range(20) - sliced = utils.newest_slice(items, 0) - assert list(sliced) == [] - - def test_count_much_smaller(self): - """Test with a count which is much smaller than the iterable.""" - items = range(20) - sliced = utils.newest_slice(items, 5) - assert list(sliced) == [15, 16, 17, 18, 19] - - def test_count_smaller(self): - """Test with a count which is exactly one smaller.""" - items = range(5) - sliced = utils.newest_slice(items, 4) - assert list(sliced) == [1, 2, 3, 4] - - def test_count_equal(self): - """Test with a count which is just as large as the iterable.""" - items = range(5) - sliced = utils.newest_slice(items, 5) - assert list(sliced) == list(items) - - def test_count_bigger(self): - """Test with a count which is one bigger than the iterable.""" - items = range(5) - sliced = utils.newest_slice(items, 6) - assert list(sliced) == list(items) - - def test_count_much_bigger(self): - """Test with a count which is much bigger than the iterable.""" - items = range(5) - sliced = utils.newest_slice(items, 50) - assert list(sliced) == list(items) + @pytest.mark.parametrize('items, count, expected', [ + # Count of -1 (all elements). + (range(20), -1, range(20)), + # Count of 0 (no elements). + (range(20), 0, []), + # Count which is much smaller than the iterable. + (range(20), 5, [15, 16, 17, 18, 19]), + # Count which is exactly one smaller.""" + (range(5), 4, [1, 2, 3, 4]), + # Count which is just as large as the iterable.""" + (range(5), 5, range(5)), + # Count which is one bigger than the iterable. + (range(5), 6, range(5)), + # Count which is much bigger than the iterable. + (range(5), 50, range(5)), + ]) + def test_good(self, items, count, expected): + """Test slices which shouldn't raise an exception.""" + sliced = utils.newest_slice(items, count) + assert list(sliced) == list(expected) From df53ccf42671276f9fd57a5d0b9857e70a335e8f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 24 May 2015 21:02:04 +0200 Subject: [PATCH 018/146] Write tests for qutebrowser.utils.version. --- qutebrowser/utils/version.py | 2 +- tests/utils/test_version.py | 657 +++++++++++++++++++++++++++++++++++ 2 files changed, 658 insertions(+), 1 deletion(-) create mode 100644 tests/utils/test_version.py diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index add7e4c84..88c9f0683 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -111,7 +111,7 @@ def _release_info(): for fn in glob.glob("/etc/*-release"): try: with open(fn, 'r', encoding='utf-8') as f: - data.append((fn, ''.join(f.readlines()))) + data.append((fn, ''.join(f.readlines()))) # pragma: no branch except OSError: log.misc.exception("Error while reading {}.".format(fn)) return data diff --git a/tests/utils/test_version.py b/tests/utils/test_version.py new file mode 100644 index 000000000..7978c06a7 --- /dev/null +++ b/tests/utils/test_version.py @@ -0,0 +1,657 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2015 Florian Bruhin (The Compiler) +# +# 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 . + +# pylint: disable=protected-access + +"""Tests for qutebrowser.utils.version.""" + +import io +import sys +import os.path +import subprocess +import contextlib +import builtins +import types +import importlib +import logging + +import pytest + +import qutebrowser +from qutebrowser.utils import version + + +class GitStrSubprocessFake: + + """Object returned by the git_str_subprocess_fake fixture. + + This provides a function which is used to patch _git_str_subprocess. + + Attributes: + retval: The value to return when called. Needs to be set before func is + called. + """ + + UNSET = object() + + def __init__(self): + self.retval = self.UNSET + + def func(self, gitpath): + """Function called instead of _git_str_subprocess. + + Checks whether the path passed is what we expected, and returns + self.retval. + """ + if self.retval is self.UNSET: + raise ValueError("func got called without retval being set!") + retval = self.retval + self.retval = self.UNSET + gitpath = os.path.normpath(gitpath) + expected = os.path.abspath(os.path.join( + os.path.dirname(qutebrowser.__file__), os.pardir)) + assert gitpath == expected + return retval + + +class TestGitStr: + + """Tests for _git_str().""" + + @pytest.yield_fixture + def commit_file_mock(self, mocker): + """Fixture providing a mock for utils.read_file for git-commit-id. + + On fixture teardown, it makes sure it got called with git-commit-id as + argument. + """ + mocker.patch('qutebrowser.utils.version.subprocess', + side_effect=AssertionError) + m = mocker.patch('qutebrowser.utils.version.utils.read_file') + yield m + m.assert_called_with('git-commit-id') + + @pytest.fixture + def git_str_subprocess_fake(self, mocker, monkeypatch): + """Fixture patching _git_str_subprocess with a GitStrSubprocessFake.""" + mocker.patch('qutebrowser.utils.version.subprocess', + side_effect=AssertionError) + fake = GitStrSubprocessFake() + monkeypatch.setattr('qutebrowser.utils.version._git_str_subprocess', + fake.func) + return fake + + def test_frozen_ok(self, commit_file_mock, monkeypatch): + """Test with sys.frozen=True and a successful git-commit-id read.""" + monkeypatch.setattr(qutebrowser.utils.version.sys, 'frozen', True, + raising=False) + commit_file_mock.return_value = 'deadbeef' + assert version._git_str() == 'deadbeef' + + def test_frozen_oserror(self, commit_file_mock, monkeypatch): + """Test with sys.frozen=True and OSError when reading git-commit-id.""" + monkeypatch.setattr(qutebrowser.utils.version.sys, 'frozen', True, + raising=False) + commit_file_mock.side_effect = OSError + assert version._git_str() is None + + def test_normal_successful(self, git_str_subprocess_fake): + """Test with git returning a successful result.""" + git_str_subprocess_fake.retval = 'c0ffeebabe' + assert version._git_str() == 'c0ffeebabe' + + def test_normal_error(self, commit_file_mock, git_str_subprocess_fake): + """Test without repo (but git-commit-id).""" + git_str_subprocess_fake.retval = None + commit_file_mock.return_value = '1b4d1dea' + assert version._git_str() == '1b4d1dea' + + def test_normal_path_oserror(self, mocker, git_str_subprocess_fake): + """Test with things raising OSError.""" + mocker.patch('qutebrowser.utils.version.os.path.join', + side_effect=OSError) + mocker.patch('qutebrowser.utils.version.utils.read_file', + side_effect=OSError) + assert version._git_str() is None + + def test_normal_path_nofile(self, monkeypatch, caplog, + git_str_subprocess_fake, commit_file_mock): + """Test with undefined __file__ but available git-commit-id.""" + monkeypatch.delattr('qutebrowser.utils.version.__file__') + commit_file_mock.return_value = '0deadcode' + with caplog.atLevel(logging.ERROR, 'misc'): + assert version._git_str() == '0deadcode' + assert len(caplog.records()) == 1 + assert caplog.records()[0].message == "Error while getting git path" + + +def _has_git(): + """Check if git is installed.""" + try: + subprocess.check_call(['git', '--version'], stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) + except (OSError, subprocess.CalledProcessError): + return False + else: + return True + + +# Decorator for tests needing git, so they get skipped when it's unavailable. +needs_git = pytest.mark.skipif(not _has_git(), reason='Needs git installed.') + + +class TestGitStrSubprocess: + + """Tests for _git_str_subprocess.""" + + @pytest.fixture + def git_repo(self, tmpdir): + """A fixture to create a temporary git repo. + + Some things are tested against a real repo so we notice if something in + git would change, or we call git incorrectly. + """ + def _git(*args): + """Helper closure to call git.""" + env = { + 'GIT_AUTHOR_NAME': 'qutebrowser testsuite', + 'GIT_AUTHOR_EMAIL': 'mail@qutebrowser.org', + 'GIT_AUTHOR_DATE': 'Thu 1 Jan 01:00:00 CET 1970', + 'GIT_COMMITTER_NAME': 'qutebrowser testsuite', + 'GIT_COMMITTER_EMAIL': 'mail@qutebrowser.org', + 'GIT_COMMITTER_DATE': 'Thu 1 Jan 01:00:00 CET 1970', + } + subprocess.check_call(['git', '-C', str(tmpdir)] + list(args), + env=env) + + (tmpdir / 'file').write_text("Hello World!", encoding='utf-8') + _git('init') + _git('add', 'file') + _git('commit', '-am', 'foo', '--no-verify', '--no-edit', + '--no-post-rewrite', '--quiet', '--no-gpg-sign') + _git('tag', 'foobar') + return tmpdir + + @needs_git + def test_real_git(self, git_repo): + """Test with a real git repository.""" + ret = version._git_str_subprocess(str(git_repo)) + assert ret == 'foobar (1970-01-01 01:00:00 +0100)' + + def test_missing_dir(self, tmpdir): + """Test with a directory which doesn't exist.""" + ret = version._git_str_subprocess(str(tmpdir / 'does-not-exist')) + assert ret is None + + @pytest.mark.parametrize('exc', [ + OSError, + subprocess.CalledProcessError(1, 'foobar') + ]) + def test_exception(self, exc, mocker, tmpdir): + """Test with subprocess.check_output raising an exception. + + Args: + exc: The exception to raise. + """ + mocker.patch('qutebrowser.utils.version.subprocess.os.path.isdir', + return_value=True) + mocker.patch('qutebrowser.utils.version.subprocess.check_output', + side_effect=exc) + ret = version._git_str_subprocess(str(tmpdir)) + assert ret is None + + +class ReleaseInfoFake: + + """An object providing fakes for glob.glob/open for test_release_info. + + Attributes: + _files: The files which should be returned, or None if an exception + should be raised. A {filename: [lines]} dict. + """ + + def __init__(self, files): + self._files = files + + def glob_fake(self, pattern): + """Fake for glob.glob. + + Verifies the arguments and returns the files listed in self._files, or + a single fake file if an exception is expected. + """ + assert pattern == '/etc/*-release' + if self._files is None: + return ['fake-file'] + else: + return sorted(list(self._files)) + + @contextlib.contextmanager + def open_fake(self, filename, mode, encoding): + """Fake for open(). + + Verifies the arguments and returns a StringIO with the content listed + in self._files. + """ + assert mode == 'r' + assert encoding == 'utf-8' + if self._files is None: + raise OSError + yield io.StringIO(''.join(self._files[filename])) + + +@pytest.mark.parametrize('files, expected', [ + ({}, []), + ({'file': ['']}, [('file', '')]), + ({'file': []}, [('file', '')]), + ( + {'file1': ['foo\n', 'bar\n'], 'file2': ['baz\n']}, + [('file1', 'foo\nbar\n'), ('file2', 'baz\n')] + ), + (None, []), +]) +def test_release_info(files, expected, caplog, monkeypatch): + """Test _release_info(). + + Args: + files: The file dict passed to ReleaseInfoFake. + expected: The expected _release_info output. + """ + fake = ReleaseInfoFake(files) + monkeypatch.setattr('qutebrowser.utils.version.glob.glob', fake.glob_fake) + monkeypatch.setattr(version, 'open', fake.open_fake, raising=False) + with caplog.atLevel(logging.ERROR, 'misc'): + assert version._release_info() == expected + if files is None: + assert len(caplog.records()) == 1 + assert caplog.records()[0].message == "Error while reading fake-file." + + +class ImportFake: + + """A fake for __import__ which is used by the import_fake fixture. + + Attributes: + exists: A dict mapping module names to bools. If True, the import will + success. Otherwise, it'll fail with ImportError. + version_attribute: The name to use in the fake modules for the version + attribute. + version: The version to use for the modules. + _real_import: Saving the real __import__ builtin so the imports can be + done normally for modules not in self.exists. + """ + + def __init__(self): + self.exists = { + 'sip': True, + 'colorlog': True, + 'colorama': True, + 'pypeg2': True, + 'jinja2': True, + 'pygments': True, + 'yaml': True, + } + self.version_attribute = '__version__' + self.version = '1.2.3' + self._real_import = builtins.__import__ + + def _do_import(self, name): + """Helper for fake_import and fake_importlib_import to do the work. + + Return: + The imported fake module, or None if normal importing should be + used. + """ + if name not in self.exists: + # Not one of the modules to test -> use real import + return None + elif self.exists[name]: + ns = types.SimpleNamespace() + if self.version_attribute is not None: + setattr(ns, self.version_attribute, self.version) + return ns + else: + raise ImportError("Fake ImportError for {}.".format(name)) + + def fake_import(self, name, *args, **kwargs): + """Fake for the builtin __import__.""" + module = self._do_import(name) + if module is not None: + return module + else: + return self._real_import(name, *args, **kwargs) + + def fake_importlib_import(self, name): + """Fake for importlib.import_module.""" + module = self._do_import(name) + if module is not None: + return module + else: + return importlib.import_module(name) + + +@pytest.fixture +def import_fake(monkeypatch): + """Fixture to patch imports using ImportFake.""" + fake = ImportFake() + monkeypatch.setattr('builtins.__import__', fake.fake_import) + monkeypatch.setattr('qutebrowser.utils.version.importlib.import_module', + fake.fake_importlib_import) + return fake + + +class TestModuleVersions: + + """Tests for _module_versions().""" + + @pytest.mark.usefixtures('import_fake') + def test_all_present(self): + """Test with all modules present in version 1.2.3.""" + expected = ['sip: yes', 'colorlog: yes', 'colorama: 1.2.3', + 'pypeg2: 1.2.3', 'jinja2: 1.2.3', 'pygments: 1.2.3', + 'yaml: 1.2.3'] + assert version._module_versions() == expected + + @pytest.mark.parametrize('module, idx, expected', [ + ('colorlog', 1, 'colorlog: no'), + ('colorama', 2, 'colorama: no'), + ]) + def test_missing_module(self, module, idx, expected, import_fake): + """Test with a module missing. + + Args: + module: The name of the missing module. + idx: The index where the given text is expected. + expected: The expected text. + """ + import_fake.exists[module] = False + assert version._module_versions()[idx] == expected + + @pytest.mark.parametrize('value, expected', [ + ('VERSION', ['sip: yes', 'colorlog: yes', 'colorama: 1.2.3', + 'pypeg2: yes', 'jinja2: yes', 'pygments: yes', + 'yaml: yes']), + ('SIP_VERSION_STR', ['sip: 1.2.3', 'colorlog: yes', 'colorama: yes', + 'pypeg2: yes', 'jinja2: yes', 'pygments: yes', + 'yaml: yes']), + (None, ['sip: yes', 'colorlog: yes', 'colorama: yes', 'pypeg2: yes', + 'jinja2: yes', 'pygments: yes', 'yaml: yes']), + ]) + def test_version_attribute(self, value, expected, import_fake): + """Test with a different version attribute. + + VERSION is tested for old colorama versions, and None to make sure + things still work if some package suddenly doesn't have __version__. + + Args: + value: The name of the version attribute. + expected: The expected return value. + """ + import_fake.version_attribute = value + assert version._module_versions() == expected + + @pytest.mark.parametrize('name, has_version', [ + ('colorlog', False), + ('sip', False), + ('colorama', True), + ('pypeg2', True), + ('jinja2', True), + ('pygments', True), + ('yaml', True), + ]) + def test_existing_attributes(self, name, has_version): + """Check if all dependencies have an expected __version__ attribute. + + The aim of this test is to fail if modules suddenly don't have a + __version__ attribute anymore in a newer version, or colorlog has one. + + Args: + name: The name of the module to check. + has_version: Whether a __version__ attribute is expected. + """ + module = importlib.import_module(name) + assert hasattr(module, '__version__') == has_version + + def test_existing_sip_attribute(self): + """Test if sip has a SIP_VERSION_STR attribute. + + The aim of this test is to fail if that gets missing in some future + version of sip. + """ + import sip + assert isinstance(sip.SIP_VERSION_STR, str) + + +class TestOsInfo: + + """Tests for _os_info.""" + + @pytest.mark.parametrize('dist, dist_str', [ + (('x', '', 'y'), 'x, y'), + (('a', 'b', 'c'), 'a, b, c'), + (('', '', ''), ''), + ]) + def test_linux_fake(self, monkeypatch, dist, dist_str): + """Test with a fake Linux. + + Args: + dist: The value to set platform.dist() to. + dist_str: The expected distribution string in version._os_info(). + """ + monkeypatch.setattr('qutebrowser.utils.version.sys.platform', 'linux') + monkeypatch.setattr('qutebrowser.utils.version._release_info', + lambda: [('releaseinfo', 'Hello World')]) + monkeypatch.setattr('qutebrowser.utils.version.platform.dist', + lambda: dist) + ret = version._os_info() + expected = ['OS Version: {}'.format(dist_str), '', + '--- releaseinfo ---', 'Hello World'] + assert ret == expected + + def test_windows_fake(self, monkeypatch): + """Test with a fake Windows.""" + monkeypatch.setattr('qutebrowser.utils.version.sys.platform', 'win32') + monkeypatch.setattr('qutebrowser.utils.version.platform.win32_ver', + lambda: ('eggs', 'bacon', 'ham', 'spam')) + ret = version._os_info() + expected = ['OS Version: eggs, bacon, ham, spam'] + assert ret == expected + + @pytest.mark.parametrize('mac_ver, mac_ver_str', [ + (('x', ('', '', ''), 'y'), 'x, y'), + (('', ('', '', ''), ''), ''), + (('x', ('1', '2', '3'), 'y'), 'x, 1.2.3, y'), + ]) + def test_os_x_fake(self, monkeypatch, mac_ver, mac_ver_str): + """Test with a fake OS X. + + Args: + mac_ver: The tuple to set platform.mac_ver() to. + mac_ver_str: The expected Mac version string in version._os_info(). + """ + monkeypatch.setattr('qutebrowser.utils.version.sys.platform', 'darwin') + monkeypatch.setattr('qutebrowser.utils.version.platform.mac_ver', + lambda: mac_ver) + ret = version._os_info() + expected = ['OS Version: {}'.format(mac_ver_str)] + assert ret == expected + + def test_unknown_fake(self, monkeypatch): + """Test with a fake unknown sys.platform.""" + monkeypatch.setattr('qutebrowser.utils.version.sys.platform', + 'toaster') + ret = version._os_info() + expected = ['OS Version: ?'] + assert ret == expected + + @pytest.mark.skipif(sys.platform != 'linux', reason="requires Linux") + def test_linux_real(self): + """Make sure there are no exceptions with a real Linux.""" + version._os_info() + + @pytest.mark.skipif(sys.platform != 'win32', reason="requires Windows") + def test_windows_real(self): + """Make sure there are no exceptions with a real Windows.""" + version._os_info() + + @pytest.mark.skipif(sys.platform != 'darwin', reason="requires OS X") + def test_os_x_real(self): + """Make sure there are no exceptions with a real OS X.""" + version._os_info() + + +class FakeQSslSocket: + + """Fake for the QSslSocket Qt class. + + Attributes: + _version: What QSslSocket::sslLibraryVersionString() should return. + """ + + def __init__(self, version=None): + self._version = version + + def supportsSsl(self): + """Fake for QSslSocket::supportsSsl().""" + return True + + def sslLibraryVersionString(self): + """Fake for QSslSocket::sslLibraryVersionString().""" + if self._version is None: + raise AssertionError("Got valled with version None!") + return self._version + + +class TestVersion: + + """Tests for version.""" + + @pytest.fixture(autouse=True) + def patch(self, monkeypatch): + """Patch some sub-functions we're not interested in.""" + monkeypatch.setattr('qutebrowser.utils.version._git_str', lambda: None) + monkeypatch.setattr('qutebrowser.utils.version._module_versions', + lambda: []) + monkeypatch.setattr('qutebrowser.utils.version._os_info', lambda: []) + + def test_qutebrowser_version(self, monkeypatch): + """Test the qutebrowser version in the output.""" + monkeypatch.setattr( + 'qutebrowser.utils.version.qutebrowser.__version__', '23.42') + lines = version.version().splitlines() + assert lines[0] == 'qutebrowser v23.42' + + def test_git_commit(self, monkeypatch): + """Test the git commit in the output.""" + monkeypatch.setattr('qutebrowser.utils.version._git_str', + lambda: 'deadbeef') + lines = version.version().splitlines() + assert lines[1] == 'Git commit: deadbeef' + + def test_no_git_commit(self, monkeypatch): + """Test the git commit with _git_str returning None.""" + monkeypatch.setattr('qutebrowser.utils.version._git_str', + lambda: None) + lines = version.version().splitlines() + assert not lines[1].startswith('Git commit:') + + def test_python_version(self, monkeypatch): + """Test the python version in the output.""" + monkeypatch.setattr( + 'qutebrowser.utils.version.platform.python_implementation', + lambda: 'python_implementation') + monkeypatch.setattr( + 'qutebrowser.utils.version.platform.python_version', + lambda: 'python_version') + lines = version.version().splitlines() + assert lines[2] == 'python_implementation: python_version' + + def test_qt_version(self, monkeypatch): + """Test the python version in the output.""" + monkeypatch.setattr('qutebrowser.utils.version.QT_VERSION_STR', '12.3') + monkeypatch.setattr('qutebrowser.utils.version.qVersion', + lambda: '45.6') + lines = version.version().splitlines() + assert lines[3] == 'Qt: 12.3, runtime: 45.6' + + def test_pyqt_version(self, monkeypatch): + """Test the PyQt version in the output.""" + monkeypatch.setattr('qutebrowser.utils.version.PYQT_VERSION_STR', + '78.9') + lines = version.version().splitlines() + assert lines[4] == 'PyQt: 78.9' + + def test_module_versions(self, monkeypatch): + """Test module versions in the output.""" + monkeypatch.setattr('qutebrowser.utils.version._module_versions', + lambda: ['Hello', 'World']) + lines = version.version().splitlines() + assert (lines[5], lines[6]) == ('Hello', 'World') + + def test_webkit_version(self, monkeypatch): + """Test the webkit version in the output.""" + monkeypatch.setattr('qutebrowser.utils.version.qWebKitVersion', + lambda: '567.1') + lines = version.version().splitlines() + assert lines[5] == 'Webkit: 567.1' + + def test_harfbuzz_none(self, monkeypatch): + """Test harfbuzz output with QT_HARFBUZZ unset.""" + monkeypatch.delenv('QT_HARFBUZZ', raising=False) + lines = version.version().splitlines() + assert lines[6] == 'Harfbuzz: system' + + def test_harfbuzz_set(self, monkeypatch): + """Test harfbuzz output with QT_HARFBUZZ set.""" + monkeypatch.setenv('QT_HARFBUZZ', 'new') + lines = version.version().splitlines() + assert lines[6] == 'Harfbuzz: new' + + def test_ssl(self, monkeypatch): + """Test SSL version in the output.""" + monkeypatch.setattr('qutebrowser.utils.version.QSslSocket', + FakeQSslSocket('1.0.1')) + lines = version.version().splitlines() + assert lines[7] == 'SSL: 1.0.1' + + @pytest.mark.parametrize('frozen, expected', [(True, 'Frozen: True'), + (False, 'Frozen: False')]) + def test_frozen(self, monkeypatch, frozen, expected): + """Test "Frozen: ..." in the version output.""" + if frozen: + monkeypatch.setattr(sys, 'frozen', True, raising=False) + else: + monkeypatch.delattr(sys, 'frozen', raising=False) + lines = version.version().splitlines() + assert lines[9] == expected + + def test_platform(self, monkeypatch): + """Test platform in the version output.""" + monkeypatch.setattr('qutebrowser.utils.version.platform.platform', + lambda: 'toaster') + monkeypatch.setattr('qutebrowser.utils.version.platform.architecture', + lambda: ('64bit', '')) + lines = version.version().splitlines() + assert lines[10] == 'Platform: toaster, 64bit' + + def test_os_info(self, monkeypatch): + """Test OS info in the output.""" + monkeypatch.setattr('qutebrowser.utils.version._os_info', + lambda: ['Hello', 'World']) + lines = version.version().splitlines() + assert (lines[11], lines[12]) == ('Hello', 'World') From e60f698615e76d396bba38bf25ae061f00d49b6a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 25 May 2015 01:20:37 +0200 Subject: [PATCH 019/146] Add/improve tests for qutebrowser.utils.standarddir. --- qutebrowser/utils/standarddir.py | 3 +- tests/utils/test_standarddir.py | 134 ++++++++++++++++++++++++++++--- 2 files changed, 126 insertions(+), 11 deletions(-) diff --git a/qutebrowser/utils/standarddir.py b/qutebrowser/utils/standarddir.py index 9dd8c5752..6d6e5a6dd 100644 --- a/qutebrowser/utils/standarddir.py +++ b/qutebrowser/utils/standarddir.py @@ -170,6 +170,7 @@ def _init_cachedir_tag(): f.write("# This file is a cache directory tag created by " "qutebrowser.\n") f.write("# For information about cache directory tags, see:\n") - f.write("# http://www.brynosaurus.com/cachedir/\n") + f.write("# http://www.brynosaurus.com/" # pragma: no branch + "cachedir/\n") except OSError: log.init.exception("Failed to create CACHEDIR.TAG") diff --git a/tests/utils/test_standarddir.py b/tests/utils/test_standarddir.py index 6be44d92c..02bcc02d6 100644 --- a/tests/utils/test_standarddir.py +++ b/tests/utils/test_standarddir.py @@ -17,6 +17,8 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . +# pylint: disable=protected-access + """Tests for qutebrowser.utils.standarddir.""" import os @@ -24,7 +26,10 @@ import os.path import sys import types import collections +import logging +import textwrap +from PyQt5.QtCore import QStandardPaths from PyQt5.QtWidgets import QApplication import pytest @@ -44,8 +49,61 @@ def change_qapp_name(): QApplication.instance().setApplicationName(old_name) +@pytest.fixture +def no_cachedir_tag(monkeypatch): + """Fixture to prevent writing a CACHEDIR.TAG.""" + monkeypatch.setattr('qutebrowser.utils.standarddir._init_cachedir_tag', + lambda: None) + + +@pytest.fixture(autouse=True) +@pytest.mark.usefixtures('no_cachedir_tag') +def reset_standarddir(): + standarddir.init(None) + + +@pytest.mark.parametrize('data_subdir, config_subdir, expected', [ + ('foo', 'foo', 'foo/data'), + ('foo', 'bar', 'foo'), +]) +def test_get_fake_windows_equal_dir(data_subdir, config_subdir, expected, + monkeypatch, tmpdir): + """Test _get with a fake Windows OS with equal data/config dirs.""" + locations = { + QStandardPaths.DataLocation: str(tmpdir / data_subdir), + QStandardPaths.ConfigLocation: str(tmpdir / config_subdir), + } + monkeypatch.setattr('qutebrowser.utils.standarddir.os.name', 'nt') + monkeypatch.setattr( + 'qutebrowser.utils.standarddir.QStandardPaths.writableLocation', + locations.get) + expected = str(tmpdir / expected) + assert standarddir.data() == expected + + +class TestWritableLocation: + + """Tests for _writable_location.""" + + def test_empty(self, monkeypatch): + """Test QStandardPaths returning an empty value.""" + monkeypatch.setattr( + 'qutebrowser.utils.standarddir.QStandardPaths.writableLocation', + lambda typ: '') + with pytest.raises(ValueError): + standarddir._writable_location(QStandardPaths.DataLocation) + + def test_sep(self, monkeypatch): + """Make sure the right kind of separator is used.""" + monkeypatch.setattr('qutebrowser.utils.standarddir.os.sep', '\\') + loc = standarddir._writable_location(QStandardPaths.DataLocation) + assert '/' not in loc + assert '\\' in loc + + @pytest.mark.skipif(not sys.platform.startswith("linux"), reason="requires Linux") +@pytest.mark.usefixtures('no_cachedir_tag') class TestGetStandardDirLinux: """Tests for standarddir under Linux.""" @@ -53,26 +111,22 @@ class TestGetStandardDirLinux: def test_data_explicit(self, monkeypatch, tmpdir): """Test data dir with XDG_DATA_HOME explicitly set.""" monkeypatch.setenv('XDG_DATA_HOME', str(tmpdir)) - standarddir.init(None) assert standarddir.data() == str(tmpdir / 'qutebrowser_test') def test_config_explicit(self, monkeypatch, tmpdir): """Test config dir with XDG_CONFIG_HOME explicitly set.""" monkeypatch.setenv('XDG_CONFIG_HOME', str(tmpdir)) - standarddir.init(None) assert standarddir.config() == str(tmpdir / 'qutebrowser_test') def test_cache_explicit(self, monkeypatch, tmpdir): """Test cache dir with XDG_CACHE_HOME explicitly set.""" monkeypatch.setenv('XDG_CACHE_HOME', str(tmpdir)) - standarddir.init(None) assert standarddir.cache() == str(tmpdir / 'qutebrowser_test') def test_data(self, monkeypatch, tmpdir): """Test data dir with XDG_DATA_HOME not set.""" monkeypatch.setenv('HOME', str(tmpdir)) monkeypatch.delenv('XDG_DATA_HOME', raising=False) - standarddir.init(None) expected = tmpdir / '.local' / 'share' / 'qutebrowser_test' assert standarddir.data() == str(expected) @@ -80,7 +134,6 @@ class TestGetStandardDirLinux: """Test config dir with XDG_CONFIG_HOME not set.""" monkeypatch.setenv('HOME', str(tmpdir)) monkeypatch.delenv('XDG_CONFIG_HOME', raising=False) - standarddir.init(None) expected = tmpdir / '.config' / 'qutebrowser_test' assert standarddir.config() == str(expected) @@ -88,21 +141,17 @@ class TestGetStandardDirLinux: """Test cache dir with XDG_CACHE_HOME not set.""" monkeypatch.setenv('HOME', str(tmpdir)) monkeypatch.delenv('XDG_CACHE_HOME', raising=False) - standarddir.init(None) expected = tmpdir / '.cache' / 'qutebrowser_test' assert standarddir.cache() == expected @pytest.mark.skipif(not sys.platform.startswith("win"), reason="requires Windows") +@pytest.mark.usefixtures('no_cachedir_tag') class TestGetStandardDirWindows: """Tests for standarddir under Windows.""" - @pytest.fixture(autouse=True) - def reset_standarddir(self): - standarddir.init(None) - def test_data(self): """Test data dir.""" expected = ['qutebrowser_test', 'data'] @@ -121,6 +170,7 @@ class TestGetStandardDirWindows: DirArgTest = collections.namedtuple('DirArgTest', 'arg, expected') +@pytest.mark.usefixtures('no_cachedir_tag') class TestArguments: """Tests with confdir/cachedir/datadir arguments.""" @@ -131,6 +181,7 @@ class TestArguments: if request.param.expected is None: return request.param else: + # prepend tmpdir to both arg = str(tmpdir / request.param.arg) return DirArgTest(arg, arg) @@ -155,6 +206,21 @@ class TestArguments: standarddir.init(args) assert standarddir.data() == testcase.expected + def test_confdir_none(self): + """Test --confdir with None given.""" + args = types.SimpleNamespace(confdir=None, cachedir=None, datadir=None) + standarddir.init(args) + assert standarddir.config().split(os.sep)[-1] == 'qutebrowser_test' + + def test_runtimedir(self, tmpdir, monkeypatch): + """Test runtime dir (which has no args).""" + monkeypatch.setattr( + 'qutebrowser.utils.standarddir.QStandardPaths.writableLocation', + lambda _typ: str(tmpdir)) + args = types.SimpleNamespace(confdir=None, cachedir=None, datadir=None) + standarddir.init(args) + assert standarddir.runtime() == str(tmpdir) + @pytest.mark.parametrize('typ', ['config', 'data', 'cache', 'download', 'runtime']) def test_basedir(self, tmpdir, typ): @@ -164,3 +230,51 @@ class TestArguments: standarddir.init(args) func = getattr(standarddir, typ) assert func() == expected + + +class TestInitCacheDirTag: + + """Tests for _init_cachedir_tag.""" + + def test_no_cache_dir(self, mocker, monkeypatch): + """Smoke test with cache() returning None.""" + monkeypatch.setattr('qutebrowser.utils.standarddir.cache', + lambda: None) + mocker.patch('builtins.open', side_effect=AssertionError) + standarddir._init_cachedir_tag() + + def test_existant_cache_dir_tag(self, tmpdir, mocker, monkeypatch): + """Test with an existant CACHEDIR.TAG.""" + monkeypatch.setattr('qutebrowser.utils.standarddir.cache', + lambda: str(tmpdir)) + mocker.patch('builtins.open', side_effect=AssertionError) + m = mocker.patch('qutebrowser.utils.standarddir.os.path.exists', + return_value=True) + standarddir._init_cachedir_tag() + assert not tmpdir.listdir() + m.assert_called_with(str(tmpdir / 'CACHEDIR.TAG')) + + def test_new_cache_dir_tag(self, tmpdir, mocker, monkeypatch): + """Test creating a new CACHEDIR.TAG.""" + monkeypatch.setattr('qutebrowser.utils.standarddir.cache', + lambda: str(tmpdir)) + standarddir._init_cachedir_tag() + assert tmpdir.listdir() == [(tmpdir / 'CACHEDIR.TAG')] + data = (tmpdir / 'CACHEDIR.TAG').read_text('utf-8') + assert data == textwrap.dedent(""" + Signature: 8a477f597d28d172789f06886806bc55 + # This file is a cache directory tag created by qutebrowser. + # For information about cache directory tags, see: + # http://www.brynosaurus.com/cachedir/ + """).lstrip() + + def test_open_oserror(self, caplog, tmpdir, mocker, monkeypatch): + """Test creating a new CACHEDIR.TAG.""" + monkeypatch.setattr('qutebrowser.utils.standarddir.cache', + lambda: str(tmpdir)) + mocker.patch('builtins.open', side_effect=OSError) + with caplog.atLevel(logging.ERROR, 'misc'): + standarddir._init_cachedir_tag() + assert len(caplog.records()) == 1 + assert caplog.records()[0].message == 'Failed to create CACHEDIR.TAG' + assert not tmpdir.listdir() From 1e982a9a84784b870a00fba13c689e146d1ffbdc Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 25 May 2015 01:21:22 +0200 Subject: [PATCH 020/146] Add/improve tests for qutebrowser.utils.qtutils. --- tests/utils/test_qtutils.py | 889 +++++++++++++++++++++++++++++++++++- 1 file changed, 876 insertions(+), 13 deletions(-) diff --git a/tests/utils/test_qtutils.py b/tests/utils/test_qtutils.py index 85a99c2f4..220297761 100644 --- a/tests/utils/test_qtutils.py +++ b/tests/utils/test_qtutils.py @@ -19,15 +19,51 @@ """Tests for qutebrowser.utils.qtutils.""" +import io +import os import sys +import operator +import os.path +try: + from test import test_file +except ImportError: + # Debian patches Python to remove the tests... + test_file = None import pytest +import unittest +import unittest.mock +from PyQt5.QtCore import (QDataStream, QPoint, QUrl, QByteArray, QIODevice, + QTimer, QBuffer, QFile, QProcess) +from PyQt5.QtWidgets import QApplication from qutebrowser import qutebrowser from qutebrowser.utils import qtutils import overflow_test_cases +@pytest.mark.parametrize('qversion, version, op, expected', [ + ('5.4.0', '5.4.0', operator.ge, True), + ('5.4.0', '5.4.0', operator.eq, True), + ('5.4.0', '5.4', operator.eq, True), + ('5.4.1', '5.4', operator.ge, True), + ('5.3.2', '5.4', operator.ge, False), + ('5.3.0', '5.3.2', operator.ge, False), +]) +def test_version_check(monkeypatch, qversion, version, op, expected): + """Test for version_check(). + + Args: + monkeypatch: The pytest monkeypatch fixture. + qversion: The version to set as fake qVersion(). + version: The version to compare with. + op: The operator to use when comparing. + expected: The expected result. + """ + monkeypatch.setattr('qutebrowser.utils.qtutils.qVersion', lambda: qversion) + assert qtutils.version_check(version, op) == expected + + class TestCheckOverflow: """Test check_overflow.""" @@ -68,20 +104,18 @@ class TestGetQtArgs: mocker.patch.object(parser, 'exit', side_effect=Exception) return parser - def test_no_qt_args(self, parser): + @pytest.mark.parametrize('args, expected', [ + # No Qt arguments + (['--debug'], [sys.argv[0]]), + # Qt flag + (['--debug', '--qt-reverse', '--nocolor'], [sys.argv[0], '-reverse']), + # Qt argument with value + (['--qt-stylesheet', 'foo'], [sys.argv[0], '-stylesheet', 'foo']), + ]) + def test_qt_args(self, args, expected, parser): """Test commandline with no Qt arguments given.""" - args = parser.parse_args(['--debug']) - assert qtutils.get_args(args) == [sys.argv[0]] - - def test_qt_flag(self, parser): - """Test commandline with a Qt flag.""" - args = parser.parse_args(['--debug', '--qt-reverse', '--nocolor']) - assert qtutils.get_args(args) == [sys.argv[0], '-reverse'] - - def test_qt_arg(self, parser): - """Test commandline with a Qt argument.""" - args = parser.parse_args(['--qt-stylesheet', 'foobar']) - assert qtutils.get_args(args) == [sys.argv[0], '-stylesheet', 'foobar'] + parsed = parser.parse_args(args) + assert qtutils.get_args(parsed) == expected def test_qt_both(self, parser): """Test commandline with a Qt argument and flag.""" @@ -91,3 +125,832 @@ class TestGetQtArgs: assert '-reverse' in qt_args assert '-stylesheet' in qt_args assert 'foobar' in qt_args + + +@pytest.mark.parametrize('os_name, qversion, expected', [ + ('linux', '5.2.1', True), # unaffected OS + ('linux', '5.4.1', True), # unaffected OS + ('nt', '5.2.1', False), + ('nt', '5.3.0', True), # unaffected Qt version + ('nt', '5.4.1', True), # unaffected Qt version +]) +def test_check_print_compat(os_name, qversion, expected, monkeypatch): + """Test check_print_compat. + + Args: + os_name: The fake os.name to set. + qversion: The fake qVersion() to set. + expected: The expected return value. + """ + monkeypatch.setattr('qutebrowser.utils.qtutils.os.name', os_name) + monkeypatch.setattr('qutebrowser.utils.qtutils.qVersion', lambda: qversion) + assert qtutils.check_print_compat() == expected + + +class QtObject: + + """Fake Qt object for test_ensure.""" + + def __init__(self, valid=True, null=False, error=None): + self._valid = valid + self._null = null + self._error = error + + def __repr__(self): + return '' + + def errorString(self): + """Get the fake error, or raise AttributeError if set to None.""" + if self._error is None: + raise AttributeError + else: + return self._error + + def isValid(self): + return self._valid + + def isNull(self): + return self._null + + +@pytest.mark.parametrize('func_name, obj, raising, exc_reason, exc_str', [ + # ensure_valid, good examples + ('ensure_valid', QtObject(valid=True, null=True), False, None, None), + ('ensure_valid', QtObject(valid=True, null=False), False, None, None), + # ensure_valid, bad examples + ('ensure_valid', QtObject(valid=False, null=True), True, None, + ' is not valid'), + ('ensure_valid', QtObject(valid=False, null=False), True, None, + ' is not valid'), + ('ensure_valid', QtObject(valid=False, null=True, error='Test'), True, + 'Test', ' is not valid: Test'), + # ensure_not_null, good examples + ('ensure_not_null', QtObject(valid=True, null=False), False, None, None), + ('ensure_not_null', QtObject(valid=False, null=False), False, None, None), + # ensure_not_null, bad examples + ('ensure_not_null', QtObject(valid=True, null=True), True, None, + ' is null'), + ('ensure_not_null', QtObject(valid=False, null=True), True, None, + ' is null'), + ('ensure_not_null', QtObject(valid=False, null=True, error='Test'), True, + 'Test', ' is null: Test'), +]) +def test_ensure(func_name, obj, raising, exc_reason, exc_str): + """Test ensure_valid and ensure_not_null. + + The function is parametrized as they do nearly the same. + + Args: + func_name: The name of the function to call. + obj: The object to test with. + raising: Whether QtValueError is expected to be raised. + exc_reason: The expected .reason attribute of the exception. + exc_str: The expected string of the exception. + """ + func = getattr(qtutils, func_name) + if raising: + with pytest.raises(qtutils.QtValueError) as excinfo: + func(obj) + assert excinfo.value.reason == exc_reason + assert str(excinfo.value) == exc_str + else: + func(obj) + + +@pytest.mark.parametrize('status, raising, message', [ + (QDataStream.Ok, False, None), + (QDataStream.ReadPastEnd, True, "The data stream has read past the end of " + "the data in the underlying device."), + (QDataStream.ReadCorruptData, True, "The data stream has read corrupt " + "data."), + (QDataStream.WriteFailed, True, "The data stream cannot write to the " + "underlying device."), +]) +def test_check_qdatastream(status, raising, message): + """Test check_qdatastream. + + Args: + status: The status to set on the QDataStream we test with. + raising: Whether check_qdatastream is expected to raise OSError. + message: The expected exception string. + """ + stream = QDataStream() + stream.setStatus(status) + if raising: + with pytest.raises(OSError) as excinfo: + qtutils.check_qdatastream(stream) + assert str(excinfo.value) == message + else: + qtutils.check_qdatastream(stream) + + +def test_qdatastream_status_count(): + """Make sure no new members are added to QDataStream.Status.""" + values = vars(QDataStream).values() + status_vals = [e for e in values if isinstance(e, QDataStream.Status)] + assert len(status_vals) == 4 + + +@pytest.mark.parametrize('obj', [ + QPoint(23, 42), + QUrl('http://www.qutebrowser.org/'), +]) +def test_serialize(obj): + """Test a serialize/deserialize round trip. + + Args: + obj: The object to test with. + """ + new_obj = type(obj)() + qtutils.deserialize(qtutils.serialize(obj), new_obj) + assert new_obj == obj + + +class TestSerializeStream: + + """Tests for serialize_stream and deserialize_stream.""" + + def _set_status(self, stream, status): + """Helper function so mocks can set an error status when used.""" + stream.status.return_value = status + + @pytest.fixture + def stream_mock(self): + """Fixture providing a QDataStream-like mock.""" + m = unittest.mock.MagicMock(spec=QDataStream) + m.status.return_value = QDataStream.Ok + return m + + def test_serialize_pre_error_mock(self, stream_mock): + """Test serialize_stream with an error already set.""" + stream_mock.status.return_value = QDataStream.ReadCorruptData + + with pytest.raises(OSError) as excinfo: + qtutils.serialize_stream(stream_mock, QPoint()) + + assert not stream_mock.__lshift__.called + assert str(excinfo.value) == "The data stream has read corrupt data." + + def test_serialize_post_error_mock(self, stream_mock): + """Test serialize_stream with an error while serializing.""" + obj = QPoint() + stream_mock.__lshift__.side_effect = lambda _other: self._set_status( + stream_mock, QDataStream.ReadCorruptData) + + with pytest.raises(OSError) as excinfo: + qtutils.serialize_stream(stream_mock, obj) + + assert stream_mock.__lshift__.called_once_with(obj) + assert str(excinfo.value) == "The data stream has read corrupt data." + + def test_deserialize_pre_error_mock(self, stream_mock): + """Test deserialize_stream with an error already set.""" + stream_mock.status.return_value = QDataStream.ReadCorruptData + + with pytest.raises(OSError) as excinfo: + qtutils.deserialize_stream(stream_mock, QPoint()) + + assert not stream_mock.__rshift__.called + assert str(excinfo.value) == "The data stream has read corrupt data." + + def test_deserialize_post_error_mock(self, stream_mock): + """Test deserialize_stream with an error while deserializing.""" + obj = QPoint() + stream_mock.__rshift__.side_effect = lambda _other: self._set_status( + stream_mock, QDataStream.ReadCorruptData) + + with pytest.raises(OSError) as excinfo: + qtutils.deserialize_stream(stream_mock, obj) + + assert stream_mock.__rshift__.called_once_with(obj) + assert str(excinfo.value) == "The data stream has read corrupt data." + + def test_round_trip_real_stream(self): + """Test a round trip with a real QDataStream.""" + src_obj = QPoint(23, 42) + dest_obj = QPoint() + data = QByteArray() + + write_stream = QDataStream(data, QIODevice.WriteOnly) + qtutils.serialize_stream(write_stream, src_obj) + + read_stream = QDataStream(data, QIODevice.ReadOnly) + qtutils.deserialize_stream(read_stream, dest_obj) + + assert src_obj == dest_obj + + @pytest.mark.qt_log_ignore('^QIODevice::write: ReadOnly device') + def test_serialize_readonly_stream(self): + """Test serialize_stream with a read-only stream.""" + data = QByteArray() + stream = QDataStream(data, QIODevice.ReadOnly) + with pytest.raises(OSError) as excinfo: + qtutils.serialize_stream(stream, QPoint()) + assert str(excinfo.value) == ("The data stream cannot write to the " + "underlying device.") + + @pytest.mark.qt_log_ignore('QIODevice::read: WriteOnly device') + def test_deserialize_writeonly_stream(self): + """Test deserialize_stream with a write-only stream.""" + data = QByteArray() + obj = QPoint() + stream = QDataStream(data, QIODevice.WriteOnly) + with pytest.raises(OSError) as excinfo: + qtutils.deserialize_stream(stream, obj) + assert str(excinfo.value) == ("The data stream has read past the end " + "of the data in the underlying device.") + + +class SavefileTestException(Exception): + + """Exception raised in TestSavefileOpen for testing.""" + + pass + + +class TestSavefileOpen: + + """Tests for savefile_open.""" + + ## Tests with a mock testing that the needed methods are called. + + @pytest.yield_fixture + def qsavefile_mock(self, mocker): + """Mock for QSaveFile.""" + m = mocker.patch('qutebrowser.utils.qtutils.QSaveFile') + instance = m() + yield instance + instance.commit.assert_called_once_with() + + def test_mock_open_error(self, qsavefile_mock): + """Test with a mock and a failing open().""" + qsavefile_mock.open.return_value = False + qsavefile_mock.errorString.return_value = "Hello World" + + with pytest.raises(OSError) as excinfo: + with qtutils.savefile_open('filename'): + pass + + qsavefile_mock.open.assert_called_once_with(QIODevice.WriteOnly) + qsavefile_mock.cancelWriting.assert_called_once_with() + assert str(excinfo.value) == "Hello World" + + def test_mock_exception(self, qsavefile_mock): + """Test with a mock and an exception in the block.""" + qsavefile_mock.open.return_value = True + + with pytest.raises(SavefileTestException): + with qtutils.savefile_open('filename'): + raise SavefileTestException + + qsavefile_mock.open.assert_called_once_with(QIODevice.WriteOnly) + qsavefile_mock.cancelWriting.assert_called_once_with() + + def test_mock_commit_failed(self, qsavefile_mock): + """Test with a mock and an exception in the block.""" + qsavefile_mock.open.return_value = True + qsavefile_mock.commit.return_value = False + + with pytest.raises(OSError) as excinfo: + with qtutils.savefile_open('filename'): + pass + + qsavefile_mock.open.assert_called_once_with(QIODevice.WriteOnly) + assert not qsavefile_mock.cancelWriting.called + assert not qsavefile_mock.errorString.called + assert str(excinfo.value) == "Commit failed!" + + def test_mock_successful(self, qsavefile_mock): + """Test with a mock and a successful write.""" + qsavefile_mock.open.return_value = True + qsavefile_mock.errorString.return_value = "Hello World" + qsavefile_mock.commit.return_value = True + qsavefile_mock.write.side_effect = len + qsavefile_mock.isOpen.return_value = True + + with qtutils.savefile_open('filename') as f: + f.write("Hello World") + + qsavefile_mock.open.assert_called_once_with(QIODevice.WriteOnly) + assert not qsavefile_mock.cancelWriting.called + qsavefile_mock.write.assert_called_once_with(b"Hello World") + + ## Tests with real files + + @pytest.mark.parametrize('data', ["Hello World", "Snowman! ☃"]) + def test_utf8(self, data, tmpdir): + """Test with UTF8 data.""" + filename = tmpdir / 'foo' + filename.write("Old data") + with qtutils.savefile_open(str(filename)) as f: + f.write(data) + assert tmpdir.listdir() == [filename] + assert filename.read_text(encoding='utf-8') == data + + def test_binary(self, tmpdir): + """Test with binary data.""" + filename = tmpdir / 'foo' + with qtutils.savefile_open(str(filename), binary=True) as f: + f.write(b'\xde\xad\xbe\xef') + assert tmpdir.listdir() == [filename] + assert filename.read_binary() == b'\xde\xad\xbe\xef' + + def test_exception(self, tmpdir): + """Test with an exception in the block.""" + filename = tmpdir / 'foo' + filename.write("Old content") + with pytest.raises(SavefileTestException): + with qtutils.savefile_open(str(filename)) as f: + f.write("Hello World!") + raise SavefileTestException + assert tmpdir.listdir() == [filename] + assert filename.read_text(encoding='utf-8') == "Old content" + + def test_existing_dir(self, tmpdir): + """Test with the filename already occupied by a directory.""" + filename = tmpdir / 'foo' + filename.mkdir() + with pytest.raises(OSError) as excinfo: + with qtutils.savefile_open(str(filename)): + pass + assert str(excinfo.value) == "Filename refers to a directory" + assert tmpdir.listdir() == [filename] + + def test_failing_commit(self, tmpdir): + """Test with the file being closed before comitting.""" + filename = tmpdir / 'foo' + with pytest.raises(OSError) as excinfo: + with qtutils.savefile_open(str(filename), binary=True) as f: + f.write(b'Hello') + f.dev.commit() # provoke failing "real" commit + + assert str(excinfo.value) == "Commit failed!" + assert tmpdir.listdir() == [filename] + + def test_line_endings(self, tmpdir): + """Make sure line endings are translated correctly. + + See https://github.com/The-Compiler/qutebrowser/issues/309 + """ + filename = tmpdir / 'foo' + with qtutils.savefile_open(str(filename)) as f: + f.write('foo\nbar\nbaz') + data = filename.read_binary() + if os.name == 'nt': + assert data == b'foo\r\nbar\r\nbaz' + else: + assert data == b'foo\nbar\nbaz' + + +@pytest.mark.parametrize('orgname, expected', [(None, ''), ('test', 'test')]) +def test_unset_organization(orgname, expected): + """Test unset_organization. + + Args: + orgname: The organizationName to set initially. + expected: The organizationName which is expected when reading back. + """ + app = QApplication.instance() + app.setOrganizationName(orgname) + assert app.organizationName() == expected # sanity check + with qtutils.unset_organization(): + assert app.organizationName() == '' + assert app.organizationName() == expected + + +if test_file is not None: + # If we were able to import Python's test_file module, we run some code + # here which defines unittest TestCases to run the python tests over + # PyQIODevice. + + @pytest.yield_fixture(scope='session', autouse=True) + def clean_up_python_testfile(): + """Clean up the python testfile after tests if tests didn't.""" + yield + try: + os.remove(test_file.TESTFN) + except FileNotFoundError: + pass + + class PyIODeviceTestMixin: + + """Some helper code to run Python's tests with PyQIODevice. + + Attributes: + _data: A QByteArray containing the data in memory. + f: The opened PyQIODevice. + """ + + def setUp(self): + """Set up self.f using a PyQIODevice instead of a real file.""" + self._data = QByteArray() + self.f = self.open(test_file.TESTFN, 'wb') + + def open(self, _fname, mode): + """Open an in-memory PyQIODevice instead of a real file.""" + modes = { + 'wb': QIODevice.WriteOnly | QIODevice.Truncate, + 'w': QIODevice.WriteOnly | QIODevice.Text | QIODevice.Truncate, + 'rb': QIODevice.ReadOnly, + 'r': QIODevice.ReadOnly | QIODevice.Text, + } + try: + qt_mode = modes[mode] + except KeyError: + raise ValueError("Invalid mode {}!".format(mode)) + f = QBuffer(self._data) + f.open(qt_mode) + qiodev = qtutils.PyQIODevice(f) + # Make sure tests using name/mode don't blow up. + qiodev.name = test_file.TESTFN + qiodev.mode = mode + # Create empty TESTFN file because the Python tests try to unlink + # it.after the test. + open(test_file.TESTFN, 'w', encoding='utf-8').close() + return qiodev + + class PyAutoFileTests(PyIODeviceTestMixin, test_file.AutoFileTests, + unittest.TestCase): + + """Unittest testcase to run Python's AutoFileTests.""" + + def testReadinto_text(self): + """Skip this test as BufferedIOBase seems to fail it.""" + pass + + class PyOtherFileTests(PyIODeviceTestMixin, test_file.OtherFileTests, + unittest.TestCase): + + """Unittest testcase to run Python's OtherFileTests.""" + + def testSetBufferSize(self): + """Skip this test as setting buffer size is unsupported.""" + pass + + def testTruncateOnWindows(self): + """Skip this test truncating is unsupported.""" + pass + + +class FailingQIODevice(QIODevice): + + """A fake QIODevice where reads/writes fail.""" + + def isOpen(self): + return True + + def isReadable(self): + return True + + def isWritable(self): + return True + + def write(self, _data): + """Simulate failed write.""" + self.setErrorString("Writing failed") + return -1 + + def read(self, _maxsize): + """Simulate failed read.""" + self.setErrorString("Reading failed") + return None + + def readAll(self): + return self.read(0) + + def readLine(self, maxsize): + return self.read(maxsize) + + +class TestPyQIODevice: + + """Tests for PyQIODevice.""" + + @pytest.yield_fixture + def pyqiodev(self): + """Fixture providing a PyQIODevice with a QByteArray to test.""" + data = QByteArray() + f = QBuffer(data) + qiodev = qtutils.PyQIODevice(f) + yield qiodev + qiodev.close() + + @pytest.fixture + def pyqiodev_failing(self): + """Fixture providing a PyQIODevice with a FailingQIODevice to test.""" + failing = FailingQIODevice() + return qtutils.PyQIODevice(failing) + + @pytest.mark.parametrize('method, args', [ + ('seek', [0]), + ('flush', []), + ('isatty', []), + ('readline', []), + ('tell', []), + ('write', [b'']), + ('read', []), + ]) + def test_closed_device(self, pyqiodev, method, args): + """Test various methods with a closed device. + + Args: + method: The name of the method to call. + args: The arguments to pass. + """ + func = getattr(pyqiodev, method) + with pytest.raises(ValueError) as excinfo: + func(*args) + assert str(excinfo.value) == "IO operation on closed device!" + + @pytest.mark.parametrize('method', ['readline', 'read']) + def test_unreadable(self, pyqiodev, method): + """Test methods with an unreadable device. + + Args: + method: The name of the method to call. + """ + pyqiodev.open(QIODevice.WriteOnly) + func = getattr(pyqiodev, method) + with pytest.raises(OSError) as excinfo: + func() + assert str(excinfo.value) == "Trying to read unreadable file!" + + def test_unwritable(self, pyqiodev): + """Test writing with a read-only device.""" + pyqiodev.open(QIODevice.ReadOnly) + with pytest.raises(OSError) as excinfo: + pyqiodev.write(b'') + assert str(excinfo.value) == "Trying to write to unwritable file!" + + @pytest.mark.parametrize('data', [b'12345', b'']) + def test_len(self, pyqiodev, data): + """Test len()/__len__. + + Args: + data: The data to write before checking if the length equals + len(data). + """ + pyqiodev.open(QIODevice.WriteOnly) + pyqiodev.write(data) + assert len(pyqiodev) == len(data) + + def test_failing_open(self, tmpdir): + """Test open() which fails (because it's an existant directory).""" + qf = QFile(str(tmpdir)) + dev = qtutils.PyQIODevice(qf) + with pytest.raises(OSError) as excinfo: + dev.open(QIODevice.WriteOnly) + assert str(excinfo.value) == 'Is a directory' + assert dev.closed + + def test_fileno(self, pyqiodev): + with pytest.raises(io.UnsupportedOperation): + pyqiodev.fileno() + + @pytest.mark.qt_log_ignore('^QBuffer::seek: Invalid pos:') + @pytest.mark.parametrize('offset, whence, pos, data, raising', [ + (0, io.SEEK_SET, 0, b'1234567890', False), + (42, io.SEEK_SET, 0, b'1234567890', True), + (8, io.SEEK_CUR, 8, b'90', False), + (-5, io.SEEK_CUR, 0, b'1234567890', True), + (-2, io.SEEK_END, 8, b'90', False), + (2, io.SEEK_END, 0, b'1234567890', True), + (0, io.SEEK_END, 10, b'', False), + ]) + def test_seek_tell(self, pyqiodev, offset, whence, pos, data, raising): + """Test seek() and tell(). + + The initial position when these tests run is 0. + + Args: + offset: The offset to pass to .seek(). + whence: The whence argument to pass to .seek(). + pos: The expected position after seeking. + data: The expected data to read after seeking. + raising: Whether seeking should raise OSError. + """ + with pyqiodev.open(QIODevice.WriteOnly) as f: + f.write(b'1234567890') + pyqiodev.open(QIODevice.ReadOnly) + if raising: + with pytest.raises(OSError) as excinfo: + pyqiodev.seek(offset, whence) + assert str(excinfo.value) == "seek failed!" + else: + pyqiodev.seek(offset, whence) + assert pyqiodev.tell() == pos + assert pyqiodev.read() == data + + def test_seek_unsupported(self, pyqiodev): + """Test seeking with unsupported whence arguments.""" + if hasattr(os, 'SEEK_HOLE'): + whence = os.SEEK_HOLE + elif hasattr(os, 'SEEK_DATA'): + whence = os.SEEK_DATA + else: + pytest.skip("Needs os.SEEK_HOLE or os.SEEK_DATA available.") + pyqiodev.open(QIODevice.ReadOnly) + with pytest.raises(io.UnsupportedOperation): + pyqiodev.seek(0, whence) + + def test_qprocess(self): + """Test PyQIODevice with a QProcess which is non-sequential. + + This also verifies seek() and tell() behave as expected. + """ + proc = QProcess() + proc.start(sys.executable, ['-c', 'print("Hello World")']) + dev = qtutils.PyQIODevice(proc) + assert not dev.closed + with pytest.raises(OSError) as excinfo: + dev.seek(0) + assert str(excinfo.value) == 'Random access not allowed!' + with pytest.raises(OSError) as excinfo: + dev.tell() + assert str(excinfo.value) == 'Random access not allowed!' + proc.waitForFinished(1000) + proc.kill() + assert dev.read() == b'Hello World\n' + + def test_truncate(self, pyqiodev): + with pytest.raises(io.UnsupportedOperation): + pyqiodev.truncate() + + def test_closed(self, pyqiodev): + """Test the closed attribute.""" + assert pyqiodev.closed + pyqiodev.open(QIODevice.ReadOnly) + assert not pyqiodev.closed + pyqiodev.close() + assert pyqiodev.closed + + def test_contextmanager(self, pyqiodev): + """Make sure using the PyQIODevice as context manager works.""" + assert pyqiodev.closed + with pyqiodev.open(QIODevice.ReadOnly) as f: + assert not f.closed + assert f is pyqiodev + assert pyqiodev.closed + + def test_flush(self, pyqiodev): + """Make sure flushing doesn't raise an exception.""" + pyqiodev.open(QIODevice.WriteOnly) + pyqiodev.write(b'test') + pyqiodev.flush() + + @pytest.mark.parametrize('method, ret', [ + ('isatty', False), + ('seekable', True), + ]) + def test_bools(self, method, ret, pyqiodev): + """Make sure simple bool arguments return the right thing. + + Args: + method: The name of the method to call. + ret: The return value we expect. + """ + pyqiodev.open(QIODevice.WriteOnly) + func = getattr(pyqiodev, method) + assert func() == ret + + @pytest.mark.parametrize('mode, readable, writable', [ + (QIODevice.ReadOnly, True, False), + (QIODevice.ReadWrite, True, True), + (QIODevice.WriteOnly, False, True), + ]) + def test_readable_writable(self, mode, readable, writable, pyqiodev): + """Test readable() and writable(). + + Args: + mode: The mode to open the PyQIODevice in. + readable: Whether the device should be readable. + writable: Whether the device should be writable. + """ + assert not pyqiodev.readable() + assert not pyqiodev.writable() + pyqiodev.open(mode) + assert pyqiodev.readable() == readable + assert pyqiodev.writable() == writable + + @pytest.mark.parametrize('size, chunks', [ + (-1, [b'one\n', b'two\n', b'three', b'']), + (0, [b'', b'', b'', b'']), + (2, [b'on', b'e\n', b'tw', b'o\n', b'th', b're', b'e']), + (10, [b'one\n', b'two\n', b'three', b'']), + ]) + def test_readline(self, size, chunks, pyqiodev): + """Test readline() with different sizes. + + Args: + size: The size to pass to readline() + chunks: A list of expected chunks to read. + """ + with pyqiodev.open(QIODevice.WriteOnly) as f: + f.write(b'one\ntwo\nthree') + pyqiodev.open(QIODevice.ReadOnly) + for i, chunk in enumerate(chunks, start=1): + print("Expecting chunk {}: {!r}".format(i, chunk)) + assert pyqiodev.readline(size) == chunk + + def test_write(self, pyqiodev): + """Make sure writing and re-reading works.""" + with pyqiodev.open(QIODevice.WriteOnly) as f: + f.write(b'foo\n') + f.write(b'bar\n') + pyqiodev.open(QIODevice.ReadOnly) + assert pyqiodev.read() == b'foo\nbar\n' + + def test_write_error(self, pyqiodev_failing): + """Test writing with FailingQIODevice.""" + with pytest.raises(OSError) as excinfo: + pyqiodev_failing.write(b'x') + assert str(excinfo.value) == 'Writing failed' + + @pytest.mark.skipif(os.name != 'posix', reason="Needs a POSIX OS.") + @pytest.mark.skipif(not os.path.exists('/dev/full'), + reason="Needs /dev/full.") + def test_write_error_real(self): + """Test a real write error with /dev/full on supported systems.""" + qf = QFile('/dev/full') + qf.open(QIODevice.WriteOnly | QIODevice.Unbuffered) + dev = qtutils.PyQIODevice(qf) + with pytest.raises(OSError) as excinfo: + dev.write(b'foo') + qf.close() + assert str(excinfo.value) == 'No space left on device' + + @pytest.mark.parametrize('size, chunks', [ + (-1, [b'1234567890']), + (0, [b'']), + (3, [b'123', b'456', b'789', b'0']), + (20, [b'1234567890']) + ]) + def test_read(self, size, chunks, pyqiodev): + """Test reading with different sizes. + + Args: + size: The size to pass to read() + chunks: A list of expected data chunks. + """ + with pyqiodev.open(QIODevice.WriteOnly) as f: + f.write(b'1234567890') + pyqiodev.open(QIODevice.ReadOnly) + for i, chunk in enumerate(chunks): + print("Expecting chunk {}: {!r}".format(i, chunk)) + assert pyqiodev.read(size) == chunk + + @pytest.mark.parametrize('method, args', [ + ('read', []), + ('read', [5]), + ('readline', []), + ('readline', [5]), + ]) + def test_failing_reads(self, method, args, pyqiodev_failing): + """Test reading with a FailingQIODevice. + + Args: + method: The name of the method to call. + args: A list of arguments to pass. + """ + func = getattr(pyqiodev_failing, method) + with pytest.raises(OSError) as excinfo: + func(*args) + assert str(excinfo.value) == 'Reading failed' + + +class TestEventLoop: + + """Tests for EventLoop. + + Attributes: + loop: The EventLoop we're testing. + """ + + # pylint: disable=protected-access + + def _assert_executing(self): + """Slot which gets called from timers to be sure the loop runs.""" + assert self.loop._executing + + def _double_exec(self): + """Slot which gets called from timers to assert double-exec fails.""" + with pytest.raises(AssertionError): + self.loop.exec_() + + def test_normal_exec(self): + """Test exec_ without double-executing.""" + self.loop = qtutils.EventLoop() + QTimer.singleShot(100, self._assert_executing) + QTimer.singleShot(200, self.loop.quit) + self.loop.exec_() + assert not self.loop._executing + + def test_double_exec(self): + """Test double-executing.""" + self.loop = qtutils.EventLoop() + QTimer.singleShot(100, self._assert_executing) + QTimer.singleShot(200, self._double_exec) + QTimer.singleShot(300, self._assert_executing) + QTimer.singleShot(400, self.loop.quit) + self.loop.exec_() + assert not self.loop._executing From ebc013ac2af4afb9aef56ee30249ff638b15e6c1 Mon Sep 17 00:00:00 2001 From: Austin Anderson Date: Thu, 4 Jun 2015 22:50:04 -0400 Subject: [PATCH 021/146] Removed redundant setter. --- qutebrowser/mainwindow/statusbar/bar.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 9b1219b38..abcc5f5f6 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -326,16 +326,6 @@ class StatusBar(QWidget): """Getter for self.command_active, so it can be used as Qt property.""" return self._command_active - def _set_command_active(self, val): - """Setter for self._command_active. - - Re-set the stylesheet after setting the value, so everything gets - updated by Qt properly. - """ - log.statusbar.debug("Setting command_active to {}".format(val)) - self._command_active = val - self.setStyleSheet(style.get_stylesheet(self.STYLESHEET)) - @pyqtProperty(bool) def insert_active(self): """Getter for self.insert_active, so it can be used as Qt property.""" From 34d4c0837470c8abe5ac92ede344812cc5fa3443 Mon Sep 17 00:00:00 2001 From: Austin Anderson Date: Thu, 4 Jun 2015 22:30:40 -0400 Subject: [PATCH 022/146] Significantly reduced the size of the bar stylesheet. --- qutebrowser/mainwindow/statusbar/bar.py | 80 +++++++------------------ 1 file changed, 23 insertions(+), 57 deletions(-) diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index abcc5f5f6..05079413b 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -106,90 +106,56 @@ class StatusBar(QWidget): _caret_mode = CaretMode.off STYLESHEET = """ - QWidget#StatusBar { + + QWidget#StatusBar, + QWidget#StatusBar * { + {{ font['statusbar'] }} {{ color['statusbar.bg'] }} - } - - QWidget#StatusBar QLabel { {{ color['statusbar.fg'] }} } - QWidget#StatusBar QLineEdit { - {{ color['statusbar.fg'] }} - } - - QWidget#StatusBar[caret_mode="on"] QLabel { + QWidget#StatusBar[caret_mode="on"], + QWidget#StatusBar[caret_mode="on"] * { {{ color['statusbar.fg.caret'] }} - } - - QWidget#StatusBar[caret_mode="on"] { {{ color['statusbar.bg.caret'] }} } - QWidget#StatusBar[caret_mode="selection"] QLabel { + QWidget#StatusBar[caret_mode="selection"], + QWidget#StatusBar[caret_mode="selection"] * { {{ color['statusbar.fg.caret-selection'] }} - } - - QWidget#StatusBar[caret_mode="selection"] { {{ color['statusbar.bg.caret-selection'] }} } - QWidget#StatusBar[prompt_active="true"] { - {{ color['statusbar.bg.prompt'] }} - } - - QWidget#StatusBar[severity="error"] { + QWidget#StatusBar[severity="error"], + QWidget#StatusBar[severity="error"] * { + {{ color['statusbar.fg.error'] }} {{ color['statusbar.bg.error'] }} } - QWidget#StatusBar[severity="error"] QLabel { - {{ color['statusbar.fg.error'] }} - } - - QWidget#StatusBar[severity="warning"] { + QWidget#StatusBar[severity="warning"], + QWidget#StatusBar[severity="warning"] * { + {{ color['statusbar.fg.warning'] }} {{ color['statusbar.bg.warning'] }} } - QWidget#StatusBar[severity="warning"] QLabel { - {{ color['statusbar.fg.warning'] }} - } - - QWidget#StatusBar[prompt_active="true"] { + QWidget#StatusBar[prompt_active="true"], + QWidget#StatusBar[prompt_active="true"] * { + {{ color['statusbar.fg.prompt'] }} {{ color['statusbar.bg.prompt'] }} } - QWidget#StatusBar[prompt_active="true"] QLabel { - {{ color['statusbar.fg.prompt'] }} - } - - QWidget#StatusBar[prompt_active="true"] QLineEdit { - {{ color['statusbar.fg.prompt'] }} - } - - QWidget#StatusBar[insert_active="true"] { + QWidget#StatusBar[insert_active="true"], + QWidget#StatusBar[insert_active="true"] * { + {{ color['statusbar.fg.insert'] }} {{ color['statusbar.bg.insert'] }} } - QWidget#StatusBar[insert_active="true"] QLabel { - {{ color['statusbar.fg.insert'] }} - } - - QWidget#StatusBar[command_active="true"] { + QWidget#StatusBar[command_active="true"], + QWidget#StatusBar[command_active="true"] * { + {{ color['statusbar.fg.command'] }} {{ color['statusbar.bg.command'] }} } - QWidget#StatusBar[command_active="true"] QLabel { - {{ color['statusbar.fg.command'] }} - } - - QWidget#StatusBar[command_active="true"] QLineEdit { - {{ color['statusbar.fg.command'] }} - } - - QLabel, QLineEdit { - {{ font['statusbar'] }} - } - """ def __init__(self, win_id, parent=None): From 261c44bea9247ae3d86f2711e44bfd470a30fd3e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 8 Jun 2015 07:42:17 +0200 Subject: [PATCH 023/146] Fix TestPyQIODevice.test_qprocess on Windows. --- tests/utils/test_qtutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/test_qtutils.py b/tests/utils/test_qtutils.py index 220297761..e74c61ed5 100644 --- a/tests/utils/test_qtutils.py +++ b/tests/utils/test_qtutils.py @@ -770,7 +770,7 @@ class TestPyQIODevice: assert str(excinfo.value) == 'Random access not allowed!' proc.waitForFinished(1000) proc.kill() - assert dev.read() == b'Hello World\n' + assert dev.read().rstrip() == b'Hello World' def test_truncate(self, pyqiodev): with pytest.raises(io.UnsupportedOperation): From 00747be9d3790534e8b32464605d1b5b6c2d6627 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 8 Jun 2015 07:43:40 +0200 Subject: [PATCH 024/146] Fix TestSavefileOpen.test_existing_dir on older Qt. --- tests/utils/test_qtutils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/utils/test_qtutils.py b/tests/utils/test_qtutils.py index e74c61ed5..8debd0380 100644 --- a/tests/utils/test_qtutils.py +++ b/tests/utils/test_qtutils.py @@ -473,7 +473,9 @@ class TestSavefileOpen: with pytest.raises(OSError) as excinfo: with qtutils.savefile_open(str(filename)): pass - assert str(excinfo.value) == "Filename refers to a directory" + errors = ["Filename refers to a directory", # Qt >= 5.4 + "Commit failed!"] # older Qt versions + assert str(excinfo.value) in errors assert tmpdir.listdir() == [filename] def test_failing_commit(self, tmpdir): From 1fcce1287067053ee504ebd5e739649aa695349c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 8 Jun 2015 07:45:19 +0200 Subject: [PATCH 025/146] Fix TestPyQIODevice.failing_open on Windows. --- tests/utils/test_qtutils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/utils/test_qtutils.py b/tests/utils/test_qtutils.py index 8debd0380..9034e8e4e 100644 --- a/tests/utils/test_qtutils.py +++ b/tests/utils/test_qtutils.py @@ -702,7 +702,9 @@ class TestPyQIODevice: dev = qtutils.PyQIODevice(qf) with pytest.raises(OSError) as excinfo: dev.open(QIODevice.WriteOnly) - assert str(excinfo.value) == 'Is a directory' + errors = ['Access is denied.', # Linux/OS X + 'Is a directory'] # Windows + assert str(excinfo.value) in errors assert dev.closed def test_fileno(self, pyqiodev): From c08078841ff41a74a495922fabf0395e76c28304 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 8 Jun 2015 07:49:22 +0200 Subject: [PATCH 026/146] Fix test_qprocess. --- tests/utils/test_qtutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/test_qtutils.py b/tests/utils/test_qtutils.py index 9034e8e4e..ad4fb0c4a 100644 --- a/tests/utils/test_qtutils.py +++ b/tests/utils/test_qtutils.py @@ -774,7 +774,7 @@ class TestPyQIODevice: assert str(excinfo.value) == 'Random access not allowed!' proc.waitForFinished(1000) proc.kill() - assert dev.read().rstrip() == b'Hello World' + assert bytes(dev.read()).rstrip() == b'Hello World' def test_truncate(self, pyqiodev): with pytest.raises(io.UnsupportedOperation): From 80a59720dec610f79999568533b26b67c7e5bf21 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 Jun 2015 17:20:21 +0200 Subject: [PATCH 027/146] Add .travis.yml. --- .travis.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..61c77bcbf --- /dev/null +++ b/.travis.yml @@ -0,0 +1,19 @@ +dist: trusty + +# Not really, but this is here so we can do stuff by hand. +language: c + +install: + - sudo apt-get -y -q update + - sudo apt-get -y -q install python3-pyqt5 python3-pyqt5.qtwebkit python-tox python3-dev xvfb + +script: + - xvfb-run -s "-screen 0 640x480x16" tox -e unittests,smoke + - tox -e misc + - tox -e pep257 + - tox -e pyflakes + - tox -e pep8 + - tox -e mccabe + - tox -e pylint + - tox -e pyroma + - tox -e check-manifest From 4532176e7b8e031961e5de5094ce82955faa220a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 Jun 2015 18:28:57 +0200 Subject: [PATCH 028/146] Don't use substitutions in tox.ini. These seem to break things on Ubuntu Trusty... --- tox.ini | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/tox.ini b/tox.ini index 870ee72fd..c588b8d5d 100644 --- a/tox.ini +++ b/tox.ini @@ -26,8 +26,6 @@ deps = pytest-qt==1.4.0 pytest-mock==0.6.0 pytest-html==1.3.1 -# We don't use {[testenv:mkvenv]commands} here because that seems to be broken -# on Ubuntu Trusty. commands = {envpython} scripts/link_pyqt.py --tox {envdir} {envpython} -m py.test --strict -rfEsw {posargs} @@ -40,7 +38,7 @@ deps = pytest-cov==1.8.1 cov-core==1.15.0 commands = - {[testenv:mkvenv]commands} + {envpython} scripts/link_pyqt.py --tox {envdir} {envpython} -m py.test --strict -rfEswx -v --cov qutebrowser --cov-report term --cov-report html {posargs} [testenv:misc] @@ -60,7 +58,7 @@ deps = logilab-common==0.63.2 six==1.9.0 commands = - {[testenv:mkvenv]commands} + {envpython} scripts/link_pyqt.py --tox {envdir} {envdir}/bin/pylint scripts qutebrowser --rcfile=.pylintrc --output-format=colorized --reports=no --expected-line-ending-format=LF {envpython} scripts/run_pylint_on_tests.py --rcfile=.pylintrc --output-format=colorized --reports=no --expected-line-ending-format=LF @@ -84,7 +82,7 @@ deps = pyflakes==0.9.0 pytest-flakes==1.0.0 commands = - {[testenv:mkvenv]commands} + {envpython} scripts/link_pyqt.py --tox {envdir} {envpython} -m py.test -q --flakes -m flakes [testenv:pep8] @@ -95,7 +93,7 @@ deps = pep8==1.6.2 pytest-pep8==1.0.6 commands = - {[testenv:mkvenv]commands} + {envpython} scripts/link_pyqt.py --tox {envdir} {envpython} -m py.test -q --pep8 -m pep8 [testenv:mccabe] @@ -106,7 +104,7 @@ deps = mccabe==0.3 pytest-mccabe==0.1 commands = - {[testenv:mkvenv]commands} + {envpython} scripts/link_pyqt.py --tox {envdir} {envpython} -m py.test -q --mccabe -m mccabe [testenv:pyroma] @@ -115,7 +113,7 @@ deps = pyroma==1.8.1 docutils==0.12 commands = - {[testenv:mkvenv]commands} + {envpython} scripts/link_pyqt.py --tox {envdir} {envdir}/bin/pyroma . [testenv:check-manifest] @@ -123,7 +121,7 @@ skip_install = true deps = check-manifest==0.25 commands = - {[testenv:mkvenv]commands} + {envpython} scripts/link_pyqt.py --tox {envdir} {envdir}/bin/check-manifest --ignore 'qutebrowser/git-commit-id,qutebrowser/html/doc,qutebrowser/html/doc/*,*/__pycache__' [testenv:docs] @@ -132,7 +130,7 @@ whitelist_externals = git deps = -r{toxinidir}/requirements.txt commands = - {[testenv:mkvenv]commands} + {envpython} scripts/link_pyqt.py --tox {envdir} {envpython} scripts/src2asciidoc.py git --no-pager diff --exit-code --stat {envpython} scripts/asciidoc2html.py {posargs} @@ -143,8 +141,6 @@ setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/pl passenv = DISPLAY XAUTHORITY HOME USERNAME USER deps = -r{toxinidir}/requirements.txt -# We don't use {[testenv:mkvenv]commands} here because that seems to be broken -# on Ubuntu Trusty. commands = {envpython} scripts/link_pyqt.py --tox {envdir} {envpython} -m qutebrowser --no-err-windows --nowindow --temp-basedir about:blank ":later 500 quit" From 645a1512dd8e1c1211fccb5cd4b2cd6e9af8b236 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Jun 2015 06:10:12 +0200 Subject: [PATCH 029/146] tox: Update pyflakes to 0.9.1. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index c588b8d5d..b51497562 100644 --- a/tox.ini +++ b/tox.ini @@ -79,7 +79,7 @@ deps = -r{toxinidir}/requirements.txt py==1.4.28 pytest==2.7.1 - pyflakes==0.9.0 + pyflakes==0.9.1 pytest-flakes==1.0.0 commands = {envpython} scripts/link_pyqt.py --tox {envdir} From 3be9a9b0512c274ec012be1ab174db857a986c4a Mon Sep 17 00:00:00 2001 From: Austin Anderson Date: Wed, 10 Jun 2015 08:16:15 -0400 Subject: [PATCH 030/146] Catalogued a configuration option change for updates. --- qutebrowser/config/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index e195310ef..02b32c583 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -298,6 +298,7 @@ class ConfigManager(QObject): ('colors', 'tab.indicator.system'): 'tabs.indicator.system', ('tabs', 'auto-hide'): 'hide-auto', ('completion', 'history-length'): 'cmd-history-max-items', + ('colors', 'downloads.fg'): 'downloads.fg.start', } DELETED_OPTIONS = [ ('colors', 'tab.separator'), From fc15e85811d3388a74ed4d1b071c88158a103d36 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Jun 2015 18:35:19 +0200 Subject: [PATCH 031/146] Add AppVeyor support. --- .appveyor.yml | 16 ++++++++++++++ scripts/appveyor_install.py | 42 +++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 .appveyor.yml create mode 100644 scripts/appveyor_install.py diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 000000000..45d25ad2b --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,16 @@ +shallow_clone: true +version: '{branch}-{build}' +cache: C:\Users\appveyor\pip\wheels +build: off +environment: + PYTHON: 'C:\Python34' + PYTHONUNBUFFERED: 1 + +install: + - C:\Python34\python -u scripts\appveyor_install.py + +test_script: + - C:\Python34\Scripts\tox -e smoke + - C:\Python34\Scripts\tox -e unittests + - C:\Python34\Scripts\tox -e pyflakes + - C:\Python34\Scripts\tox -e pylint diff --git a/scripts/appveyor_install.py b/scripts/appveyor_install.py new file mode 100644 index 000000000..9758713be --- /dev/null +++ b/scripts/appveyor_install.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2015 Florian Bruhin (The Compiler) + +# 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 . + +"""Install needed prerequisites on the AppVeyor CI.""" + +import subprocess +import urllib.request + +PYQT_VERSION = '5.4.1' + +print("Getting PyQt5...") +urllib.request.urlretrieve( + ('http://sourceforge.net/projects/pyqt/files/PyQt5/PyQt-{v}/' + 'PyQt5-{v}-gpl-Py3.4-Qt{v}-x32.exe'.format(v=PYQT_VERSION)), + r'C:\install-PyQt5.exe') + +print("Installing PyQt5...") +subprocess.check_call([r'C:\install-PyQt5.exe', '/S']) + +print("Installing tox...") +subprocess.check_call([r'C:\Python34\Scripts\pip', 'install', 'tox']) + +print("Linking Python...") +with open(r'C:\Windows\system32\python3.bat', 'w', encoding='ascii') as f: + f.write(r'@C:\Python34\python %*') From b57027f8005fb3fe4578e93579e162732e467b96 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Jun 2015 18:29:33 +0200 Subject: [PATCH 032/146] Fix pylint warnings on Windows. --- qutebrowser/misc/crashsignal.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py index 2e336bc1e..2d3399a33 100644 --- a/qutebrowser/misc/crashsignal.py +++ b/qutebrowser/misc/crashsignal.py @@ -300,6 +300,7 @@ class SignalHandler(QObject): signal.SIGTERM, self.interrupt) if os.name == 'posix' and hasattr(signal, 'set_wakeup_fd'): + # pylint: disable=import-error,no-member import fcntl read_fd, write_fd = os.pipe() for fd in (read_fd, write_fd): From 67e895b6c79bd07042c830ea0a401ae0352e89aa Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Jun 2015 18:30:42 +0200 Subject: [PATCH 033/146] Hide SetProcessDpiAwareness Qt warning. This shows up on AppVeyor CI for some reason. See https://bugreports.qt.io/browse/QTBUG-38993 --- qutebrowser/utils/log.py | 2 ++ tox.ini | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 1f1071673..269e1e38d 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -285,6 +285,8 @@ def qt_message_handler(msg_type, context, msg): 'QXcbWindow: Unhandled client message: "_E_', 'QXcbWindow: Unhandled client message: "_ECORE_', 'QXcbWindow: Unhandled client message: "_GTK_', + # Happens on AppVeyor CI + 'SetProcessDpiAwareness failed:', ) if any(msg.strip().startswith(pattern) for pattern in suppressed_msgs): level = logging.DEBUG diff --git a/tox.ini b/tox.ini index b51497562..603a771a6 100644 --- a/tox.ini +++ b/tox.ini @@ -162,4 +162,6 @@ pep8ignore = resources.py ALL mccabe-complexity = 12 qt_log_level_fail = WARNING -qt_log_ignore = ^SpellCheck: .* +qt_log_ignore = + ^SpellCheck: .* + ^SetProcessDpiAwareness failed: .* From ddd343c89c2ddf6e478d8d4097f48caa018ae2f7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Jun 2015 18:32:35 +0200 Subject: [PATCH 034/146] link_pyqt: Be less verbose. --- scripts/link_pyqt.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/link_pyqt.py b/scripts/link_pyqt.py index dbfeaed99..b35fed126 100644 --- a/scripts/link_pyqt.py +++ b/scripts/link_pyqt.py @@ -40,7 +40,8 @@ class Error(Exception): def verbose_copy(src, dst, *, follow_symlinks=True): """Copy function for shutil.copytree which prints copied files.""" - print('{} -> {}'.format(src, dst)) + if '-v' in sys.argv: + print('{} -> {}'.format(src, dst)) shutil.copy(src, dst, follow_symlinks=follow_symlinks) @@ -112,6 +113,7 @@ def copy_or_link(source, dest): """Copy or symlink source to dest.""" if os.name == 'nt': if os.path.isdir(source): + print('{} -> {}'.format(source, dest)) shutil.copytree(source, dest, ignore=get_ignored_files, copy_function=verbose_copy) else: From 68d8900c6c03f16d83fafb48cbdee555ee47f132 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Jun 2015 18:32:47 +0200 Subject: [PATCH 035/146] link_pyqt: Support PYTHON environment variable. --- scripts/link_pyqt.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/link_pyqt.py b/scripts/link_pyqt.py index b35fed126..20e8e3e21 100644 --- a/scripts/link_pyqt.py +++ b/scripts/link_pyqt.py @@ -140,7 +140,10 @@ def get_python_lib(executable, venv=False): treatments for Windows/Ubuntu shouldn't take place. """ distribution = platform.linux_distribution(full_distribution_name=False) - if os.name == 'nt' and not venv: + if 'PYTHON' in os.environ and not venv: + # e.g. on AppVeyor + return os.path.join(os.environ['PYTHON'], 'Lib', 'site-packages') + elif os.name == 'nt' and not venv: # For some reason, we get an empty string from get_python_lib() on # Windows when running via tox, and sys.prefix is empty too... return os.path.join(os.path.dirname(executable), '..', 'Lib', From 67473c6db196b71fb2ad37c62f5a1380e79ce342 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Jun 2015 18:33:15 +0200 Subject: [PATCH 036/146] tox: Add PYTHON to passenv. --- tox.ini | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index 603a771a6..8085ae2ea 100644 --- a/tox.ini +++ b/tox.ini @@ -7,6 +7,7 @@ envlist = unittests,misc,pep257,pyflakes,pep8,mccabe,pylint,pyroma,check-manifest [testenv] +passenv = PYTHON basepython = python3 [testenv:mkvenv] @@ -17,7 +18,7 @@ usedevelop = true [testenv:unittests] # https://bitbucket.org/hpk42/tox/issue/246/ - only needed for Windows though setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/platforms -passenv = DISPLAY XAUTHORITY HOME +passenv = PYTHON DISPLAY XAUTHORITY HOME deps = -r{toxinidir}/requirements.txt py==1.4.28 @@ -31,7 +32,7 @@ commands = {envpython} -m py.test --strict -rfEsw {posargs} [testenv:coverage] -passenv = DISPLAY XAUTHORITY HOME +passenv = PYTHON DISPLAY XAUTHORITY HOME deps = {[testenv:unittests]deps} coverage==3.7.1 @@ -65,7 +66,7 @@ commands = [testenv:pep257] skip_install = true deps = pep257==0.5.0 -passenv = LANG +passenv = PYTHON LANG # Disabled checks: # D102: Docstring missing, will be handled by others # D209: Blank line before closing """ (removed from PEP257) @@ -138,7 +139,7 @@ commands = [testenv:smoke] # https://bitbucket.org/hpk42/tox/issue/246/ - only needed for Windows though setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/platforms -passenv = DISPLAY XAUTHORITY HOME USERNAME USER +passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER deps = -r{toxinidir}/requirements.txt commands = From 5a8b7910e0ccfd52305e57d43f78f04da8e9b1af Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Jun 2015 18:33:37 +0200 Subject: [PATCH 037/146] tox: Use python -m to start pylint. This makes it also work on Windows, where bin/ is called Scripts/. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 8085ae2ea..99690987d 100644 --- a/tox.ini +++ b/tox.ini @@ -60,7 +60,7 @@ deps = six==1.9.0 commands = {envpython} scripts/link_pyqt.py --tox {envdir} - {envdir}/bin/pylint scripts qutebrowser --rcfile=.pylintrc --output-format=colorized --reports=no --expected-line-ending-format=LF + {envpython} -m pylint scripts qutebrowser --rcfile=.pylintrc --output-format=colorized --reports=no --expected-line-ending-format=LF {envpython} scripts/run_pylint_on_tests.py --rcfile=.pylintrc --output-format=colorized --reports=no --expected-line-ending-format=LF [testenv:pep257] From 863e194073e41c677b56d2cf44b78cb67612aa08 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Jun 2015 18:34:11 +0200 Subject: [PATCH 038/146] Update MANIFEST.in --- MANIFEST.in | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MANIFEST.in b/MANIFEST.in index 4092f81c5..340fc796c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -31,3 +31,6 @@ exclude .coveragerc exclude .pylintrc exclude .eslintrc exclude doc/help +exclude .appveyor.yml +exclude .travis.yml +exclude misc/appveyor_install.py From 9b7b97d6267af4860dba9b8e61856401de98ded7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Jun 2015 20:55:15 +0200 Subject: [PATCH 039/146] Improve docs. --- README.asciidoc | 1 + doc/help/settings.asciidoc | 163 +++++++++++++++++++++++-------- qutebrowser/config/configdata.py | 13 ++- 3 files changed, 131 insertions(+), 46 deletions(-) diff --git a/README.asciidoc b/README.asciidoc index 547d940e7..e1706d8e7 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -140,6 +140,7 @@ Contributors, sorted by the number of commits in descending order: * Claude * Lamar Pavel * Martin Tournoij +* Austin Anderson * Artur Shaik * Antoni Boucher * ZDarian diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 3b6319af9..837f84a3e 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -187,13 +187,21 @@ |<>|Top border color of the completion widget category headers. |<>|Bottom border color of the selected completion item. |<>|Foreground color of the matched text in the completion. -|<>|Foreground color of the statusbar. |<>|Foreground color of the statusbar. +|<>|Foreground color of the statusbar. +|<>|Foreground color of the statusbar if there was an error. |<>|Background color of the statusbar if there was an error. +|<>|Foreground color of the statusbar if there is a warning. |<>|Background color of the statusbar if there is a warning. +|<>|Foreground color of the statusbar if there is a prompt. |<>|Background color of the statusbar if there is a prompt. +|<>|Foreground color of the statusbar in insert mode. |<>|Background color of the statusbar in insert mode. +|<>|Foreground color of the statusbar in command mode. +|<>|Background color of the statusbar in command mode. +|<>|Foreground color of the statusbar in caret mode. |<>|Background color of the statusbar in caret mode. +|<>|Foreground color of the statusbar in caret mode with a selection |<>|Background color of the statusbar in caret mode with a selection |<>|Background color of the progress bar. |<>|Default foreground color of the URL in the statusbar. @@ -202,10 +210,10 @@ |<>|Foreground color of the URL in the statusbar when there's a warning. |<>|Foreground color of the URL in the statusbar for hovered links. |<>|Foreground color of unselected odd tabs. -|<>|Foreground color of unselected even tabs. -|<>|Foreground color of selected tabs. |<>|Background color of unselected odd tabs. +|<>|Foreground color of unselected even tabs. |<>|Background color of unselected even tabs. +|<>|Foreground color of selected tabs. |<>|Background color of selected tabs. |<>|Background color of the tab bar. |<>|Color gradient start for the tab indicator. @@ -213,13 +221,16 @@ |<>|Color for the tab indicator on errors.. |<>|Color gradient interpolation system for the tab indicator. |<>|Font color for hints. -|<>|Font color for the matched part of hints. |<>|Background color for hints. -|<>|Foreground color for downloads. +|<>|Font color for the matched part of hints. |<>|Background color for the download bar. -|<>|Color gradient start for downloads. -|<>|Color gradient end for downloads. -|<>|Color gradient interpolation system for downloads. +|<>|Color gradient start for download text. +|<>|Color gradient start for download backgrounds. +|<>|Color gradient end for download text. +|<>|Color gradient stop for download backgrounds. +|<>|Color gradient interpolation system for download text. +|<>|Color gradient interpolation system for download backgrounds. +|<>|Foreground color for downloads with errors. |<>|Background color for downloads with errors. |<>|Background color for webpages if unset (or empty to use the theme's color) |============== @@ -1465,6 +1476,8 @@ A value can be in one of the following format: * `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359) * A gradient as explained in http://qt-project.org/doc/qt-4.8/stylesheet-reference.html#list-of-property-types[the Qt documentation] under ``Gradient''. +A *.system value determines the color system to use for color interpolation between similarly-named *.start and *.stop entries, regardless of how they are defined in the options. Valid values are 'rgb', 'hsv', and 'hsl'. + The `hints.*` values are a special case as they're real CSS colors, not Qt-CSS colors. There, for a gradient, you need to use `-webkit-gradient`, see https://www.webkit.org/blog/175/introducing-css-gradients/[the WebKit documentation]. [[colors-completion.fg]] @@ -1539,17 +1552,23 @@ Foreground color of the matched text in the completion. Default: +pass:[#ff4444]+ +[[colors-statusbar.fg]] +=== statusbar.fg +Foreground color of the statusbar. + +Default: +pass:[white]+ + [[colors-statusbar.bg]] === statusbar.bg Foreground color of the statusbar. Default: +pass:[black]+ -[[colors-statusbar.fg]] -=== statusbar.fg -Foreground color of the statusbar. +[[colors-statusbar.fg.error]] +=== statusbar.fg.error +Foreground color of the statusbar if there was an error. -Default: +pass:[white]+ +Default: +pass:[${statusbar.fg}]+ [[colors-statusbar.bg.error]] === statusbar.bg.error @@ -1557,30 +1576,72 @@ Background color of the statusbar if there was an error. Default: +pass:[red]+ +[[colors-statusbar.fg.warning]] +=== statusbar.fg.warning +Foreground color of the statusbar if there is a warning. + +Default: +pass:[${statusbar.fg}]+ + [[colors-statusbar.bg.warning]] === statusbar.bg.warning Background color of the statusbar if there is a warning. Default: +pass:[darkorange]+ +[[colors-statusbar.fg.prompt]] +=== statusbar.fg.prompt +Foreground color of the statusbar if there is a prompt. + +Default: +pass:[${statusbar.fg}]+ + [[colors-statusbar.bg.prompt]] === statusbar.bg.prompt Background color of the statusbar if there is a prompt. Default: +pass:[darkblue]+ +[[colors-statusbar.fg.insert]] +=== statusbar.fg.insert +Foreground color of the statusbar in insert mode. + +Default: +pass:[${statusbar.fg}]+ + [[colors-statusbar.bg.insert]] === statusbar.bg.insert Background color of the statusbar in insert mode. Default: +pass:[darkgreen]+ +[[colors-statusbar.fg.command]] +=== statusbar.fg.command +Foreground color of the statusbar in command mode. + +Default: +pass:[${statusbar.fg}]+ + +[[colors-statusbar.bg.command]] +=== statusbar.bg.command +Background color of the statusbar in command mode. + +Default: +pass:[${statusbar.bg}]+ + +[[colors-statusbar.fg.caret]] +=== statusbar.fg.caret +Foreground color of the statusbar in caret mode. + +Default: +pass:[${statusbar.fg}]+ + [[colors-statusbar.bg.caret]] === statusbar.bg.caret Background color of the statusbar in caret mode. Default: +pass:[purple]+ +[[colors-statusbar.fg.caret-selection]] +=== statusbar.fg.caret-selection +Foreground color of the statusbar in caret mode with a selection + +Default: +pass:[${statusbar.fg}]+ + [[colors-statusbar.bg.caret-selection]] === statusbar.bg.caret-selection Background color of the statusbar in caret mode with a selection @@ -1629,30 +1690,30 @@ Foreground color of unselected odd tabs. Default: +pass:[white]+ -[[colors-tabs.fg.even]] -=== tabs.fg.even -Foreground color of unselected even tabs. - -Default: +pass:[white]+ - -[[colors-tabs.fg.selected]] -=== tabs.fg.selected -Foreground color of selected tabs. - -Default: +pass:[white]+ - [[colors-tabs.bg.odd]] === tabs.bg.odd Background color of unselected odd tabs. Default: +pass:[grey]+ +[[colors-tabs.fg.even]] +=== tabs.fg.even +Foreground color of unselected even tabs. + +Default: +pass:[white]+ + [[colors-tabs.bg.even]] === tabs.bg.even Background color of unselected even tabs. Default: +pass:[darkgrey]+ +[[colors-tabs.fg.selected]] +=== tabs.fg.selected +Foreground color of selected tabs. + +Default: +pass:[white]+ + [[colors-tabs.bg.selected]] === tabs.bg.selected Background color of selected tabs. @@ -1701,23 +1762,17 @@ Font color for hints. Default: +pass:[black]+ -[[colors-hints.fg.match]] -=== hints.fg.match -Font color for the matched part of hints. - -Default: +pass:[green]+ - [[colors-hints.bg]] === hints.bg Background color for hints. Default: +pass:[-webkit-gradient(linear, left top, left bottom, color-stop(0%,#FFF785), color-stop(100%,#FFC542))]+ -[[colors-downloads.fg]] -=== downloads.fg -Foreground color for downloads. +[[colors-hints.fg.match]] +=== hints.fg.match +Font color for the matched part of hints. -Default: +pass:[#ffffff]+ +Default: +pass:[green]+ [[colors-downloads.bg.bar]] === downloads.bg.bar @@ -1725,21 +1780,33 @@ Background color for the download bar. Default: +pass:[black]+ +[[colors-downloads.fg.start]] +=== downloads.fg.start +Color gradient start for download text. + +Default: +pass:[white]+ + [[colors-downloads.bg.start]] === downloads.bg.start -Color gradient start for downloads. +Color gradient start for download backgrounds. Default: +pass:[#0000aa]+ +[[colors-downloads.fg.stop]] +=== downloads.fg.stop +Color gradient end for download text. + +Default: +pass:[${downloads.fg.start}]+ + [[colors-downloads.bg.stop]] === downloads.bg.stop -Color gradient end for downloads. +Color gradient stop for download backgrounds. Default: +pass:[#00aa00]+ -[[colors-downloads.bg.system]] -=== downloads.bg.system -Color gradient interpolation system for downloads. +[[colors-downloads.fg.system]] +=== downloads.fg.system +Color gradient interpolation system for download text. Valid values: @@ -1749,6 +1816,24 @@ Valid values: Default: +pass:[rgb]+ +[[colors-downloads.bg.system]] +=== downloads.bg.system +Color gradient interpolation system for download backgrounds. + +Valid values: + + * +rgb+: Interpolate in the RGB color system. + * +hsv+: Interpolate in the HSV color system. + * +hsl+: Interpolate in the HSL color system. + +Default: +pass:[rgb]+ + +[[colors-downloads.fg.error]] +=== downloads.fg.error +Foreground color for downloads with errors. + +Default: +pass:[white]+ + [[colors-downloads.bg.error]] === downloads.bg.error Background color for downloads with errors. diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index ccd71da51..eb1b529c7 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -980,28 +980,27 @@ def data(readonly=False): ('downloads.fg.start', SettingValue(typ.QtColor(), 'white'), - "Color gradient start for download foreground text."), + "Color gradient start for download text."), ('downloads.bg.start', SettingValue(typ.QtColor(), '#0000aa'), - "Color gradient start for download background."), + "Color gradient start for download backgrounds."), ('downloads.fg.stop', SettingValue(typ.QtColor(), '${downloads.fg.start}'), - "Color gradient end for download foreground text."), + "Color gradient end for download text."), ('downloads.bg.stop', SettingValue(typ.QtColor(), '#00aa00'), - "Color gradient stop for download background."), + "Color gradient stop for download backgrounds."), ('downloads.fg.system', SettingValue(typ.ColorSystem(), 'rgb'), - "Color gradient interpolation system for download foreground" - "text."), + "Color gradient interpolation system for download text."), ('downloads.bg.system', SettingValue(typ.ColorSystem(), 'rgb'), - "Color gradient interpolation system for download background."), + "Color gradient interpolation system for download backgrounds."), ('downloads.fg.error', SettingValue(typ.QtColor(), 'white'), From fdd302e4f7e3ec3a63355bb797fe3f38ae68d309 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Jun 2015 21:17:23 +0200 Subject: [PATCH 040/146] Update changelog. --- CHANGELOG.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index dff5e2d13..890636b3a 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -39,6 +39,7 @@ Added - New setting `ui -> modal-js-dialog` to use the standard modal dialogs for javascript questions instead of using the statusbar. - New setting `colors -> webpage.bg` to set the background color to use for websites which don't set one. - New (hidden) command `:clear-keychain` to clear a partially entered keychain (bound to `` by default, in addition to clearing search). +- Many new color settings (foreground setting for every background setting). Changed ~~~~~~~ From 480c4e878e6d385902c3eee0de1f85fc01d61969 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2015 10:26:18 +0200 Subject: [PATCH 041/146] Ignore pylint warning on Ubuntu/Travis. --- tests/utils/test_qtutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/test_qtutils.py b/tests/utils/test_qtutils.py index ad4fb0c4a..1d52d677c 100644 --- a/tests/utils/test_qtutils.py +++ b/tests/utils/test_qtutils.py @@ -25,7 +25,7 @@ import sys import operator import os.path try: - from test import test_file + from test import test_file # pylint: disable=no-name-in-module except ImportError: # Debian patches Python to remove the tests... test_file = None From 3e8a394217668d00a0c249d5d01f1f40dcb3f328 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2015 10:49:06 +0200 Subject: [PATCH 042/146] Disable no-member for pylint for os.SEEK_*. This should fix pylint on Windows. --- tests/utils/test_qtutils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/utils/test_qtutils.py b/tests/utils/test_qtutils.py index 1d52d677c..25bf728c2 100644 --- a/tests/utils/test_qtutils.py +++ b/tests/utils/test_qtutils.py @@ -748,9 +748,9 @@ class TestPyQIODevice: def test_seek_unsupported(self, pyqiodev): """Test seeking with unsupported whence arguments.""" if hasattr(os, 'SEEK_HOLE'): - whence = os.SEEK_HOLE + whence = os.SEEK_HOLE # pylint: disable=no-member elif hasattr(os, 'SEEK_DATA'): - whence = os.SEEK_DATA + whence = os.SEEK_DATA # pylint: disable=no-member else: pytest.skip("Needs os.SEEK_HOLE or os.SEEK_DATA available.") pyqiodev.open(QIODevice.ReadOnly) From 599f582c205ce6ba31e02acadca8c3bf6fa222f8 Mon Sep 17 00:00:00 2001 From: Austin Anderson Date: Thu, 11 Jun 2015 08:07:59 -0400 Subject: [PATCH 043/146] More specific statusbar styling: resolves #750. --- qutebrowser/mainwindow/statusbar/bar.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 689ea77bb..fa16c24a4 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -108,50 +108,58 @@ class StatusBar(QWidget): STYLESHEET = """ QWidget#StatusBar, - QWidget#StatusBar * { + QWidget#StatusBar QLabel, + QWidget#StatusBar QLineEdit { {{ font['statusbar'] }} {{ color['statusbar.bg'] }} {{ color['statusbar.fg'] }} } QWidget#StatusBar[caret_mode="on"], - QWidget#StatusBar[caret_mode="on"] * { + QWidget#StatusBar[caret_mode="on"] QLabel, + QWidget#StatusBar[caret_mode="on"] QLineEdit { {{ color['statusbar.fg.caret'] }} {{ color['statusbar.bg.caret'] }} } QWidget#StatusBar[caret_mode="selection"], - QWidget#StatusBar[caret_mode="selection"] * { + QWidget#StatusBar[caret_mode="selection"] QLabel, + QWidget#StatusBar[caret_mode="selection"] QLineEdit { {{ color['statusbar.fg.caret-selection'] }} {{ color['statusbar.bg.caret-selection'] }} } QWidget#StatusBar[severity="error"], - QWidget#StatusBar[severity="error"] * { + QWidget#StatusBar[severity="error"] QLabel, + QWidget#StatusBar[severity="error"] QLineEdit { {{ color['statusbar.fg.error'] }} {{ color['statusbar.bg.error'] }} } QWidget#StatusBar[severity="warning"], - QWidget#StatusBar[severity="warning"] * { + QWidget#StatusBar[severity="warning"] QLabel, + QWidget#StatusBar[severity="warning"] QLineEdit { {{ color['statusbar.fg.warning'] }} {{ color['statusbar.bg.warning'] }} } QWidget#StatusBar[prompt_active="true"], - QWidget#StatusBar[prompt_active="true"] * { + QWidget#StatusBar[prompt_active="true"] QLabel, + QWidget#StatusBar[prompt_active="true"] QLineEdit { {{ color['statusbar.fg.prompt'] }} {{ color['statusbar.bg.prompt'] }} } QWidget#StatusBar[insert_active="true"], - QWidget#StatusBar[insert_active="true"] * { + QWidget#StatusBar[insert_active="true"] QLabel, + QWidget#StatusBar[insert_active="true"] QLineEdit { {{ color['statusbar.fg.insert'] }} {{ color['statusbar.bg.insert'] }} } QWidget#StatusBar[command_active="true"], - QWidget#StatusBar[command_active="true"] * { + QWidget#StatusBar[command_active="true"] QLabel, + QWidget#StatusBar[command_active="true"] QLineEdit { {{ color['statusbar.fg.command'] }} {{ color['statusbar.bg.command'] }} } From a79b07bd94f26f282e042f0b8126b7ff968ba09b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2015 16:15:33 +0200 Subject: [PATCH 044/146] Rename appveyor_install to ci_install. --- .appveyor.yml | 2 +- scripts/{appveyor_install.py => ci_install.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename scripts/{appveyor_install.py => ci_install.py} (100%) diff --git a/.appveyor.yml b/.appveyor.yml index 45d25ad2b..947d0fa91 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -7,7 +7,7 @@ environment: PYTHONUNBUFFERED: 1 install: - - C:\Python34\python -u scripts\appveyor_install.py + - C:\Python34\python -u scripts\ci_install.py test_script: - C:\Python34\Scripts\tox -e smoke diff --git a/scripts/appveyor_install.py b/scripts/ci_install.py similarity index 100% rename from scripts/appveyor_install.py rename to scripts/ci_install.py From 364e13f4c2fff2ce16e1291ccd1734de42037aca Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2015 16:36:58 +0200 Subject: [PATCH 045/146] Add OS X support for Travis. --- .travis.yml | 7 +++-- scripts/ci_install.py | 70 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/.travis.yml b/.travis.yml index 61c77bcbf..9ed28743e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,14 @@ dist: trusty +os: + - linux + - osx + # Not really, but this is here so we can do stuff by hand. language: c install: - - sudo apt-get -y -q update - - sudo apt-get -y -q install python3-pyqt5 python3-pyqt5.qtwebkit python-tox python3-dev xvfb + - python scripts/ci_install.py script: - xvfb-run -s "-screen 0 640x480x16" tox -e unittests,smoke diff --git a/scripts/ci_install.py b/scripts/ci_install.py index 9758713be..c0edb95d1 100644 --- a/scripts/ci_install.py +++ b/scripts/ci_install.py @@ -18,25 +18,71 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . +# pylint: disable=open-without-encoding + """Install needed prerequisites on the AppVeyor CI.""" +import os +import sys import subprocess import urllib.request PYQT_VERSION = '5.4.1' -print("Getting PyQt5...") -urllib.request.urlretrieve( - ('http://sourceforge.net/projects/pyqt/files/PyQt5/PyQt-{v}/' - 'PyQt5-{v}-gpl-Py3.4-Qt{v}-x32.exe'.format(v=PYQT_VERSION)), - r'C:\install-PyQt5.exe') -print("Installing PyQt5...") -subprocess.check_call([r'C:\install-PyQt5.exe', '/S']) +def apt_get(args): + subprocess.check_call(['sudo', 'apt-get', '-y', '-q'] + args) -print("Installing tox...") -subprocess.check_call([r'C:\Python34\Scripts\pip', 'install', 'tox']) -print("Linking Python...") -with open(r'C:\Windows\system32\python3.bat', 'w', encoding='ascii') as f: - f.write(r'@C:\Python34\python %*') +def brew(args): + subprocess.check_call(['brew'] + args) + + +if 'APPVEYOR' in os.environ: + print("Getting PyQt5...") + urllib.request.urlretrieve( + ('http://sourceforge.net/projects/pyqt/files/PyQt5/PyQt-{v}/' + 'PyQt5-{v}-gpl-Py3.4-Qt{v}-x32.exe'.format(v=PYQT_VERSION)), + r'C:\install-PyQt5.exe') + + print("Installing PyQt5...") + subprocess.check_call([r'C:\install-PyQt5.exe', '/S']) + + print("Installing tox...") + subprocess.check_call([r'C:\Python34\Scripts\pip', 'install', 'tox']) + + print("Linking Python...") + with open(r'C:\Windows\system32\python3.bat', 'w') as f: + f.write(r'@C:\Python34\python %*') +elif os.environ.get('TRAVIS_OS_NAME', None) == 'linux': + print("apt-get update...") + apt_get(['update']) + + print("Installing packages...") + pkgs = 'python3-pyqt5 python3-pyqt5.qtwebkit python-tox python3-dev xvfb' + apt_get(['install'] + pkgs.split()) +elif os.environ.get('TRAVIS_OS_NAME', None) == 'osx': + print("brew update...") + brew(['update']) + + print("Installing packages...") + brew(['install', 'python3', 'pyqt5']) + + print("Installing tox...") + subprocess.check_call(['pip3.4', 'install', 'tox']) + + print("Creating xvfb-run stub...") + with open('/usr/local/bin/xvfb-run', 'w') as f: + # This will break when xvfb-run is called differently in .travis.yml, + # but I can't be bothered to do it in a nicer way. + f.write('#!/bin/bash\n') + f.write('shift 2\n') + f.write('exec "$@"\n') +else: + def env(key): + return os.environ.get(key, None) + print("Unknown environment! (CI {}, APPVEYOR {}, TRAVIS {}, " + "TRAVIS_OS_NAME {})".format(env('CI'), env('APPVEYOR'), + env('TRAVIS'), env('TRAVIS_OS_NAME')), + file=sys.stderr) + sys.exit(1) From 90bbe4d1ef93a7abe80333c94299cf4b9e779f17 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2015 16:41:36 +0200 Subject: [PATCH 046/146] Make ci_install.py python2 compatible. --- .appveyor.yml | 2 +- .pylintrc | 3 ++- scripts/ci_install.py | 16 +++++++++++----- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 947d0fa91..d2488cfe4 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -7,7 +7,7 @@ environment: PYTHONUNBUFFERED: 1 install: - - C:\Python34\python -u scripts\ci_install.py + - C:\Python27\python -u scripts\ci_install.py test_script: - C:\Python34\Scripts\tox -e smoke diff --git a/.pylintrc b/.pylintrc index a4abb32a0..664eda267 100644 --- a/.pylintrc +++ b/.pylintrc @@ -27,7 +27,8 @@ disable=no-self-use, broad-except, bare-except, eval-used, - exec-used + exec-used, + file-ignored [BASIC] module-rgx=(__)?[a-z][a-z0-9_]*(__)?$ diff --git a/scripts/ci_install.py b/scripts/ci_install.py index c0edb95d1..1af604892 100644 --- a/scripts/ci_install.py +++ b/scripts/ci_install.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python2 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: # Copyright 2015 Florian Bruhin (The Compiler) @@ -18,14 +18,20 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -# pylint: disable=open-without-encoding +# pylint: skip-file -"""Install needed prerequisites on the AppVeyor CI.""" +"""Install needed prerequisites on the AppVeyor/Travis CI. + +Note this file is written in python2 as this is more readily available on the +CI machines. +""" + +from __future__ import print_function import os import sys import subprocess -import urllib.request +import urllib PYQT_VERSION = '5.4.1' @@ -40,7 +46,7 @@ def brew(args): if 'APPVEYOR' in os.environ: print("Getting PyQt5...") - urllib.request.urlretrieve( + urllib.urlretrieve( ('http://sourceforge.net/projects/pyqt/files/PyQt5/PyQt-{v}/' 'PyQt5-{v}-gpl-Py3.4-Qt{v}-x32.exe'.format(v=PYQT_VERSION)), r'C:\install-PyQt5.exe') From 17bb9fc21c3e364e36de42d2df2bd5c4ad0b625c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 29 May 2015 23:46:07 +0200 Subject: [PATCH 047/146] Use QProcess instead of subprocess. Closes #646. Fixes #688. --- CONTRIBUTING.asciidoc | 4 +--- qutebrowser/browser/commands.py | 33 ++++++++++++----------------- qutebrowser/browser/hints.py | 20 +++++++++++------ qutebrowser/commands/userscripts.py | 19 ++--------------- qutebrowser/misc/editor.py | 15 +++---------- qutebrowser/utils/qtutils.py | 16 +++++++++++++- 6 files changed, 47 insertions(+), 60 deletions(-) diff --git a/CONTRIBUTING.asciidoc b/CONTRIBUTING.asciidoc index 1975a9d7c..8123514a9 100644 --- a/CONTRIBUTING.asciidoc +++ b/CONTRIBUTING.asciidoc @@ -238,9 +238,7 @@ There are some exceptions to that: * `QThread` is used instead of Python threads because it provides signals and slots. -* `QProcess` is used instead of Python's `subprocess` if certain actions (e.g. -cleanup) when the process finished are desired, as it provides signals for -that. +* `QProcess` is used instead of Python's `subprocess` * `QUrl` is used instead of storing URLs as string, see the <> section for details. diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 9bfe81a10..984963b19 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -22,14 +22,13 @@ import re import os import shlex -import subprocess import posixpath import functools import xml.etree.ElementTree from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWidgets import QApplication, QTabBar -from PyQt5.QtCore import Qt, QUrl, QEvent +from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QEvent, QProcess from PyQt5.QtGui import QClipboard, QKeyEvent from PyQt5.QtPrintSupport import QPrintDialog, QPrintPreviewDialog from PyQt5.QtWebKitWidgets import QWebPage @@ -929,11 +928,6 @@ class CommandDispatcher: Note the {url} variable which gets replaced by the current URL might be useful here. - // - - We use subprocess rather than Qt's QProcess here because we really - don't care about the process anymore as soon as it's spawned. - Args: userscript: Run the command as an userscript. quiet: Don't print the commandline being executed. @@ -944,16 +938,21 @@ class CommandDispatcher: if not quiet: fake_cmdline = ' '.join(shlex.quote(arg) for arg in args) message.info(win_id, 'Executing: ' + fake_cmdline) + cmd, *args = args if userscript: - cmd = args[0] - args = [] if not args else args[1:] self.run_userscript(cmd, *args) else: - try: - subprocess.Popen(args) - except OSError as e: - raise cmdexc.CommandError("Error while spawning command: " - "{}".format(e)) + proc = QProcess(self._tabbed_browser) + proc.error.connect(self.on_process_error) + proc.start(cmd, args) + + @pyqtSlot('QProcess::ProcessError') + def on_process_error(self, error): + """Display an error if a :spawn'ed process failed.""" + msg = qtutils.QPROCESS_ERRORS[error] + message.error(self._win_id, + "Error while spawning command: {}".format(msg), + immediately=True) @cmdutils.register(instance='command-dispatcher', scope='window') def home(self): @@ -1166,12 +1165,6 @@ class CommandDispatcher: The editor which should be launched can be configured via the `general -> editor` config option. - - // - - We use QProcess rather than subprocess here because it makes it a lot - easier to execute some code as soon as the process has been finished - and do everything async. """ frame = self._current_widget().page().currentFrame() try: diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index eef512f55..b9b1664b5 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -21,11 +21,10 @@ import math import functools -import subprocess import collections from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl, - QTimer) + QTimer, QProcess) from PyQt5.QtGui import QMouseEvent, QClipboard from PyQt5.QtWidgets import QApplication from PyQt5.QtWebKit import QWebElement @@ -548,11 +547,18 @@ class HintManager(QObject): """ urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword) args = context.get_args(urlstr) - try: - subprocess.Popen(args) - except OSError as e: - msg = "Error while spawning command: {}".format(e) - message.error(self._win_id, msg, immediately=True) + cmd, *args = args + proc = QProcess(self) + proc.error.connect(self.on_process_error) + proc.start(cmd, args) + + @pyqtSlot('QProcess::ProcessError') + def on_process_error(self, error): + """Display an error if a :spawn'ed process failed.""" + msg = qtutils.QPROCESS_ERRORS[error] + message.error(self._win_id, + "Error while spawning command: {}".format(msg), + immediately=True) def _resolve_url(self, elem, baseurl): """Resolve a URL and check if we want to keep it. diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py index 76e9f94a6..ceff1dffc 100644 --- a/qutebrowser/commands/userscripts.py +++ b/qutebrowser/commands/userscripts.py @@ -26,7 +26,7 @@ import tempfile from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QSocketNotifier, QProcessEnvironment, QProcess) -from qutebrowser.utils import message, log, objreg, standarddir +from qutebrowser.utils import message, log, objreg, standarddir, qtutils from qutebrowser.commands import runners, cmdexc from qutebrowser.config import config @@ -73,10 +73,6 @@ class _BaseUserscriptRunner(QObject): _proc: The QProcess which is being executed. _win_id: The window ID this runner is associated with. - Class attributes: - PROCESS_MESSAGES: A mapping of QProcess::ProcessError members to - human-readable error strings. - Signals: got_cmd: Emitted when a new command arrived and should be executed. finished: Emitted when the userscript finished running. @@ -85,17 +81,6 @@ class _BaseUserscriptRunner(QObject): got_cmd = pyqtSignal(str) finished = pyqtSignal() - PROCESS_MESSAGES = { - QProcess.FailedToStart: "The process failed to start.", - QProcess.Crashed: "The process crashed.", - QProcess.Timedout: "The last waitFor...() function timed out.", - QProcess.WriteError: ("An error occurred when attempting to write to " - "the process."), - QProcess.ReadError: ("An error occurred when attempting to read from " - "the process."), - QProcess.UnknownError: "An unknown error occurred.", - } - def __init__(self, win_id, parent=None): super().__init__(parent) self._win_id = win_id @@ -166,7 +151,7 @@ class _BaseUserscriptRunner(QObject): def on_proc_error(self, error): """Called when the process encountered an error.""" - msg = self.PROCESS_MESSAGES[error] + msg = qtutils.QPROCESS_ERRORS[error] # NOTE: Do not replace this with "raise CommandError" as it's # executed async. message.error(self._win_id, diff --git a/qutebrowser/misc/editor.py b/qutebrowser/misc/editor.py index 32e4100ca..32d108490 100644 --- a/qutebrowser/misc/editor.py +++ b/qutebrowser/misc/editor.py @@ -25,7 +25,7 @@ import tempfile from PyQt5.QtCore import pyqtSignal, QProcess, QObject from qutebrowser.config import config -from qutebrowser.utils import message, log +from qutebrowser.utils import message, log, qtutils class ExternalEditor(QObject): @@ -96,20 +96,11 @@ class ExternalEditor(QObject): def on_proc_error(self, error): """Display an error message and clean up when editor crashed.""" - messages = { - QProcess.FailedToStart: "The process failed to start.", - QProcess.Crashed: "The process crashed.", - QProcess.Timedout: "The last waitFor...() function timed out.", - QProcess.WriteError: ("An error occurred when attempting to write " - "to the process."), - QProcess.ReadError: ("An error occurred when attempting to read " - "from the process."), - QProcess.UnknownError: "An unknown error occurred.", - } + msg = qtutils.QPROCESS_ERRORS[error] # NOTE: Do not replace this with "raise CommandError" as it's # executed async. message.error(self._win_id, - "Error while calling editor: {}".format(messages[error])) + "Error while calling editor: {}".format(msg)) self._cleanup() def edit(self, text): diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 6573306ab..42bca6bec 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -36,7 +36,7 @@ import distutils.version # pylint: disable=no-name-in-module,import-error import contextlib from PyQt5.QtCore import (qVersion, QEventLoop, QDataStream, QByteArray, - QIODevice, QSaveFile) + QIODevice, QSaveFile, QProcess) from PyQt5.QtWidgets import QApplication @@ -400,3 +400,17 @@ class EventLoop(QEventLoop): self._executing = True super().exec_(flags) self._executing = False + + +# A mapping of QProcess::ErrorCode's to human-readable strings. + +QPROCESS_ERRORS = { + QProcess.FailedToStart: "The process failed to start.", + QProcess.Crashed: "The process crashed.", + QProcess.Timedout: "The last waitFor...() function timed out.", + QProcess.WriteError: ("An error occurred when attempting to write to the " + "process."), + QProcess.ReadError: ("An error occurred when attempting to read from the " + "process."), + QProcess.UnknownError: "An unknown error occurred.", +} From 231f1d90ce063489cc8ba192f57604b9ac0916f3 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 31 May 2015 16:25:25 +0200 Subject: [PATCH 048/146] Add a -d/--detach argument to :spawn. --- qutebrowser/browser/commands.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 984963b19..2c4816e33 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -922,7 +922,8 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window', win_id='win_id') - def spawn(self, win_id, userscript=False, quiet=False, *args): + def spawn(self, win_id, userscript=False, quiet=False, detach=False, + *args): """Spawn a command in a shell. Note the {url} variable which gets replaced by the current URL might be @@ -931,6 +932,7 @@ class CommandDispatcher: Args: userscript: Run the command as an userscript. quiet: Don't print the commandline being executed. + detach: Whether the command should be detached from qutebrowser. *args: The commandline to execute. """ log.procs.debug("Executing: {}, userscript={}".format( @@ -944,7 +946,12 @@ class CommandDispatcher: else: proc = QProcess(self._tabbed_browser) proc.error.connect(self.on_process_error) - proc.start(cmd, args) + if detach: + ok = proc.startDetached(cmd, args) + if not ok: + raise cmdexc.CommandError("Error while spawning command") + else: + proc.start(cmd, args) @pyqtSlot('QProcess::ProcessError') def on_process_error(self, error): From 1a9bc64776bfcd0a0b49ce4b7fa0fe4cee8498f5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 31 May 2015 16:32:21 +0200 Subject: [PATCH 049/146] Display an error on non-zero :spawn exit. --- qutebrowser/browser/commands.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 2c4816e33..1aed25ce4 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -952,6 +952,8 @@ class CommandDispatcher: raise cmdexc.CommandError("Error while spawning command") else: proc.start(cmd, args) + if not quiet: + proc.finished.connect(self.on_process_finished) @pyqtSlot('QProcess::ProcessError') def on_process_error(self, error): @@ -961,6 +963,13 @@ class CommandDispatcher: "Error while spawning command: {}".format(msg), immediately=True) + @pyqtSlot(int, 'QProcess::ExitStatus') + def on_process_finished(self, code, _status): + """Display an error if a :spawn'ed process exited with non-0 status.""" + if code != 0: + message.error(self._win_id, "Spawned command exited with status " + "{}!".format(code)) + @cmdutils.register(instance='command-dispatcher', scope='window') def home(self): """Open main startpage in current tab.""" From 163bc2e12e21955b65ba27e0bb97b9ca35ff8c81 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 Jun 2015 07:57:27 +0200 Subject: [PATCH 050/146] Add GUIProcess. This aims to unify the code which spawns a process and then shows statusbar notifications when it exited, etc. --- qutebrowser/browser/commands.py | 32 ++----- qutebrowser/browser/hints.py | 14 +-- qutebrowser/commands/userscripts.py | 47 ++++------ qutebrowser/misc/editor.py | 30 +++---- qutebrowser/misc/guiprocess.py | 131 ++++++++++++++++++++++++++++ qutebrowser/utils/qtutils.py | 16 +--- tests/misc/test_editor.py | 8 +- 7 files changed, 174 insertions(+), 104 deletions(-) create mode 100644 qutebrowser/misc/guiprocess.py diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 1aed25ce4..e73255eab 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -28,7 +28,7 @@ import xml.etree.ElementTree from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWidgets import QApplication, QTabBar -from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QEvent, QProcess +from PyQt5.QtCore import Qt, QUrl, QEvent from PyQt5.QtGui import QClipboard, QKeyEvent from PyQt5.QtPrintSupport import QPrintDialog, QPrintPreviewDialog from PyQt5.QtWebKitWidgets import QWebPage @@ -43,7 +43,7 @@ from qutebrowser.keyinput import modeman from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils, objreg, utils) from qutebrowser.utils.usertypes import KeyMode -from qutebrowser.misc import editor +from qutebrowser.misc import editor, guiprocess class CommandDispatcher: @@ -944,31 +944,13 @@ class CommandDispatcher: if userscript: self.run_userscript(cmd, *args) else: - proc = QProcess(self._tabbed_browser) - proc.error.connect(self.on_process_error) + proc = guiprocess.GUIProcess(self._win_id, what='command', + verbose=not quiet, + parent=self._tabbed_browser) if detach: - ok = proc.startDetached(cmd, args) - if not ok: - raise cmdexc.CommandError("Error while spawning command") + proc.start_detached(cmd, args) else: proc.start(cmd, args) - if not quiet: - proc.finished.connect(self.on_process_finished) - - @pyqtSlot('QProcess::ProcessError') - def on_process_error(self, error): - """Display an error if a :spawn'ed process failed.""" - msg = qtutils.QPROCESS_ERRORS[error] - message.error(self._win_id, - "Error while spawning command: {}".format(msg), - immediately=True) - - @pyqtSlot(int, 'QProcess::ExitStatus') - def on_process_finished(self, code, _status): - """Display an error if a :spawn'ed process exited with non-0 status.""" - if code != 0: - message.error(self._win_id, "Spawned command exited with status " - "{}!".format(code)) @cmdutils.register(instance='command-dispatcher', scope='window') def home(self): @@ -1202,7 +1184,7 @@ class CommandDispatcher: def on_editing_finished(self, elem, text): """Write the editor text into the form field and clean up tempfile. - Callback for QProcess when the editor was closed. + Callback for GUIProcess when the editor was closed. Args: elem: The WebElementWrapper which was modified. diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index b9b1664b5..4f03a5774 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -24,7 +24,7 @@ import functools import collections from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl, - QTimer, QProcess) + QTimer) from PyQt5.QtGui import QMouseEvent, QClipboard from PyQt5.QtWidgets import QApplication from PyQt5.QtWebKit import QWebElement @@ -35,6 +35,7 @@ from qutebrowser.keyinput import modeman, modeparsers from qutebrowser.browser import webelem from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners from qutebrowser.utils import usertypes, log, qtutils, message, objreg +from qutebrowser.misc import guiprocess ElemTuple = collections.namedtuple('ElemTuple', ['elem', 'label']) @@ -548,18 +549,9 @@ class HintManager(QObject): urlstr = url.toString(QUrl.FullyEncoded | QUrl.RemovePassword) args = context.get_args(urlstr) cmd, *args = args - proc = QProcess(self) - proc.error.connect(self.on_process_error) + proc = guiprocess.GUIProcess(self._win_id, what='command', parent=self) proc.start(cmd, args) - @pyqtSlot('QProcess::ProcessError') - def on_process_error(self, error): - """Display an error if a :spawn'ed process failed.""" - msg = qtutils.QPROCESS_ERRORS[error] - message.error(self._win_id, - "Error while spawning command: {}".format(msg), - immediately=True) - def _resolve_url(self, elem, baseurl): """Resolve a URL and check if we want to keep it. diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py index ceff1dffc..08117f2fd 100644 --- a/qutebrowser/commands/userscripts.py +++ b/qutebrowser/commands/userscripts.py @@ -23,12 +23,12 @@ import os import os.path import tempfile -from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QSocketNotifier, - QProcessEnvironment, QProcess) +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QSocketNotifier -from qutebrowser.utils import message, log, objreg, standarddir, qtutils +from qutebrowser.utils import message, log, objreg, standarddir from qutebrowser.commands import runners, cmdexc from qutebrowser.config import config +from qutebrowser.misc import guiprocess class _QtFIFOReader(QObject): @@ -70,7 +70,7 @@ class _BaseUserscriptRunner(QObject): Attributes: _filepath: The path of the file/FIFO which is being read. - _proc: The QProcess which is being executed. + _proc: The GUIProcess which is being executed. _win_id: The window ID this runner is associated with. Signals: @@ -89,33 +89,29 @@ class _BaseUserscriptRunner(QObject): self._env = None def _run_process(self, cmd, *args, env): - """Start the given command via QProcess. + """Start the given command. Args: cmd: The command to be started. *args: The arguments to hand to the command env: A dictionary of environment variables to add. """ - self._env = env - self._proc = QProcess(self) - procenv = QProcessEnvironment.systemEnvironment() - procenv.insert('QUTE_FIFO', self._filepath) - if env is not None: - for k, v in env.items(): - procenv.insert(k, v) - self._proc.setProcessEnvironment(procenv) - self._proc.error.connect(self.on_proc_error) - self._proc.finished.connect(self.on_proc_finished) + self._env = {'QUTE_FIFO': self._filepath} + self._env.update(env) + self._proc = guiprocess.GUIProcess(self._win_id, 'userscript', + additional_env=self._env, + parent=self) + self._proc.proc.error.connect(self.on_proc_error) + self._proc.proc.finished.connect(self.on_proc_finished) self._proc.start(cmd, args) def _cleanup(self): """Clean up temporary files.""" tempfiles = [self._filepath] - if self._env is not None: - if 'QUTE_HTML' in self._env: - tempfiles.append(self._env['QUTE_HTML']) - if 'QUTE_TEXT' in self._env: - tempfiles.append(self._env['QUTE_TEXT']) + if 'QUTE_HTML' in self._env: + tempfiles.append(self._env['QUTE_HTML']) + if 'QUTE_TEXT' in self._env: + tempfiles.append(self._env['QUTE_TEXT']) for fn in tempfiles: log.procs.debug("Deleting temporary file {}.".format(fn)) try: @@ -151,12 +147,7 @@ class _BaseUserscriptRunner(QObject): def on_proc_error(self, error): """Called when the process encountered an error.""" - msg = qtutils.QPROCESS_ERRORS[error] - # NOTE: Do not replace this with "raise CommandError" as it's - # executed async. - message.error(self._win_id, - "Error while calling userscript: {}".format(msg)) - log.procs.debug("Userscript process error: {} - {}".format(error, msg)) + raise NotImplementedError class _POSIXUserscriptRunner(_BaseUserscriptRunner): @@ -195,12 +186,10 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner): def on_proc_finished(self): """Interrupt the reader when the process finished.""" - log.procs.debug("Userscript process finished.") self.finish() def on_proc_error(self, error): """Interrupt the reader when the process had an error.""" - super().on_proc_error(error) self.finish() def finish(self): @@ -245,7 +234,6 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner): def on_proc_finished(self): """Read back the commands when the process finished.""" - log.procs.debug("Userscript process finished.") try: with open(self._filepath, 'r', encoding='utf-8') as f: for line in f: @@ -257,7 +245,6 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner): def on_proc_error(self, error): """Clean up when the process had an error.""" - super().on_proc_error(error) self._cleanup() self.finished.emit() diff --git a/qutebrowser/misc/editor.py b/qutebrowser/misc/editor.py index 32d108490..8294e377c 100644 --- a/qutebrowser/misc/editor.py +++ b/qutebrowser/misc/editor.py @@ -22,10 +22,11 @@ import os import tempfile -from PyQt5.QtCore import pyqtSignal, QProcess, QObject +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QProcess from qutebrowser.config import config -from qutebrowser.utils import message, log, qtutils +from qutebrowser.utils import message, log +from qutebrowser.misc import guiprocess class ExternalEditor(QObject): @@ -36,7 +37,7 @@ class ExternalEditor(QObject): _text: The current text before the editor is opened. _oshandle: The OS level handle to the tmpfile. _filehandle: The file handle to the tmpfile. - _proc: The QProcess of the editor. + _proc: The GUIProcess of the editor. _win_id: The window ID the ExternalEditor is associated with. """ @@ -69,15 +70,10 @@ class ExternalEditor(QObject): log.procs.debug("Editor closed") if exitstatus != QProcess.NormalExit: # No error/cleanup here, since we already handle this in - # on_proc_error + # on_proc_error. return try: if exitcode != 0: - # NOTE: Do not replace this with "raise CommandError" as it's - # executed async. - message.error( - self._win_id, "Editor did quit abnormally (status " - "{})!".format(exitcode)) return encoding = config.get('general', 'editor-encoding') try: @@ -94,13 +90,8 @@ class ExternalEditor(QObject): finally: self._cleanup() - def on_proc_error(self, error): - """Display an error message and clean up when editor crashed.""" - msg = qtutils.QPROCESS_ERRORS[error] - # NOTE: Do not replace this with "raise CommandError" as it's - # executed async. - message.error(self._win_id, - "Error while calling editor: {}".format(msg)) + @pyqtSlot(QProcess.ProcessError) + def on_proc_error(self, _err): self._cleanup() def edit(self, text): @@ -123,9 +114,10 @@ class ExternalEditor(QObject): message.error(self._win_id, "Failed to create initial file: " "{}".format(e)) return - self._proc = QProcess(self) - self._proc.finished.connect(self.on_proc_closed) - self._proc.error.connect(self.on_proc_error) + self._proc = guiprocess.GUIProcess(self._win_id, what='editor', + parent=self) + self._proc.proc.finished.connect(self.on_proc_closed) + self._proc.proc.error.connect(self.on_proc_error) editor = config.get('general', 'editor') executable = editor[0] args = [self._filename if arg == '{}' else arg for arg in editor[1:]] diff --git a/qutebrowser/misc/guiprocess.py b/qutebrowser/misc/guiprocess.py new file mode 100644 index 000000000..7497c3457 --- /dev/null +++ b/qutebrowser/misc/guiprocess.py @@ -0,0 +1,131 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2015 Florian Bruhin (The Compiler) +# +# 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 . + +"""A QProcess which shows notifications in the GUI.""" + +import shlex + +from PyQt5.QtCore import pyqtSlot, QProcess, QIODevice, QProcessEnvironment + +from qutebrowser.utils import message, log + +# A mapping of QProcess::ErrorCode's to human-readable strings. + +ERROR_STRINGS = { + QProcess.FailedToStart: "The process failed to start.", + QProcess.Crashed: "The process crashed.", + QProcess.Timedout: "The last waitFor...() function timed out.", + QProcess.WriteError: ("An error occurred when attempting to write to the " + "process."), + QProcess.ReadError: ("An error occurred when attempting to read from the " + "process."), + QProcess.UnknownError: "An unknown error occurred.", +} + + +class GUIProcess: + + """An external process which shows notifications in the GUI. + + Args: + proc: The underlying QProcess. + _win_id: The window ID this process is used in. + _what: What kind of thing is spawned (process/editor/userscript/...). + Used in messages. + _verbose: Whether to show more messages. + _started: Whether the underlying process is started. + """ + + def __init__(self, win_id, what, *, verbose=False, additional_env=None, + parent=None): + self._win_id = win_id + self._what = what + self._verbose = verbose + self._started = False + + self.proc = QProcess(parent) + self.proc.error.connect(self.on_error) + self.proc.finished.connect(self.on_finished) + self.proc.started.connect(self.on_started) + + if additional_env is not None: + procenv = QProcessEnvironment.systemEnvironment() + for k, v in additional_env.items(): + procenv.insert(k, v) + self.proc.setProcessEnvironment(procenv) + + @pyqtSlot(QProcess.ProcessError) + def on_error(self, error): + """Show a message if there was an error while spawning.""" + msg = ERROR_STRINGS[error] + message.error(self._win_id, "Error while spawning {}: {}".format( + self._what, msg), immediately=True) + + @pyqtSlot(int, QProcess.ExitStatus) + def on_finished(self, code, status): + """Show a message when the process finished.""" + self._started = False + log.procs.debug("Process finished with code {}, status {}.".format( + code, status)) + if status == QProcess.CrashExit: + message.error(self._win_id, + "{} crashed!".format(self._what.capitalize()), + immediately=True) + elif status == QProcess.NormalExit and code == 0: + if self._verbose: + message.info(self._win_id, "{} exited successfully.".format( + self._what.capitalize())) + else: + assert status == QProcess.NormalExit + message.error(self._win_id, "{} exited with status {}.".format( + self._what.capitalize(), code)) + + @pyqtSlot() + def on_started(self): + """Called when the process started successfully.""" + log.procs.debug("Process started.") + assert not self._started + self._started = True + + def _pre_start(self, cmd, args): + """Things to do before starting a QProcess.""" + if self._started: + raise ValueError("Trying to start a running QProcess!") + if self._verbose: + fake_cmdline = ' '.join(shlex.quote(e) for e in [cmd] + list(args)) + message.info(self._win_id, 'Executing: ' + fake_cmdline) + + def start(self, cmd, args, mode=QIODevice.ReadWrite): + """Convenience wrapper around QProcess::start.""" + log.procs.debug("Starting process.") + self._pre_start(cmd, args) + self.proc.start(cmd, args, mode) + + def start_detached(self, cmd, args, cwd=None): + """Convenience wrapper around QProcess::startDetached.""" + log.procs.debug("Starting detached.") + self._pre_start(cmd, args) + ok = self.proc.startDetached(cmd, args, cwd) + + if ok: + log.procs.debug("Process started.") + self._started = True + else: + message.error(self._win_id, "Error while spawning {}: {}.".format( + self._what, self.proc.error()), immediately=True) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 42bca6bec..6573306ab 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -36,7 +36,7 @@ import distutils.version # pylint: disable=no-name-in-module,import-error import contextlib from PyQt5.QtCore import (qVersion, QEventLoop, QDataStream, QByteArray, - QIODevice, QSaveFile, QProcess) + QIODevice, QSaveFile) from PyQt5.QtWidgets import QApplication @@ -400,17 +400,3 @@ class EventLoop(QEventLoop): self._executing = True super().exec_(flags) self._executing = False - - -# A mapping of QProcess::ErrorCode's to human-readable strings. - -QPROCESS_ERRORS = { - QProcess.FailedToStart: "The process failed to start.", - QProcess.Crashed: "The process crashed.", - QProcess.Timedout: "The last waitFor...() function timed out.", - QProcess.WriteError: ("An error occurred when attempting to write to the " - "process."), - QProcess.ReadError: ("An error occurred when attempting to read from the " - "process."), - QProcess.UnknownError: "An unknown error occurred.", -} diff --git a/tests/misc/test_editor.py b/tests/misc/test_editor.py index abc2eabb0..a2ecf230a 100644 --- a/tests/misc/test_editor.py +++ b/tests/misc/test_editor.py @@ -42,7 +42,7 @@ class TestArg: @pytest.yield_fixture(autouse=True) def setup(self, monkeypatch, stubs): - monkeypatch.setattr('qutebrowser.misc.editor.QProcess', + monkeypatch.setattr('qutebrowser.misc.editor.guiprocess.QProcess', stubs.FakeQProcess()) self.editor = editor.ExternalEditor(0) yield @@ -101,7 +101,7 @@ class TestFileHandling: def setup(self, monkeypatch, stubs, config_stub): monkeypatch.setattr('qutebrowser.misc.editor.message', stubs.MessageModule()) - monkeypatch.setattr('qutebrowser.misc.editor.QProcess', + monkeypatch.setattr('qutebrowser.misc.editor.guiprocess.QProcess', stubs.FakeQProcess()) config_stub.data = {'general': {'editor': [''], 'editor-encoding': 'utf-8'}} @@ -148,7 +148,7 @@ class TestModifyTests: @pytest.fixture(autouse=True) def setup(self, monkeypatch, stubs, config_stub): - monkeypatch.setattr('qutebrowser.misc.editor.QProcess', + monkeypatch.setattr('qutebrowser.misc.editor.guiprocess.QProcess', stubs.FakeQProcess()) config_stub.data = {'general': {'editor': [''], 'editor-encoding': 'utf-8'}} @@ -220,7 +220,7 @@ class TestErrorMessage: @pytest.yield_fixture(autouse=True) def setup(self, monkeypatch, stubs, config_stub): - monkeypatch.setattr('qutebrowser.misc.editor.QProcess', + monkeypatch.setattr('qutebrowser.misc.editor.guiprocess.QProcess', stubs.FakeQProcess()) monkeypatch.setattr('qutebrowser.misc.editor.message', stubs.MessageModule()) From 62d2018695db8865b409dd23591ca6e5225d1258 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2015 18:31:25 +0200 Subject: [PATCH 051/146] Add cmd/args/started to GUIProcess. --- qutebrowser/misc/guiprocess.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/qutebrowser/misc/guiprocess.py b/qutebrowser/misc/guiprocess.py index 7497c3457..1506c1da6 100644 --- a/qutebrowser/misc/guiprocess.py +++ b/qutebrowser/misc/guiprocess.py @@ -45,11 +45,13 @@ class GUIProcess: Args: proc: The underlying QProcess. + cmd: The command which was started. + args: A list of arguments which gets passed. + started: Whether the underlying process is started. _win_id: The window ID this process is used in. _what: What kind of thing is spawned (process/editor/userscript/...). Used in messages. _verbose: Whether to show more messages. - _started: Whether the underlying process is started. """ def __init__(self, win_id, what, *, verbose=False, additional_env=None, @@ -57,7 +59,9 @@ class GUIProcess: self._win_id = win_id self._what = what self._verbose = verbose - self._started = False + self.started = False + self.cmd = None + self.args = None self.proc = QProcess(parent) self.proc.error.connect(self.on_error) @@ -80,7 +84,7 @@ class GUIProcess: @pyqtSlot(int, QProcess.ExitStatus) def on_finished(self, code, status): """Show a message when the process finished.""" - self._started = False + self.started = False log.procs.debug("Process finished with code {}, status {}.".format( code, status)) if status == QProcess.CrashExit: @@ -100,13 +104,15 @@ class GUIProcess: def on_started(self): """Called when the process started successfully.""" log.procs.debug("Process started.") - assert not self._started - self._started = True + assert not self.started + self.started = True def _pre_start(self, cmd, args): - """Things to do before starting a QProcess.""" - if self._started: + """Prepare starting of a QProcess.""" + if self.started: raise ValueError("Trying to start a running QProcess!") + self.cmd = cmd + self.args = args if self._verbose: fake_cmdline = ' '.join(shlex.quote(e) for e in [cmd] + list(args)) message.info(self._win_id, 'Executing: ' + fake_cmdline) @@ -125,7 +131,7 @@ class GUIProcess: if ok: log.procs.debug("Process started.") - self._started = True + self.started = True else: message.error(self._win_id, "Error while spawning {}: {}.".format( self._what, self.proc.error()), immediately=True) From 1f67353a40fe6943dc3a86ee57580cf6b482c558 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2015 18:34:57 +0200 Subject: [PATCH 052/146] Adjust editor tests for GUIProcess. --- tests/misc/test_editor.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/misc/test_editor.py b/tests/misc/test_editor.py index a2ecf230a..25925f7e1 100644 --- a/tests/misc/test_editor.py +++ b/tests/misc/test_editor.py @@ -60,7 +60,7 @@ class TestArg: stubbed_config.data = { 'general': {'editor': ['bin'], 'editor-encoding': 'utf-8'}} self.editor.edit("") - self.editor._proc.start.assert_called_with("bin", []) + self.editor._proc.proc.start.assert_called_with("bin", []) def test_start_args(self, stubbed_config): """Test starting editor with static arguments.""" @@ -68,7 +68,7 @@ class TestArg: 'general': {'editor': ['bin', 'foo', 'bar'], 'editor-encoding': 'utf-8'}} self.editor.edit("") - self.editor._proc.start.assert_called_with("bin", ["foo", "bar"]) + self.editor._proc.proc.start.assert_called_with("bin", ["foo", "bar"]) def test_placeholder(self, stubbed_config): """Test starting editor with placeholder argument.""" @@ -77,7 +77,7 @@ class TestArg: 'editor-encoding': 'utf-8'}} self.editor.edit("") filename = self.editor._filename - self.editor._proc.start.assert_called_with( + self.editor._proc.proc.start.assert_called_with( "bin", ["foo", filename, "bar"]) def test_in_arg_placeholder(self, stubbed_config): @@ -86,7 +86,7 @@ class TestArg: 'general': {'editor': ['bin', 'foo{}bar'], 'editor-encoding': 'utf-8'}} self.editor.edit("") - self.editor._proc.start.assert_called_with("bin", ["foo{}bar"]) + self.editor._proc.proc.start.assert_called_with("bin", ["foo{}bar"]) class TestFileHandling: @@ -113,7 +113,7 @@ class TestFileHandling: self.editor.edit("") filename = self.editor._filename assert os.path.exists(filename) - self.editor.on_proc_closed(0, QProcess.NormalExit) + self.editor._proc.proc.finished.emit(0, QProcess.NormalExit) assert not os.path.exists(filename) def test_file_handling_closed_error(self, caplog): @@ -122,7 +122,7 @@ class TestFileHandling: filename = self.editor._filename assert os.path.exists(filename) with caplog.atLevel(logging.ERROR): - self.editor.on_proc_closed(1, QProcess.NormalExit) + self.editor._proc.proc.finished.emit(1, QProcess.NormalExit) assert len(caplog.records()) == 2 assert not os.path.exists(filename) @@ -132,9 +132,9 @@ class TestFileHandling: filename = self.editor._filename assert os.path.exists(filename) with caplog.atLevel(logging.ERROR): - self.editor.on_proc_error(QProcess.Crashed) + self.editor._proc.proc.error.emit(QProcess.Crashed) assert len(caplog.records()) == 2 - self.editor.on_proc_closed(0, QProcess.CrashExit) + self.editor._proc.proc.finished.emit(0, QProcess.CrashExit) assert not os.path.exists(filename) @@ -182,7 +182,7 @@ class TestModifyTests: self.editor.edit("") assert self._read() == "" self._write("Hello") - self.editor.on_proc_closed(0, QProcess.NormalExit) + self.editor._proc.proc.finished.emit(0, QProcess.NormalExit) self.editor.editing_finished.emit.assert_called_with("Hello") def test_simple_input(self): @@ -190,7 +190,7 @@ class TestModifyTests: self.editor.edit("Hello") assert self._read() == "Hello" self._write("World") - self.editor.on_proc_closed(0, QProcess.NormalExit) + self.editor._proc.proc.finished.emit(0, QProcess.NormalExit) self.editor.editing_finished.emit.assert_called_with("World") def test_umlaut(self): @@ -198,7 +198,7 @@ class TestModifyTests: self.editor.edit("Hällö Wörld") assert self._read() == "Hällö Wörld" self._write("Überprüfung") - self.editor.on_proc_closed(0, QProcess.NormalExit) + self.editor._proc.proc.finished.emit(0, QProcess.NormalExit) self.editor.editing_finished.emit.assert_called_with("Überprüfung") def test_unicode(self): From ad401e035f2f5b33c4bab0782c2bfd346aba5840 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2015 19:10:01 +0200 Subject: [PATCH 053/146] Proxy QProcess signals. --- qutebrowser/commands/userscripts.py | 4 +-- qutebrowser/misc/editor.py | 4 +-- qutebrowser/misc/guiprocess.py | 39 ++++++++++++++++++++--------- tests/misc/test_editor.py | 22 ++++++++-------- 4 files changed, 42 insertions(+), 27 deletions(-) diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py index 08117f2fd..ffd71bbbb 100644 --- a/qutebrowser/commands/userscripts.py +++ b/qutebrowser/commands/userscripts.py @@ -101,8 +101,8 @@ class _BaseUserscriptRunner(QObject): self._proc = guiprocess.GUIProcess(self._win_id, 'userscript', additional_env=self._env, parent=self) - self._proc.proc.error.connect(self.on_proc_error) - self._proc.proc.finished.connect(self.on_proc_finished) + self._proc.error.connect(self.on_proc_error) + self._proc.finished.connect(self.on_proc_finished) self._proc.start(cmd, args) def _cleanup(self): diff --git a/qutebrowser/misc/editor.py b/qutebrowser/misc/editor.py index 8294e377c..b5a9e5995 100644 --- a/qutebrowser/misc/editor.py +++ b/qutebrowser/misc/editor.py @@ -116,8 +116,8 @@ class ExternalEditor(QObject): return self._proc = guiprocess.GUIProcess(self._win_id, what='editor', parent=self) - self._proc.proc.finished.connect(self.on_proc_closed) - self._proc.proc.error.connect(self.on_proc_error) + self._proc.finished.connect(self.on_proc_closed) + self._proc.error.connect(self.on_proc_error) editor = config.get('general', 'editor') executable = editor[0] args = [self._filename if arg == '{}' else arg for arg in editor[1:]] diff --git a/qutebrowser/misc/guiprocess.py b/qutebrowser/misc/guiprocess.py index 1506c1da6..04b9f27d9 100644 --- a/qutebrowser/misc/guiprocess.py +++ b/qutebrowser/misc/guiprocess.py @@ -21,7 +21,8 @@ import shlex -from PyQt5.QtCore import pyqtSlot, QProcess, QIODevice, QProcessEnvironment +from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QObject, QProcess, + QProcessEnvironment) from qutebrowser.utils import message, log @@ -39,23 +40,31 @@ ERROR_STRINGS = { } -class GUIProcess: +class GUIProcess(QObject): """An external process which shows notifications in the GUI. Args: - proc: The underlying QProcess. cmd: The command which was started. args: A list of arguments which gets passed. started: Whether the underlying process is started. + _proc: The underlying QProcess. _win_id: The window ID this process is used in. _what: What kind of thing is spawned (process/editor/userscript/...). Used in messages. _verbose: Whether to show more messages. + + Signals: + error/finished/started signals proxied from QProcess. """ + error = pyqtSignal(QProcess.ProcessError) + finished = pyqtSignal(int, QProcess.ExitStatus) + started = pyqtSignal() + def __init__(self, win_id, what, *, verbose=False, additional_env=None, parent=None): + super().__init__(parent) self._win_id = win_id self._what = what self._verbose = verbose @@ -63,16 +72,19 @@ class GUIProcess: self.cmd = None self.args = None - self.proc = QProcess(parent) - self.proc.error.connect(self.on_error) - self.proc.finished.connect(self.on_finished) - self.proc.started.connect(self.on_started) + self._proc = QProcess(self) + self._proc.error.connect(self.on_error) + self._proc.error.connect(self.error) + self._proc.finished.connect(self.on_finished) + self._proc.finished.connect(self.finished) + self._proc.started.connect(self.on_started) + self._proc.started.connect(self.started) if additional_env is not None: procenv = QProcessEnvironment.systemEnvironment() for k, v in additional_env.items(): procenv.insert(k, v) - self.proc.setProcessEnvironment(procenv) + self._proc.setProcessEnvironment(procenv) @pyqtSlot(QProcess.ProcessError) def on_error(self, error): @@ -117,21 +129,24 @@ class GUIProcess: fake_cmdline = ' '.join(shlex.quote(e) for e in [cmd] + list(args)) message.info(self._win_id, 'Executing: ' + fake_cmdline) - def start(self, cmd, args, mode=QIODevice.ReadWrite): + def start(self, cmd, args, mode=None): """Convenience wrapper around QProcess::start.""" log.procs.debug("Starting process.") self._pre_start(cmd, args) - self.proc.start(cmd, args, mode) + if mode is None: + self._proc.start(cmd, args) + else: + self._proc.start(cmd, args, mode) def start_detached(self, cmd, args, cwd=None): """Convenience wrapper around QProcess::startDetached.""" log.procs.debug("Starting detached.") self._pre_start(cmd, args) - ok = self.proc.startDetached(cmd, args, cwd) + ok = self._proc.startDetached(cmd, args, cwd) if ok: log.procs.debug("Process started.") self.started = True else: message.error(self._win_id, "Error while spawning {}: {}.".format( - self._what, self.proc.error()), immediately=True) + self._what, self._proc.error()), immediately=True) diff --git a/tests/misc/test_editor.py b/tests/misc/test_editor.py index 25925f7e1..10142b525 100644 --- a/tests/misc/test_editor.py +++ b/tests/misc/test_editor.py @@ -60,7 +60,7 @@ class TestArg: stubbed_config.data = { 'general': {'editor': ['bin'], 'editor-encoding': 'utf-8'}} self.editor.edit("") - self.editor._proc.proc.start.assert_called_with("bin", []) + self.editor._proc._proc.start.assert_called_with("bin", []) def test_start_args(self, stubbed_config): """Test starting editor with static arguments.""" @@ -68,7 +68,7 @@ class TestArg: 'general': {'editor': ['bin', 'foo', 'bar'], 'editor-encoding': 'utf-8'}} self.editor.edit("") - self.editor._proc.proc.start.assert_called_with("bin", ["foo", "bar"]) + self.editor._proc._proc.start.assert_called_with("bin", ["foo", "bar"]) def test_placeholder(self, stubbed_config): """Test starting editor with placeholder argument.""" @@ -77,7 +77,7 @@ class TestArg: 'editor-encoding': 'utf-8'}} self.editor.edit("") filename = self.editor._filename - self.editor._proc.proc.start.assert_called_with( + self.editor._proc._proc.start.assert_called_with( "bin", ["foo", filename, "bar"]) def test_in_arg_placeholder(self, stubbed_config): @@ -86,7 +86,7 @@ class TestArg: 'general': {'editor': ['bin', 'foo{}bar'], 'editor-encoding': 'utf-8'}} self.editor.edit("") - self.editor._proc.proc.start.assert_called_with("bin", ["foo{}bar"]) + self.editor._proc._proc.start.assert_called_with("bin", ["foo{}bar"]) class TestFileHandling: @@ -113,7 +113,7 @@ class TestFileHandling: self.editor.edit("") filename = self.editor._filename assert os.path.exists(filename) - self.editor._proc.proc.finished.emit(0, QProcess.NormalExit) + self.editor._proc.finished.emit(0, QProcess.NormalExit) assert not os.path.exists(filename) def test_file_handling_closed_error(self, caplog): @@ -122,7 +122,7 @@ class TestFileHandling: filename = self.editor._filename assert os.path.exists(filename) with caplog.atLevel(logging.ERROR): - self.editor._proc.proc.finished.emit(1, QProcess.NormalExit) + self.editor._proc.finished.emit(1, QProcess.NormalExit) assert len(caplog.records()) == 2 assert not os.path.exists(filename) @@ -132,9 +132,9 @@ class TestFileHandling: filename = self.editor._filename assert os.path.exists(filename) with caplog.atLevel(logging.ERROR): - self.editor._proc.proc.error.emit(QProcess.Crashed) + self.editor._proc.error.emit(QProcess.Crashed) assert len(caplog.records()) == 2 - self.editor._proc.proc.finished.emit(0, QProcess.CrashExit) + self.editor._proc.finished.emit(0, QProcess.CrashExit) assert not os.path.exists(filename) @@ -182,7 +182,7 @@ class TestModifyTests: self.editor.edit("") assert self._read() == "" self._write("Hello") - self.editor._proc.proc.finished.emit(0, QProcess.NormalExit) + self.editor._proc.finished.emit(0, QProcess.NormalExit) self.editor.editing_finished.emit.assert_called_with("Hello") def test_simple_input(self): @@ -190,7 +190,7 @@ class TestModifyTests: self.editor.edit("Hello") assert self._read() == "Hello" self._write("World") - self.editor._proc.proc.finished.emit(0, QProcess.NormalExit) + self.editor._proc.finished.emit(0, QProcess.NormalExit) self.editor.editing_finished.emit.assert_called_with("World") def test_umlaut(self): @@ -198,7 +198,7 @@ class TestModifyTests: self.editor.edit("Hällö Wörld") assert self._read() == "Hällö Wörld" self._write("Überprüfung") - self.editor._proc.proc.finished.emit(0, QProcess.NormalExit) + self.editor._proc.finished.emit(0, QProcess.NormalExit) self.editor.editing_finished.emit.assert_called_with("Überprüfung") def test_unicode(self): From 8a87b5d3573ea1ebd71f9610bc1767207c9ea4c7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2015 20:09:41 +0200 Subject: [PATCH 054/146] Rename GUIProcess.started to _started. It's unneeded for this to be public, and it conflicts with the pyqtSignal. --- qutebrowser/misc/guiprocess.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/qutebrowser/misc/guiprocess.py b/qutebrowser/misc/guiprocess.py index 04b9f27d9..53cdea8c8 100644 --- a/qutebrowser/misc/guiprocess.py +++ b/qutebrowser/misc/guiprocess.py @@ -47,7 +47,7 @@ class GUIProcess(QObject): Args: cmd: The command which was started. args: A list of arguments which gets passed. - started: Whether the underlying process is started. + _started: Whether the underlying process is started. _proc: The underlying QProcess. _win_id: The window ID this process is used in. _what: What kind of thing is spawned (process/editor/userscript/...). @@ -68,7 +68,7 @@ class GUIProcess(QObject): self._win_id = win_id self._what = what self._verbose = verbose - self.started = False + self._started = False self.cmd = None self.args = None @@ -96,7 +96,7 @@ class GUIProcess(QObject): @pyqtSlot(int, QProcess.ExitStatus) def on_finished(self, code, status): """Show a message when the process finished.""" - self.started = False + self._started = False log.procs.debug("Process finished with code {}, status {}.".format( code, status)) if status == QProcess.CrashExit: @@ -116,12 +116,12 @@ class GUIProcess(QObject): def on_started(self): """Called when the process started successfully.""" log.procs.debug("Process started.") - assert not self.started - self.started = True + assert not self._started + self._started = True def _pre_start(self, cmd, args): """Prepare starting of a QProcess.""" - if self.started: + if self._started: raise ValueError("Trying to start a running QProcess!") self.cmd = cmd self.args = args @@ -146,7 +146,7 @@ class GUIProcess(QObject): if ok: log.procs.debug("Process started.") - self.started = True + self._started = True else: message.error(self._win_id, "Error while spawning {}: {}.".format( self._what, self._proc.error()), immediately=True) From 84dacc9bc85621e810d7f8dd2639c9129c3eb9a2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2015 20:11:44 +0200 Subject: [PATCH 055/146] Remove double output for :spawn. --- qutebrowser/browser/commands.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index e73255eab..105d70832 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -937,9 +937,6 @@ class CommandDispatcher: """ log.procs.debug("Executing: {}, userscript={}".format( args, userscript)) - if not quiet: - fake_cmdline = ' '.join(shlex.quote(arg) for arg in args) - message.info(win_id, 'Executing: ' + fake_cmdline) cmd, *args = args if userscript: self.run_userscript(cmd, *args) From 5828bbafe9fb73625acbc16ea7ec4c719a840dcf Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2015 20:19:31 +0200 Subject: [PATCH 056/146] Add -v (not -q) to :spawn and make it work with -u. --- qutebrowser/browser/commands.py | 14 ++++++++------ qutebrowser/commands/userscripts.py | 24 ++++++++++++++---------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 105d70832..529a10663 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -922,7 +922,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window', win_id='win_id') - def spawn(self, win_id, userscript=False, quiet=False, detach=False, + def spawn(self, win_id, userscript=False, verbose=False, detach=False, *args): """Spawn a command in a shell. @@ -931,7 +931,7 @@ class CommandDispatcher: Args: userscript: Run the command as an userscript. - quiet: Don't print the commandline being executed. + verbose: Show notifications when the command started/exited. detach: Whether the command should be detached from qutebrowser. *args: The commandline to execute. """ @@ -939,10 +939,10 @@ class CommandDispatcher: args, userscript)) cmd, *args = args if userscript: - self.run_userscript(cmd, *args) + self.run_userscript(cmd, *args, verbose=verbose) else: proc = guiprocess.GUIProcess(self._win_id, what='command', - verbose=not quiet, + verbose=verbose, parent=self._tabbed_browser) if detach: proc.start_detached(cmd, args) @@ -956,12 +956,13 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window', deprecated='Use :spawn --userscript instead!') - def run_userscript(self, cmd, *args: {'nargs': '*'}): + def run_userscript(self, cmd, *args: {'nargs': '*'}, verbose=False): """Run an userscript given as argument. Args: cmd: The userscript to run. args: Arguments to pass to the userscript. + verbose: Show notifications when the command started/exited. """ cmd = os.path.expanduser(cmd) env = { @@ -989,7 +990,8 @@ class CommandDispatcher: env['QUTE_URL'] = url.toString(QUrl.FullyEncoded) env.update(userscripts.store_source(mainframe)) - userscripts.run(cmd, *args, win_id=self._win_id, env=env) + userscripts.run(cmd, *args, win_id=self._win_id, env=env, + verbose=verbose) @cmdutils.register(instance='command-dispatcher', scope='window') def quickmark_save(self): diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py index ffd71bbbb..136b23e57 100644 --- a/qutebrowser/commands/userscripts.py +++ b/qutebrowser/commands/userscripts.py @@ -88,19 +88,20 @@ class _BaseUserscriptRunner(QObject): self._proc = None self._env = None - def _run_process(self, cmd, *args, env): + def _run_process(self, cmd, *args, env, verbose): """Start the given command. Args: cmd: The command to be started. *args: The arguments to hand to the command env: A dictionary of environment variables to add. + verbose: Show notifications when the command started/exited. """ self._env = {'QUTE_FIFO': self._filepath} self._env.update(env) self._proc = guiprocess.GUIProcess(self._win_id, 'userscript', additional_env=self._env, - parent=self) + verbose=verbose, parent=self) self._proc.error.connect(self.on_proc_error) self._proc.finished.connect(self.on_proc_finished) self._proc.start(cmd, args) @@ -126,7 +127,7 @@ class _BaseUserscriptRunner(QObject): self._proc = None self._env = None - def run(self, cmd, *args, env=None): + def run(self, cmd, *args, env=None, verbose=False): """Run the userscript given. Needs to be overridden by subclasses. @@ -135,6 +136,7 @@ class _BaseUserscriptRunner(QObject): cmd: The command to be started. *args: The arguments to hand to the command env: A dictionary of environment variables to add. + verbose: Show notifications when the command started/exited. """ raise NotImplementedError @@ -164,7 +166,7 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner): super().__init__(win_id, parent) self._reader = None - def run(self, cmd, *args, env=None): + def run(self, cmd, *args, env=None, verbose=False): try: # tempfile.mktemp is deprecated and discouraged, but we use it here # to create a FIFO since the only other alternative would be to @@ -182,7 +184,7 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner): self._reader = _QtFIFOReader(self._filepath) self._reader.got_line.connect(self.got_cmd) - self._run_process(cmd, *args, env=env) + self._run_process(cmd, *args, env=env, verbose=verbose) def on_proc_finished(self): """Interrupt the reader when the process finished.""" @@ -248,14 +250,14 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner): self._cleanup() self.finished.emit() - def run(self, cmd, *args, env=None): + def run(self, cmd, *args, env=None, verbose=False): try: self._oshandle, self._filepath = tempfile.mkstemp(text=True) except OSError as e: message.error(self._win_id, "Error while creating tempfile: " "{}".format(e)) return - self._run_process(cmd, *args, env=env) + self._run_process(cmd, *args, env=env, verbose=verbose) class _DummyUserscriptRunner: @@ -271,8 +273,9 @@ class _DummyUserscriptRunner: finished = pyqtSignal() - def run(self, _cmd, *_args, _env=None): + def run(self, cmd, *args, env=None, verbose=False): """Print an error as userscripts are not supported.""" + # pylint: disable=unused-argument,unused-variable self.finished.emit() raise cmdexc.CommandError( "Userscripts are not supported on this platform!") @@ -319,7 +322,7 @@ def store_source(frame): return env -def run(cmd, *args, win_id, env): +def run(cmd, *args, win_id, env, verbose=False): """Convenience method to run an userscript. Args: @@ -327,6 +330,7 @@ def run(cmd, *args, win_id, env): *args: The arguments to pass to the userscript. win_id: The window id the userscript is executed in. env: A dictionary of variables to add to the process environment. + verbose: Show notifications when the command started/exited. """ tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) @@ -339,6 +343,6 @@ def run(cmd, *args, win_id, env): user_agent = config.get('network', 'user-agent') if user_agent is not None: env['QUTE_USER_AGENT'] = user_agent - runner.run(cmd, *args, env=env) + runner.run(cmd, *args, env=env, verbose=verbose) runner.finished.connect(commandrunner.deleteLater) runner.finished.connect(runner.deleteLater) From 6736f6a3f28f0179a60ff4360e74d06f53191ac7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2015 20:25:24 +0200 Subject: [PATCH 057/146] Regenerate docs. --- CHANGELOG.asciidoc | 1 + doc/help/commands.asciidoc | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 890636b3a..a41732c2c 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -35,6 +35,7 @@ Added - New setting `tabs -> mousewheel-tab-switching` to control mousewheel behavior on the tab bar. - New arguments `--top-navigate` and `--bottom-navigate` (`-t`/`-b`) for `:scroll-page` to specify a navigation action (e.g. automatically go to the next page when arriving at the bottom). - New flag `-d`/`--detach` for `:spawn` to detach the spawned process so it's not closed when qutebrowser is. +- New flag `-v`/`--verbose` for `:spawn` to print informations when the process started/exited successfully. - New (hidden) command `:follow-selected` (bound to `Enter`/`Ctrl-Enter` by default) to follow the link which is currently selected (e.g. after searching via `/`). - New setting `ui -> modal-js-dialog` to use the standard modal dialogs for javascript questions instead of using the statusbar. - New setting `colors -> webpage.bg` to set the background color to use for websites which don't set one. diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 5a6ce06bf..5827a8909 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -512,7 +512,7 @@ Preset the statusbar to some text. [[spawn]] === spawn -Syntax: +:spawn [*--userscript*] [*--quiet*] 'args' ['args' ...]+ +Syntax: +:spawn [*--userscript*] [*--verbose*] [*--detach*] 'args' ['args' ...]+ Spawn a command in a shell. @@ -523,7 +523,8 @@ Note the {url} variable which gets replaced by the current URL might be useful h ==== optional arguments * +*-u*+, +*--userscript*+: Run the command as an userscript. -* +*-q*+, +*--quiet*+: Don't print the commandline being executed. +* +*-v*+, +*--verbose*+: Show notifications when the command started/exited. +* +*-d*+, +*--detach*+: Whether the command should be detached from qutebrowser. [[stop]] === stop From d3b727d0c7a66e85ecb58595c4a04a293f106e34 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2015 20:32:27 +0200 Subject: [PATCH 058/146] Fix lint. --- qutebrowser/browser/commands.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 529a10663..41835d53b 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -21,7 +21,6 @@ import re import os -import shlex import posixpath import functools import xml.etree.ElementTree @@ -920,10 +919,8 @@ class CommandDispatcher: finally: self._tabbed_browser.setUpdatesEnabled(True) - @cmdutils.register(instance='command-dispatcher', scope='window', - win_id='win_id') - def spawn(self, win_id, userscript=False, verbose=False, detach=False, - *args): + @cmdutils.register(instance='command-dispatcher', scope='window') + def spawn(self, userscript=False, verbose=False, detach=False, *args): """Spawn a command in a shell. Note the {url} variable which gets replaced by the current URL might be From fc5349e1dcdba6574c8f45f9b9774d2d2d2596f9 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2015 23:02:18 +0200 Subject: [PATCH 059/146] Change FakeQProcess stub to a function with spec. --- tests/misc/test_editor.py | 8 ++++---- tests/stubs.py | 23 +++++++---------------- 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/tests/misc/test_editor.py b/tests/misc/test_editor.py index 10142b525..773769f80 100644 --- a/tests/misc/test_editor.py +++ b/tests/misc/test_editor.py @@ -43,7 +43,7 @@ class TestArg: @pytest.yield_fixture(autouse=True) def setup(self, monkeypatch, stubs): monkeypatch.setattr('qutebrowser.misc.editor.guiprocess.QProcess', - stubs.FakeQProcess()) + stubs.fake_qprocess()) self.editor = editor.ExternalEditor(0) yield self.editor._cleanup() # pylint: disable=protected-access @@ -102,7 +102,7 @@ class TestFileHandling: monkeypatch.setattr('qutebrowser.misc.editor.message', stubs.MessageModule()) monkeypatch.setattr('qutebrowser.misc.editor.guiprocess.QProcess', - stubs.FakeQProcess()) + stubs.fake_qprocess()) config_stub.data = {'general': {'editor': [''], 'editor-encoding': 'utf-8'}} monkeypatch.setattr('qutebrowser.misc.editor.config', config_stub) @@ -149,7 +149,7 @@ class TestModifyTests: @pytest.fixture(autouse=True) def setup(self, monkeypatch, stubs, config_stub): monkeypatch.setattr('qutebrowser.misc.editor.guiprocess.QProcess', - stubs.FakeQProcess()) + stubs.fake_qprocess()) config_stub.data = {'general': {'editor': [''], 'editor-encoding': 'utf-8'}} monkeypatch.setattr('qutebrowser.misc.editor.config', config_stub) @@ -221,7 +221,7 @@ class TestErrorMessage: @pytest.yield_fixture(autouse=True) def setup(self, monkeypatch, stubs, config_stub): monkeypatch.setattr('qutebrowser.misc.editor.guiprocess.QProcess', - stubs.FakeQProcess()) + stubs.fake_qprocess()) monkeypatch.setattr('qutebrowser.misc.editor.message', stubs.MessageModule()) config_stub.data = {'general': {'editor': [''], diff --git a/tests/stubs.py b/tests/stubs.py index 9d7091581..381ccea7d 100644 --- a/tests/stubs.py +++ b/tests/stubs.py @@ -147,22 +147,13 @@ class FakeNetworkReply: self.headers[key] = value -class FakeQProcess(mock.Mock): - - """QProcess stub. - - Gets some enum values from the real QProcess. - """ - - NormalExit = QProcess.NormalExit - CrashExit = QProcess.CrashExit - - FailedToStart = QProcess.FailedToStart - Crashed = QProcess.Crashed - Timedout = QProcess.Timedout - WriteError = QProcess.WriteError - ReadError = QProcess.ReadError - UnknownError = QProcess.UnknownError +def fake_qprocess(): + """Factory for a QProcess mock which has the QProcess enum values.""" + m = mock.Mock(spec=QProcess) + for attr in ['NormalExit', 'CrashExit', 'FailedToStart', 'Crashed', + 'Timedout', 'WriteError', 'ReadError', 'UnknownError']: + setattr(m, attr, getattr(QProcess, attr)) + return m class FakeSignal: From 33ad0ab1fcf08bbf12ac5314b4722d9d49bfedfc Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2015 23:02:47 +0200 Subject: [PATCH 060/146] Fix startDetached return value for GUIProcess. --- qutebrowser/misc/guiprocess.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/misc/guiprocess.py b/qutebrowser/misc/guiprocess.py index 53cdea8c8..7db0e56f8 100644 --- a/qutebrowser/misc/guiprocess.py +++ b/qutebrowser/misc/guiprocess.py @@ -142,7 +142,7 @@ class GUIProcess(QObject): """Convenience wrapper around QProcess::startDetached.""" log.procs.debug("Starting detached.") self._pre_start(cmd, args) - ok = self._proc.startDetached(cmd, args, cwd) + ok, _pid = self._proc.startDetached(cmd, args, cwd) if ok: log.procs.debug("Process started.") From 19561580967cf49511d7d6c5db23c710a6456321 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2015 23:03:03 +0200 Subject: [PATCH 061/146] Make keyword arguments work for MessageModule stub. --- tests/stubs.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/stubs.py b/tests/stubs.py index 381ccea7d..ab642160f 100644 --- a/tests/stubs.py +++ b/tests/stubs.py @@ -258,14 +258,16 @@ class MessageModule: """A drop-in replacement for qutebrowser.utils.message.""" - def error(self, _win_id, message, _immediately=False): + # pylint: disable=unused-argument + + def error(self, _win_id, message, immediately=False): """Log an error to the message logger.""" logging.getLogger('message').error(message) - def warning(self, _win_id, message, _immediately=False): + def warning(self, _win_id, message, immediately=False): """Log a warning to the message logger.""" logging.getLogger('message').warning(message) - def info(self, _win_id, message, _immediately=True): + def info(self, _win_id, message, immediately=True): """Log an info message to the message logger.""" logging.getLogger('message').info(message) From fc32858e5c1954e42f9129f4a3cdbb3e5b7474f0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2015 23:05:57 +0200 Subject: [PATCH 062/146] Add GUIProcess tests. --- tests/misc/test_guiprocess.py | 122 ++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 tests/misc/test_guiprocess.py diff --git a/tests/misc/test_guiprocess.py b/tests/misc/test_guiprocess.py new file mode 100644 index 000000000..894f0aff4 --- /dev/null +++ b/tests/misc/test_guiprocess.py @@ -0,0 +1,122 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2015 Florian Bruhin (The Compiler) +# +# 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 . + +# pylint: disable=protected-access + +"""Tests for qutebrowser.misc.guiprocess.""" + +import sys +import textwrap + +import pytest +from PyQt5.QtCore import QProcess + +from qutebrowser.misc import guiprocess + + +# FIXME check statusbar messages + + +def _py_proc(code): + """Get a python executable and args list which executes the given code.""" + return (sys.executable, ['-c', textwrap.dedent(code.strip('\n'))]) + + +@pytest.fixture(autouse=True) +def mock_modules(monkeypatch, stubs): + monkeypatch.setattr('qutebrowser.misc.guiprocess.message', + stubs.MessageModule()) + + +@pytest.yield_fixture() +def proc(qtbot): + """A fixture providing a GUIProcess and cleaning it up after the test.""" + p = guiprocess.GUIProcess(0, 'test') + yield p + if p._proc.state() == QProcess.Running: + with qtbot.waitSignal(p.finished, timeout=2000) as blocker: + p._proc.terminate() + if not blocker.signal_triggered: + p._proc.kill() + + +@pytest.fixture() +def fake_proc(monkeypatch, stubs): + """A fixture providing a GUIProcess with a mocked QProcess.""" + p = guiprocess.GUIProcess(0, 'test') + monkeypatch.setattr(p, '_proc', stubs.fake_qprocess()) + return p + + +def test_start(proc, qtbot): + """Test simply starting a process.""" + with qtbot.waitSignals([proc.started, proc.finished], raising=True): + argv = _py_proc("import sys; print('test'); sys.exit(0)") + proc.start(*argv) + + assert bytes(proc._proc.readAll()).rstrip(b'\n') == b'test' + + +@pytest.mark.parametrize('argv', [ + _py_proc('import sys; sys.exit(0)'), + ('does_not', 'exist'), +]) +def test_start_detached(fake_proc, argv): + """Test starting a detached process.""" + fake_proc._proc.startDetached.return_value = (True, 0) + fake_proc.start_detached(*argv) + fake_proc._proc.startDetached.assert_called_with(*list(argv) + [None]) + + +def test_double_start(qtbot, proc): + """Test starting a GUIProcess twice.""" + with qtbot.waitSignal(proc.started, raising=True): + argv = _py_proc("import time; time.sleep(10)") + proc.start(*argv) + with pytest.raises(ValueError): + proc.start('', []) + + +def test_double_start_finished(qtbot, proc): + """Test starting a GUIProcess twice (with the first call finished).""" + with qtbot.waitSignals([proc.started, proc.finished], raising=True): + argv = _py_proc("import sys; sys.exit(0)") + proc.start(*argv) + with qtbot.waitSignals([proc.started, proc.finished], raising=True): + argv = _py_proc("import sys; sys.exit(0)") + proc.start(*argv) + + +def test_cmd_args(proc): + """Test the cmd and args attributes.""" + cmd = 'does_not_exist' + args = ['arg1', 'arg2'] + proc.start(cmd, args) + assert (proc.cmd, proc.args) == (cmd, args) + + +def test_error(qtbot, proc): + """Test the process emitting an error.""" + with qtbot.waitSignal(proc.error, raising=True): + proc.start('this_does_not_exist_either', []) + + +def test_exit_unsuccessful(qtbot, proc): + with qtbot.waitSignal(proc.finished, raising=True): + proc.start(*_py_proc('import sys; sys.exit(0)')) From 36a2f4a15a13d7d71877eb1e479b19834272cf16 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Jun 2015 23:14:56 +0200 Subject: [PATCH 063/146] Fix newline stripping. --- tests/misc/test_guiprocess.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/misc/test_guiprocess.py b/tests/misc/test_guiprocess.py index 894f0aff4..011d4fb23 100644 --- a/tests/misc/test_guiprocess.py +++ b/tests/misc/test_guiprocess.py @@ -70,7 +70,7 @@ def test_start(proc, qtbot): argv = _py_proc("import sys; print('test'); sys.exit(0)") proc.start(*argv) - assert bytes(proc._proc.readAll()).rstrip(b'\n') == b'test' + assert bytes(proc._proc.readAll()).rstrip() == b'test' @pytest.mark.parametrize('argv', [ From 8ecc3a3bb0b1fd769c22cef9ab042665b328ab97 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 12 Jun 2015 11:22:37 +0200 Subject: [PATCH 064/146] Fix lint. --- qutebrowser/browser/commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 9ff65e7bc..9ba2b3cc2 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1572,8 +1572,8 @@ class CommandDispatcher: Args: js_code: The string to evaluate. """ - out = self._current_widget().page().mainFrame().evaluateJavaScript( - js_code) + frame = self._current_widget().page().mainFrame() + out = frame.evaluateJavaScript(js_code) if out is None: # Getting the actual error (if any) seems to be difficult. The From efcea65596f5a2a01db00d77d27916e55414b48e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 12 Jun 2015 11:24:04 +0200 Subject: [PATCH 065/146] Add --quiet argument to :jseval. --- doc/help/commands.asciidoc | 7 +++++-- qutebrowser/browser/commands.py | 6 +++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 9e387f42e..fe71e8eb0 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -244,12 +244,15 @@ Toggle the web inspector. [[jseval]] === jseval -Syntax: +:jseval 'js_code'+ +Syntax: +:jseval [*--quiet*] 'js-code'+ Evaluate a JavaScript string. ==== positional arguments -* +'js_code'+: The string to evaluate. +* +'js-code'+: The string to evaluate. + +==== optional arguments +* +*-q*+, +*--quiet*+: Don't show resulting JS object. ==== note * This command does not split arguments after the last argument and handles quotes literally. diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 9ba2b3cc2..af8815a78 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1566,15 +1566,19 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window', maxsplit=0, no_cmd_split=True) - def jseval(self, js_code): + def jseval(self, js_code, quiet=False): """Evaluate a JavaScript string. Args: js_code: The string to evaluate. + quiet: Don't show resulting JS object. """ frame = self._current_widget().page().mainFrame() out = frame.evaluateJavaScript(js_code) + if quiet: + return + if out is None: # Getting the actual error (if any) seems to be difficult. The # error does end up in BrowserPage.javaScriptConsoleMessage(), but From 6f690c442ee06b44cbd218b8375d5493c76fc966 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 12 Jun 2015 11:24:21 +0200 Subject: [PATCH 066/146] Regenerate authors. --- README.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.asciidoc b/README.asciidoc index e1706d8e7..0cb373219 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -138,8 +138,8 @@ Contributors, sorted by the number of commits in descending order: * Raphael Pierzina * Joel Torstensson * Claude -* Lamar Pavel * Martin Tournoij +* Lamar Pavel * Austin Anderson * Artur Shaik * Antoni Boucher From 8369c74f74dc46606003e54a3bf1c582d06a451d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 12 Jun 2015 11:24:57 +0200 Subject: [PATCH 067/146] Update changelog. --- CHANGELOG.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index a41732c2c..8db54d7bf 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -41,6 +41,7 @@ Added - New setting `colors -> webpage.bg` to set the background color to use for websites which don't set one. - New (hidden) command `:clear-keychain` to clear a partially entered keychain (bound to `` by default, in addition to clearing search). - Many new color settings (foreground setting for every background setting). +- New command `:jseval` to run a javascript snippet on the current page. Changed ~~~~~~~ From 167faafff2d5845851a834b5fa5b89feead748d9 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 12 Jun 2015 11:30:46 +0200 Subject: [PATCH 068/146] Fix command parsing for arguments containing _. --- qutebrowser/commands/command.py | 22 +++++++++++++--------- scripts/src2asciidoc.py | 10 +++++++++- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 4bda15f8a..8da3bc714 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -29,6 +29,11 @@ from qutebrowser.utils import log, utils, message, docutils, objreg, usertypes from qutebrowser.utils import debug as debug_utils +def arg_name(name): + """Get the name an argument should have based on its Python name.""" + return name.rstrip('_').replace('_', '-') + + class Command: """Base skeleton for a command. @@ -288,7 +293,7 @@ class Command: A list of args. """ args = [] - name = param.name.rstrip('_').replace('_', '-') + name = arg_name(param.name) shortname = annotation_info.flag or name[0] if len(shortname) != 1: raise ValueError("Flag '{}' of parameter {} (command {}) must be " @@ -304,7 +309,7 @@ class Command: if typ is not bool: self.flags_with_args += [short_flag, long_flag] else: - args.append(name) + args.append(param.name) if not annotation_info.hide: self.pos_args.append((param.name, name)) return args @@ -408,17 +413,16 @@ class Command: raise TypeError("{}: invalid parameter type {} for argument " "{!r}!".format(self.name, param.kind, param.name)) - def _get_param_name_and_value(self, param): - """Get the converted name and value for an inspect.Parameter.""" - name = param.name.rstrip('_') - value = getattr(self.namespace, name) + def _get_param_value(self, param): + """Get the converted value for an inspect.Parameter.""" + value = getattr(self.namespace, param.name) if param.name in self._type_conv: # We convert enum types after getting the values from # argparse, because argparse's choices argument is # processed after type conversation, which is not what we # want. value = self._type_conv[param.name](value) - return name, value + return value def _get_call_args(self, win_id): """Get arguments for a function call. @@ -452,14 +456,14 @@ class Command: # Special case for win_id parameter. self._get_win_id_arg(win_id, param, args, kwargs) continue - name, value = self._get_param_name_and_value(param) + value = self._get_param_value(param) if param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: args.append(value) elif param.kind == inspect.Parameter.VAR_POSITIONAL: if value is not None: args += value elif param.kind == inspect.Parameter.KEYWORD_ONLY: - kwargs[name] = value + kwargs[param.name] = value else: raise TypeError("{}: Invalid parameter type {} for argument " "'{}'!".format( diff --git a/scripts/src2asciidoc.py b/scripts/src2asciidoc.py index 31d82f6e8..dddbc3a4f 100755 --- a/scripts/src2asciidoc.py +++ b/scripts/src2asciidoc.py @@ -37,7 +37,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) import qutebrowser.app from scripts import asciidoc2html, utils from qutebrowser import qutebrowser -from qutebrowser.commands import cmdutils +from qutebrowser.commands import cmdutils, command from qutebrowser.config import configdata from qutebrowser.utils import docutils @@ -54,6 +54,14 @@ class UsageFormatter(argparse.HelpFormatter): """Override _format_usage to not add the 'usage:' prefix.""" return super()._format_usage(usage, actions, groups, '') + def _get_default_metavar_for_optional(self, action): + """Do name transforming when getting metavar.""" + return command.arg_name(action.dest.upper()) + + def _get_default_metavar_for_positional(self, action): + """Do name transforming when getting metavar.""" + return command.arg_name(action.dest) + def _metavar_formatter(self, action, default_metavar): """Override _metavar_formatter to add asciidoc markup to metavars. From e8830a631e034b23b92d95c5abb0098cc49fa45c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 12 Jun 2015 11:54:20 +0200 Subject: [PATCH 069/146] Increase test_guiprocess timeouts. Apparently 1 second is not enough for Windows to start a process... --- tests/misc/test_guiprocess.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/misc/test_guiprocess.py b/tests/misc/test_guiprocess.py index 011d4fb23..a975c53f2 100644 --- a/tests/misc/test_guiprocess.py +++ b/tests/misc/test_guiprocess.py @@ -66,7 +66,8 @@ def fake_proc(monkeypatch, stubs): def test_start(proc, qtbot): """Test simply starting a process.""" - with qtbot.waitSignals([proc.started, proc.finished], raising=True): + with qtbot.waitSignals([proc.started, proc.finished], raising=True, + timeout=2000): argv = _py_proc("import sys; print('test'); sys.exit(0)") proc.start(*argv) @@ -86,7 +87,7 @@ def test_start_detached(fake_proc, argv): def test_double_start(qtbot, proc): """Test starting a GUIProcess twice.""" - with qtbot.waitSignal(proc.started, raising=True): + with qtbot.waitSignal(proc.started, raising=True, timeout=2000): argv = _py_proc("import time; time.sleep(10)") proc.start(*argv) with pytest.raises(ValueError): @@ -95,10 +96,12 @@ def test_double_start(qtbot, proc): def test_double_start_finished(qtbot, proc): """Test starting a GUIProcess twice (with the first call finished).""" - with qtbot.waitSignals([proc.started, proc.finished], raising=True): + with qtbot.waitSignals([proc.started, proc.finished], raising=True, + timeout=2000): argv = _py_proc("import sys; sys.exit(0)") proc.start(*argv) - with qtbot.waitSignals([proc.started, proc.finished], raising=True): + with qtbot.waitSignals([proc.started, proc.finished], raising=True, + timeout=2000): argv = _py_proc("import sys; sys.exit(0)") proc.start(*argv) @@ -118,5 +121,5 @@ def test_error(qtbot, proc): def test_exit_unsuccessful(qtbot, proc): - with qtbot.waitSignal(proc.finished, raising=True): + with qtbot.waitSignal(proc.finished, raising=True, timeout=2000): proc.start(*_py_proc('import sys; sys.exit(0)')) From b1e9ff059a3356ffdec58856581de602affbce1b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 12 Jun 2015 13:21:37 +0200 Subject: [PATCH 070/146] Update for PyQt 5.4.2. Upstream changelog: 2015-06-11 Phil Thompson * .hgtags: Added tag 5.4.2 for changeset 5a34feb6b31d [6f80aa2771d3] [tip] <5.4-maint> * NEWS: Released as v5.4.2. [5a34feb6b31d] [5.4.2] <5.4-maint> * installers/PyQt5-Qt5-gpl.nsi: Fixed a missing image plugin in the Windows installer. [29760ab3d5f9] <5.4-maint> * Makefile: Clean up any extra Mac crap. [dcbc92d15a8b] <5.4-maint> 2015-06-07 Phil Thompson * pyuic/uic/Compiler/compiler.py, pyuic/uic/Compiler/qobjectcreator.py: Make sure all generedt imports are sorted and therefore repeatable. [9ad1a251d97b] <5.4-maint> 2015-06-05 Phil Thompson * NEWS, PyQt5.msp: Completed the support for Qt v5.4.2. [02c99f5affde] <5.4-maint> * PyQt5.msp: Scanned Qt v5.4.2. [7fbd795f8c5e] <5.4-maint> * installers/PyQt5-Qt5-gpl.nsi: Updated the Windows installer for Qt v5.4.2. [74c080b5bdb2] <5.4-maint> * PyQt5.msp: Added missing /Factory/ annotations from the create() and beginCreate() methods of QQmlComponent. [56be1a87fd2c] <5.4-maint> 2015-06-02 Phil Thompson * PyQt5.msp: Fixed the handling of the value returned by Python re- implementations of QSGMaterialShader.attributeNames(). [cb620297cbc8] <5.4-maint> 2015-05-23 Phil Thompson * lib/configure.py, sphinx/installation.rst: Added the --no-python-dbus option to configure.py. [df17d3eace7a] <5.4-maint> 2015-05-18 Phil Thompson * pyuic/uic/uiparser.py: Fixed another deprecation warning in pyuic. [6333c15a9a6b] <5.4-maint> * pyuic/uic/driver.py, pyuic/uic/objcreator.py, pyuic/uic/port_v2/load_plugin.py, pyuic/uic/port_v3/load_plugin.py: Fixed all the deprecation warnings from pyuic. [e8f96fbc8cf0] <5.4-maint> 2015-05-08 Phil Thompson * Makefile: Fixed the path to SIP on OS/X. [39ecf0bc71e1] <5.4-maint> 2015-05-06 Phil Thompson * pyuic/uic/Compiler/qobjectcreator.py, pyuic/uic/Loader/qobjectcreator.py, pyuic/uic/icon_cache.py, pyuic/uic/objcreator.py: Fixed the handling of themed icons by uic.loadUi(). [506c268c8f43] <5.4-maint> 2015-04-24 Phil Thompson * qpy/QtCore/qpycore_chimera.cpp: Handle properties that are objects that are defined in QML. [aebd6aab85d4] <5.4-maint> 2015-04-04 Phil Thompson * pyuic/uic/properties.py, pyuic/uic/uiparser.py: Fixed pyuic's handling of default margins. [6a7e3e6175c8] <5.4-maint> * pyuic/uic/properties.py, pyuic/uic/uiparser.py: Fixed pyuic's handling of the default spacing. [12193d5afbe1] <5.4-maint> 2015-04-03 Phil Thompson * pylupdate/main.cpp: pylupdate now saves locations as relative to the .ts file. [1757d2e318f6] <5.4-maint> 2015-04-01 Phil Thompson * PyQt5.msp: Added QWIDGETSIZE_MAX to QtWidgets. [b136fd7c485e] <5.4-maint> 2015-03-25 Phil Thompson * sphinx/static/classic.css, sphinx/static/default.css: Fixed the stylesheet. [d35996e57f02] <5.4-maint> 2015-03-16 Phil Thompson * PyQt5.msp: The GIL is now released for all QImage ctors and methods that might block. [3fd70eec66b9] <5.4-maint> * PyQt5.msp: Removed the internal QGraphicsSceneEvent.setWidget(). [622e5b5ebcfc] <5.4-maint> 2015-03-11 Phil Thompson * installers/PyQt5-Qt5-gpl.nsi: Added the OpenGL v2.1 backend to the Windows installer. [ca1e4c121c78] <5.4-maint> * sphinx/conf.py: Updated for sphinx v1.3. [1c1cd1eac7ce] <5.4-maint> * qpy/QtCore/qsysinfo.sip: Added Yosemite and iOS v8.0 to QSysInfo. [01d4d1af5961] <5.4-maint> * pyuic/uic/uiparser.py: pyuic now handles empty zorder elements. [a0dcd07b7e72] <5.4-maint> * lib/configure.py: Added nostrup to the generated .pro file. [d6445df281a6] <5.4-maint> 2015-03-01 Phil Thompson * pyuic/uic/uiparser.py: pyuic will now ignore spacer items when setting the z-order. [28704a096a3a] <5.4-maint> 2015-02-26 Phil Thompson * installers/PyQt5-Qt5-gpl.nsi: Installer fix for Qt v5.4.1. [0b21a7fa6750] <5.4-maint>: --- README.asciidoc | 2 +- scripts/ci_install.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.asciidoc b/README.asciidoc index 0cb373219..065e57e57 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -92,7 +92,7 @@ The following software and libraries are required to run qutebrowser: * http://qt-project.org/[Qt] 5.2.0 or newer (5.4.2 recommended) * QtWebKit * http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.2.0 or newer -(5.4.1 recommended) for Python 3 +(5.4.2 recommended) for Python 3 * https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools] * http://fdik.org/pyPEG/[pyPEG2] * http://jinja.pocoo.org/[jinja2] diff --git a/scripts/ci_install.py b/scripts/ci_install.py index 1af604892..6d0ea9016 100644 --- a/scripts/ci_install.py +++ b/scripts/ci_install.py @@ -33,7 +33,7 @@ import sys import subprocess import urllib -PYQT_VERSION = '5.4.1' +PYQT_VERSION = '5.4.2' def apt_get(args): From 9b066ec50af4cc38c0bc8fb909dd97469b521406 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 12 Jun 2015 13:44:10 +0200 Subject: [PATCH 071/146] Set geometry in test_textbase.py. See 83f7cf84a9af99bfad52bc4e07aff157fae70804 - it seems with the Qt 5.4.2 upgrade there are some more warnings on Windows. --- tests/mainwindow/statusbar/test_textbase.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/mainwindow/statusbar/test_textbase.py b/tests/mainwindow/statusbar/test_textbase.py index eadf9c46a..0c2108dbe 100644 --- a/tests/mainwindow/statusbar/test_textbase.py +++ b/tests/mainwindow/statusbar/test_textbase.py @@ -46,6 +46,7 @@ def test_elided_text(qtbot, elidemode, check): """ label = TextBase(elidemode=elidemode) qtbot.add_widget(label) + label.setGeometry(200, 200, 200, 200) long_string = 'Hello world! ' * 20 label.setText(long_string) label.resize(100, 50) From 8033931baed37cc0d4b76ce3bcf1ce53643a92ab Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 12 Jun 2015 16:37:07 +0200 Subject: [PATCH 072/146] Test key_to_string with all Qt.Key members. --- tests/utils/test_utils.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/utils/test_utils.py b/tests/utils/test_utils.py index 52daab386..04291dfdd 100644 --- a/tests/utils/test_utils.py +++ b/tests/utils/test_utils.py @@ -410,6 +410,7 @@ class TestKeyToString: (Qt.Key_Escape, 'Escape'), (Qt.Key_A, 'A'), (Qt.Key_degree, '°'), + (Qt.Key_Meta, 'Meta'), ]) def test_normal(self, key, expected): """Test a special key where QKeyEvent::toString works incorrectly.""" @@ -422,6 +423,16 @@ class TestKeyToString: # want to know if the mapping still behaves properly. assert utils.key_to_string(Qt.Key_A) == 'A' + def test_all(self): + """Make sure there's some sensible output for all keys.""" + for name, value in sorted(vars(Qt).items()): + if not isinstance(value, Qt.Key): + continue + print(name) + string = utils.key_to_string(value) + assert string + string.encode('utf-8') # make sure it's encodable + class TestKeyEventToString: From 84a269f36a412417e1582ad486576b1800444937 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 12 Jun 2015 16:37:17 +0200 Subject: [PATCH 073/146] Add missing keys to key_to_string. --- qutebrowser/utils/utils.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index b113a2356..9af412d1c 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -288,6 +288,18 @@ def key_to_string(key): 'Key_TouchpadOn': 'Touchpad On', 'Key_TouchpadToggle': 'Touchpad toggle', 'Key_Yellow': 'Yellow', + 'Key_Alt': 'Alt', + 'Key_AltGr': 'AltGr', + 'Key_Control': 'Control', + 'Key_Direction_L': 'Direction L', + 'Key_Direction_R': 'Direction R', + 'Key_Hyper_L': 'Hyper L', + 'Key_Hyper_R': 'Hyper R', + 'Key_Meta': 'Meta', + 'Key_Shift': 'Shift', + 'Key_Super_L': 'Super L', + 'Key_Super_R': 'Super R', + 'Key_unknown': 'Unknown', } # We now build our real special_names dict from the string mapping above. # The reason we don't do this directly is that certain Qt versions don't From f17131f6c2bb22d23bb345faa50cf59e80b3c706 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 12 Jun 2015 16:59:33 +0200 Subject: [PATCH 074/146] Change Qt links to point to qt.io. --- CONTRIBUTING.asciidoc | 11 ++++----- FAQ.asciidoc | 25 ++++++++++---------- README.asciidoc | 4 ++-- doc/help/settings.asciidoc | 2 +- qutebrowser/browser/cookies.py | 2 +- qutebrowser/browser/downloads.py | 2 +- qutebrowser/completion/completiondelegate.py | 1 - qutebrowser/config/configdata.py | 2 +- qutebrowser/mainwindow/statusbar/textbase.py | 2 +- qutebrowser/misc/earlyinit.py | 4 ++-- qutebrowser/utils/log.py | 8 +++---- qutebrowser/utils/standarddir.py | 4 ++-- qutebrowser/utils/urlutils.py | 2 +- qutebrowser/utils/utils.py | 2 +- scripts/segfault_test.py | 10 ++++---- 15 files changed, 39 insertions(+), 42 deletions(-) diff --git a/CONTRIBUTING.asciidoc b/CONTRIBUTING.asciidoc index 8123514a9..558887a5b 100644 --- a/CONTRIBUTING.asciidoc +++ b/CONTRIBUTING.asciidoc @@ -153,7 +153,7 @@ Useful websites Some resources which might be handy: -* http://qt-project.org/doc/qt-5/classes.html[The Qt5 reference] +* http://doc.qt.io/qt-5/classes.html[The Qt5 reference] * https://docs.python.org/3/library/index.html[The Python reference] * http://httpbin.org/[httpbin, a test service for HTTP requests/responses] * http://requestb.in/[RequestBin, a service to inspect HTTP requests] @@ -211,8 +211,7 @@ Other Languages] (http://www.rfc-editor.org/errata_search.php?rfc=5646[Errata]) * http://www.w3.org/TR/CSS2/[Cascading Style Sheets Level 2 Revision 1 (CSS 2.1) Specification] -* http://qt-project.org/doc/qt-4.8/stylesheet-reference.html[Qt Style Sheets -Reference] +* http://doc.qt.io/qt-5/stylesheet-reference.html[Qt Style Sheets Reference] * http://mimesniff.spec.whatwg.org/[MIME Sniffing Standard] * http://spec.whatwg.org/[WHATWG specifications] * http://www.w3.org/html/wg/drafts/html/master/Overview.html[HTML 5.1 Nightly] @@ -293,8 +292,8 @@ All objects can be printed by starting with the `--debug` flag and using the The registry is mainly used for <> but also can be useful in places where using Qt's -http://qt-project.org/doc/qt-5/signalsandslots.html[signals and slots] -mechanism would be difficult. +http://doc.qt.io/qt-5/signalsandslots.html[signals and slots] mechanism would +be difficult. Logging ~~~~~~~ @@ -539,7 +538,7 @@ New Qt release * Run all tests and check nothing is broken. * Check the -https://bugreports.qt-project.org/issues/?jql=reporter%20%3D%20%22The%20Compiler%22%20ORDER%20BY%20fixVersion%20ASC[Qt bugtracker] +https://bugreports.qt.io/issues/?jql=reporter%20%3D%20%22The%20Compiler%22%20ORDER%20BY%20fixVersion%20ASC[Qt bugtracker] and make sure all bugs marked as resolved are actually fixed. * Update own PKGBUILDs based on upstream Archlinux updates and rebuild. * Update recommended Qt version in `README` diff --git a/FAQ.asciidoc b/FAQ.asciidoc index 16d02cfa0..886721a18 100644 --- a/FAQ.asciidoc +++ b/FAQ.asciidoc @@ -4,8 +4,8 @@ The Compiler [qanda] What is qutebrowser based on?:: - qutebrowser uses http://www.python.org/[Python], http://qt-project.org/[Qt] - and http://www.riverbankcomputing.com/software/pyqt/intro[PyQt]. + qutebrowser uses http://www.python.org/[Python], http://qt.io/[Qt] and + http://www.riverbankcomputing.com/software/pyqt/intro[PyQt]. + The concept of it is largely inspired by http://portix.bitbucket.org/dwb/[dwb] and http://www.vimperator.org/vimperator[Vimperator]. Many actions and @@ -15,7 +15,7 @@ Why another browser?:: It might be hard to believe, but I didn't find any browser which I was happy with, so I started to write my own. Also, I needed a project to get into writing GUI applications with Python and - link:http://qt-project.org/[Qt]/link:http://www.riverbankcomputing.com/software/pyqt/intro[PyQt]. + link:http://qt.io/[Qt]/link:http://www.riverbankcomputing.com/software/pyqt/intro[PyQt]. + Read the next few questions to find out why I was unhappy with existing software. @@ -32,12 +32,11 @@ API] seems to lack basic features like proxy support, and almost no projects seem to have started porting to WebKit2 (I only know of http://www.uzbl.org/[uzbl]). + -qutebrowser uses http://qt-project.org/[Qt] and -http://qt-project.org/wiki/QtWebKit[QtWebKit] instead, which suffers from far -less such crashes. It might switch to -http://qt-project.org/wiki/QtWebEngine[QtWebEngine] in the future, which is -based on Google's https://en.wikipedia.org/wiki/Blink_(layout_engine)[Blink] -rendering engine. +qutebrowser uses http://qt.io/[Qt] and http://wiki.qt.io/QtWebKit[QtWebKit] +instead, which suffers from far less such crashes. It might switch to +http://wiki.qt.io/QtWebEngine[QtWebEngine] in the future, which is based on +Google's https://en.wikipedia.org/wiki/Blink_(layout_engine)[Blink] rendering +engine. What's wrong with https://www.mozilla.org/en-US/firefox/new/[Firefox] and link:http://5digits.org/pentadactyl/[Pentadactyl]/link:http://www.vimperator.org/vimperator[Vimperator]?:: Firefox likes to break compatibility with addons on each upgrade, gets @@ -54,10 +53,10 @@ What's wrong with http://www.chromium.org/Home[Chromium] and https://vimium.gith Why Python?:: I enjoy writing Python since 2011, which made it one of the possible - choices. I wanted to use http://qt-project.org/[Qt] because of - http://qt-project.org/wiki/QtWebKit[QtWebKit] so I didn't have - http://qt-project.org/wiki/Category:LanguageBindings[many other choices]. I - don't like C++ and can't write it very well, so that wasn't an alternative. + choices. I wanted to use http://qt.io/[Qt] because of + http://wiki.qt.io/QtWebKit[QtWebKit] so I didn't have + http://wiki.qt.io/Category:LanguageBindings[many other choices]. I don't + like C++ and can't write it very well, so that wasn't an alternative. But isn't Python too slow for a browser?:: http://www.infoworld.com/d/application-development/van-rossum-python-not-too-slow-188715[No.] diff --git a/README.asciidoc b/README.asciidoc index 065e57e57..03aefa777 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -89,7 +89,7 @@ Requirements The following software and libraries are required to run qutebrowser: * http://www.python.org/[Python] 3.4 -* http://qt-project.org/[Qt] 5.2.0 or newer (5.4.2 recommended) +* http://qt.io/[Qt] 5.2.0 or newer (5.4.2 recommended) * QtWebKit * http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.2.0 or newer (5.4.2 recommended) for Python 3 @@ -222,7 +222,7 @@ Also, thanks to: * Everyone who had the patience to test qutebrowser before v0.1. * Everyone triaging/fixing my bugs in the -https://bugreports.qt-project.org/secure/Dashboard.jspa[Qt bugtracker] +https://bugreports.qt.io/secure/Dashboard.jspa[Qt bugtracker] * Everyone answering my questions on http://stackoverflow.com/[Stack Overflow] and in IRC. * All the projects which were a great help while developing qutebrowser. diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 837f84a3e..2d0f9364c 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -1474,7 +1474,7 @@ A value can be in one of the following format: * transparent (no color) * `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or percentages) * `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359) - * A gradient as explained in http://qt-project.org/doc/qt-4.8/stylesheet-reference.html#list-of-property-types[the Qt documentation] under ``Gradient''. + * A gradient as explained in http://doc.qt.io/qt-5/stylesheet-reference.html#list-of-property-types[the Qt documentation] under ``Gradient''. A *.system value determines the color system to use for color interpolation between similarly-named *.start and *.stop entries, regardless of how they are defined in the options. Valid values are 'rgb', 'hsv', and 'hsl'. diff --git a/qutebrowser/browser/cookies.py b/qutebrowser/browser/cookies.py index f6af98d57..0c6b8c036 100644 --- a/qutebrowser/browser/cookies.py +++ b/qutebrowser/browser/cookies.py @@ -84,7 +84,7 @@ class CookieJar(RAMCookieJar): def purge_old_cookies(self): """Purge expired cookies from the cookie jar.""" # Based on: - # http://qt-project.org/doc/qt-5/qtwebkitexamples-webkitwidgets-browser-cookiejar-cpp.html + # http://doc.qt.io/qt-5/qtwebkitexamples-webkitwidgets-browser-cookiejar-cpp.html now = QDateTime.currentDateTime() cookies = [c for c in self.allCookies() if c.isSessionCookie() or c.expirationDate() >= now] diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index cfcf23eec..3f8bf116c 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -685,7 +685,7 @@ class DownloadManager(QAbstractListModel): if fileobj is not None and filename is not None: raise TypeError("Only one of fileobj/filename may be given!") # WORKAROUND for Qt corrupting data loaded from cache: - # https://bugreports.qt-project.org/browse/QTBUG-42757 + # https://bugreports.qt.io/browse/QTBUG-42757 request.setAttribute(QNetworkRequest.CacheLoadControlAttribute, QNetworkRequest.AlwaysNetwork) suggested_fn = urlutils.filename_from_url(request.url()) diff --git a/qutebrowser/completion/completiondelegate.py b/qutebrowser/completion/completiondelegate.py index 03caa5158..3fe7e00ba 100644 --- a/qutebrowser/completion/completiondelegate.py +++ b/qutebrowser/completion/completiondelegate.py @@ -145,7 +145,6 @@ class CompletionItemDelegate(QStyledItemDelegate): rect: The QRect to clip the drawing to. """ # We can't use drawContents because then the color would be ignored. - # See: https://qt-project.org/forums/viewthread/21492 clip = QRectF(0, 0, rect.width(), rect.height()) self._painter.save() if self._opt.state & QStyle.State_Selected: diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index eb1b529c7..a19a4fcdb 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -100,7 +100,7 @@ SECTION_DESC = { " * `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or " "percentages)\n" " * `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359)\n" - " * A gradient as explained in http://qt-project.org/doc/qt-4.8/" + " * A gradient as explained in http://doc.qt.io/qt-5/" "stylesheet-reference.html#list-of-property-types[the Qt " "documentation] under ``Gradient''.\n\n" "A *.system value determines the color system to use for color " diff --git a/qutebrowser/mainwindow/statusbar/textbase.py b/qutebrowser/mainwindow/statusbar/textbase.py index 1006effe7..5a5954f85 100644 --- a/qutebrowser/mainwindow/statusbar/textbase.py +++ b/qutebrowser/mainwindow/statusbar/textbase.py @@ -70,7 +70,7 @@ class TextBase(QLabel): More info: http://stackoverflow.com/q/21890462/2085149 - https://bugreports.qt-project.org/browse/QTBUG-36945 + https://bugreports.qt.io/browse/QTBUG-36945 https://codereview.qt-project.org/#/c/79181/ Args: diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py index 43806df58..ca8096b03 100644 --- a/qutebrowser/misc/earlyinit.py +++ b/qutebrowser/misc/earlyinit.py @@ -137,10 +137,10 @@ def fix_harfbuzz(args): - On Qt 5.2 (and probably earlier) the new engine probably has more crashes and is also experimental. - e.g. https://bugreports.qt-project.org/browse/QTBUG-36099 + e.g. https://bugreports.qt.io/browse/QTBUG-36099 - On Qt 5.3.0 there's a bug that affects a lot of websites: - https://bugreports.qt-project.org/browse/QTBUG-39278 + https://bugreports.qt.io/browse/QTBUG-39278 So the new engine will be more stable. - On Qt 5.3.1 this bug is fixed and the old engine will be the more stable diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 269e1e38d..9106eb7dd 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -259,23 +259,23 @@ def qt_message_handler(msg_type, context, msg): # suppressed_msgs is a list of regexes matching the message texts to hide. suppressed_msgs = ( # PNGs in Qt with broken color profile - # https://bugreports.qt-project.org/browse/QTBUG-39788 + # https://bugreports.qt.io/browse/QTBUG-39788 "libpng warning: iCCP: Not recognizing known sRGB profile that has " "been edited", # Hopefully harmless warning "OpenType support missing for script ", # Error if a QNetworkReply gets two different errors set. Harmless Qt # bug on some pages. - # https://bugreports.qt-project.org/browse/QTBUG-30298 + # https://bugreports.qt.io/browse/QTBUG-30298 "QNetworkReplyImplPrivate::error: Internal problem, this method must " "only be called once.", # Sometimes indicates missing text, but most of the time harmless "load glyph failed ", - # Harmless, see https://bugreports.qt-project.org/browse/QTBUG-42479 + # Harmless, see https://bugreports.qt.io/browse/QTBUG-42479 "content-type missing in HTTP POST, defaulting to " "application/x-www-form-urlencoded. Use QNetworkRequest::setHeader() " "to fix this problem.", - # https://bugreports.qt-project.org/browse/QTBUG-43118 + # https://bugreports.qt.io/browse/QTBUG-43118 "Using blocking call!", # Hopefully harmless '"Method "GetAll" with signature "s" on interface ' diff --git a/qutebrowser/utils/standarddir.py b/qutebrowser/utils/standarddir.py index 6d6e5a6dd..bbe61c459 100644 --- a/qutebrowser/utils/standarddir.py +++ b/qutebrowser/utils/standarddir.py @@ -118,7 +118,7 @@ def _get(typ): Args: typ: A member of the QStandardPaths::StandardLocation enum, - see http://qt-project.org/doc/qt-5/qstandardpaths.html#StandardLocation-enum + see http://doc.qt.io/qt-5/qstandardpaths.html#StandardLocation-enum """ overridden, path = _from_args(typ, _args) if not overridden: @@ -127,7 +127,7 @@ def _get(typ): if (typ == QStandardPaths.ConfigLocation and path.split(os.sep)[-1] != appname): # WORKAROUND - see - # https://bugreports.qt-project.org/browse/QTBUG-38872 + # https://bugreports.qt.io/browse/QTBUG-38872 path = os.path.join(path, appname) if typ == QStandardPaths.DataLocation and os.name == 'nt': # Under windows, config/data might end up in the same directory. diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 143e7cfc5..5d1c40465 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -279,7 +279,7 @@ def qurl_from_user_input(urlstr): IPv6, so we first try to handle it as a valid IPv6, and if that fails we use QUrl.fromUserInput. - WORKAROUND - https://bugreports.qt-project.org/browse/QTBUG-41089 + WORKAROUND - https://bugreports.qt.io/browse/QTBUG-41089 FIXME - Maybe https://codereview.qt-project.org/#/c/93851/ has a better way to solve this? https://github.com/The-Compiler/qutebrowser/issues/109 diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 9af412d1c..b25997662 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -240,7 +240,7 @@ def key_to_string(key): """ special_names_str = { # Some keys handled in a weird way by QKeySequence::toString. - # See https://bugreports.qt-project.org/browse/QTBUG-40030 + # See https://bugreports.qt.io/browse/QTBUG-40030 # Most are unlikely to be ever needed, but you never know ;) # For dead/combining keys, we return the corresponding non-combining # key, as that's easier to add to the config. diff --git a/scripts/segfault_test.py b/scripts/segfault_test.py index e2a374343..091b1b7e4 100755 --- a/scripts/segfault_test.py +++ b/scripts/segfault_test.py @@ -70,20 +70,20 @@ def main(): if len(sys.argv) < 2: # pages which previously caused problems pages = [ - # ANGLE, https://bugreports.qt-project.org/browse/QTBUG-39723 + # ANGLE, https://bugreports.qt.io/browse/QTBUG-39723 ('http://www.binpress.com/', False), ('http://david.li/flow/', False), ('https://imzdl.com/', False), # not reproducible - # https://bugreports.qt-project.org/browse/QTBUG-39847 + # https://bugreports.qt.io/browse/QTBUG-39847 ('http://www.20min.ch/', True), - # HarfBuzz, https://bugreports.qt-project.org/browse/QTBUG-39278 + # HarfBuzz, https://bugreports.qt.io/browse/QTBUG-39278 ('http://www.the-compiler.org/', True), ('http://phoronix.com', True), ('http://twitter.com', True), - # HarfBuzz #2, https://bugreports.qt-project.org/browse/QTBUG-36099 + # HarfBuzz #2, https://bugreports.qt.io/browse/QTBUG-36099 ('http://lenta.ru/', True), - # Unknown, https://bugreports.qt-project.org/browse/QTBUG-41360 + # Unknown, https://bugreports.qt.io/browse/QTBUG-41360 ('http://salt.readthedocs.org/en/latest/topics/pillar/', True), ] else: From 219c2f8ae85aad7233f1d423b4b11a5dc39c1c62 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 12 Jun 2015 17:04:12 +0200 Subject: [PATCH 075/146] Ignore "Unable to set geometry" warnings in tests. This reverts commits 9b066ec50af4cc38c0bc8fb909dd97469b521406 and 83f7cf84a9af99bfad52bc4e07aff157fae70804. This was still broken even after setting the geometry: https://ci.appveyor.com/project/The-Compiler/qutebrowser --- tests/mainwindow/statusbar/test_progress.py | 1 - tests/mainwindow/statusbar/test_textbase.py | 1 - tox.ini | 1 + 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/mainwindow/statusbar/test_progress.py b/tests/mainwindow/statusbar/test_progress.py index b8b03cd29..07e93e0e5 100644 --- a/tests/mainwindow/statusbar/test_progress.py +++ b/tests/mainwindow/statusbar/test_progress.py @@ -39,7 +39,6 @@ def progress_widget(qtbot, monkeypatch, config_stub): 'qutebrowser.mainwindow.statusbar.progress.style.config', config_stub) widget = Progress() qtbot.add_widget(widget) - widget.setGeometry(200, 200, 200, 200) assert not widget.isVisible() assert not widget.isTextVisible() return widget diff --git a/tests/mainwindow/statusbar/test_textbase.py b/tests/mainwindow/statusbar/test_textbase.py index 0c2108dbe..eadf9c46a 100644 --- a/tests/mainwindow/statusbar/test_textbase.py +++ b/tests/mainwindow/statusbar/test_textbase.py @@ -46,7 +46,6 @@ def test_elided_text(qtbot, elidemode, check): """ label = TextBase(elidemode=elidemode) qtbot.add_widget(label) - label.setGeometry(200, 200, 200, 200) long_string = 'Hello world! ' * 20 label.setText(long_string) label.resize(100, 50) diff --git a/tox.ini b/tox.ini index 99690987d..19165402b 100644 --- a/tox.ini +++ b/tox.ini @@ -166,3 +166,4 @@ qt_log_level_fail = WARNING qt_log_ignore = ^SpellCheck: .* ^SetProcessDpiAwareness failed: .* + ^QWindowsWindow::setGeometryDp: Unable to set geometry .* From 9bf749643a6bc5b577a5ed987c28e194ee74be29 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 12 Jun 2015 13:49:50 +0200 Subject: [PATCH 076/146] Fix ci_install.py for Travis on OS X. --- scripts/ci_install.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/scripts/ci_install.py b/scripts/ci_install.py index 6d0ea9016..4bd4698ad 100644 --- a/scripts/ci_install.py +++ b/scripts/ci_install.py @@ -40,8 +40,12 @@ def apt_get(args): subprocess.check_call(['sudo', 'apt-get', '-y', '-q'] + args) -def brew(args): - subprocess.check_call(['brew'] + args) +def brew(args, silent=False): + if silent: + with open(os.devnull, 'w') as f: + subprocess.check_call(['brew'] + args, stdout=f) + else: + subprocess.check_call(['brew'] + args) if 'APPVEYOR' in os.environ: @@ -69,14 +73,15 @@ elif os.environ.get('TRAVIS_OS_NAME', None) == 'linux': apt_get(['install'] + pkgs.split()) elif os.environ.get('TRAVIS_OS_NAME', None) == 'osx': print("brew update...") - brew(['update']) + brew(['update'], silent=True) print("Installing packages...") brew(['install', 'python3', 'pyqt5']) print("Installing tox...") - subprocess.check_call(['pip3.4', 'install', 'tox']) + subprocess.check_call(['sudo', 'pip3.4', 'install', 'tox']) + os.system('ls -l /usr/local/bin/xvfb-run') print("Creating xvfb-run stub...") with open('/usr/local/bin/xvfb-run', 'w') as f: # This will break when xvfb-run is called differently in .travis.yml, @@ -84,6 +89,8 @@ elif os.environ.get('TRAVIS_OS_NAME', None) == 'osx': f.write('#!/bin/bash\n') f.write('shift 2\n') f.write('exec "$@"\n') + os.system('sudo chmod 755 /usr/local/bin/xvfb-run') + os.system('ls -l /usr/local/bin/xvfb-run') else: def env(key): return os.environ.get(key, None) From f1ba14b496e49576590398609666a41e83e80c7f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 13 Jun 2015 13:26:29 +0200 Subject: [PATCH 077/146] Fix exception when using :set. This is a regression introduced in 167faafff2d5845851a834b5fa5b89feead748d9. --- qutebrowser/commands/command.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index 8da3bc714..5135c07e0 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -262,10 +262,12 @@ class Command: except KeyError: pass + kwargs['dest'] = param.name + if isinstance(typ, tuple): kwargs['metavar'] = annotation_info.metavar or param.name elif utils.is_enum(typ): - kwargs['choices'] = [e.name.replace('_', '-') for e in typ] + kwargs['choices'] = [arg_name(e.name) for e in typ] kwargs['metavar'] = annotation_info.metavar or param.name elif typ is bool: kwargs['action'] = 'store_true' @@ -309,7 +311,6 @@ class Command: if typ is not bool: self.flags_with_args += [short_flag, long_flag] else: - args.append(param.name) if not annotation_info.hide: self.pos_args.append((param.name, name)) return args From 8dc9f0562af90ab91ac4dee8a24acb9d18045653 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 14 Jun 2015 13:45:09 +0200 Subject: [PATCH 078/146] tox: Update pyroma to 1.8.2. Upstream changelog: Do not complain that the version number should be a string, when it is a basestring. [maurits] --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 19165402b..b72dc3bf4 100644 --- a/tox.ini +++ b/tox.ini @@ -111,7 +111,7 @@ commands = [testenv:pyroma] skip_install = true deps = - pyroma==1.8.1 + pyroma==1.8.2 docutils==0.12 commands = {envpython} scripts/link_pyqt.py --tox {envdir} From 70699988ed8f4455c00396100791657f0766dfd6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 15 Jun 2015 06:18:16 +0200 Subject: [PATCH 079/146] Fix context managers behavior on exceptions. --- qutebrowser/utils/debug.py | 10 ++++++---- qutebrowser/utils/log.py | 12 ++++++++---- qutebrowser/utils/utils.py | 12 +++++++----- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/qutebrowser/utils/debug.py b/qutebrowser/utils/debug.py index 97a21c0e5..16aa53adf 100644 --- a/qutebrowser/utils/debug.py +++ b/qutebrowser/utils/debug.py @@ -230,10 +230,12 @@ def log_time(logger, action='operation'): action: A description of what's being done. """ started = datetime.datetime.now() - yield - finished = datetime.datetime.now() - delta = (finished - started).total_seconds() - logger.debug("{} took {} seconds.".format(action.capitalize(), delta)) + try: + yield + finally: + finished = datetime.datetime.now() + delta = (finished - started).total_seconds() + logger.debug("{} took {} seconds.".format(action.capitalize(), delta)) def _get_widgets(): diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 9106eb7dd..bac158c3d 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -159,8 +159,10 @@ def init_log(args): def disable_qt_msghandler(): """Contextmanager which temporarily disables the Qt message handler.""" old_handler = QtCore.qInstallMessageHandler(None) - yield - QtCore.qInstallMessageHandler(old_handler) + try: + yield + finally: + QtCore.qInstallMessageHandler(old_handler) def _init_handlers(level, color, ram_capacity): @@ -319,8 +321,10 @@ def hide_qt_warning(pattern, logger='qt'): log_filter = QtWarningFilter(pattern) logger_obj = logging.getLogger(logger) logger_obj.addFilter(log_filter) - yield - logger_obj.removeFilter(log_filter) + try: + yield + finally: + logger_obj.removeFilter(log_filter) class QtWarningFilter(logging.Filter): diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index b25997662..b5f9ac25c 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -438,11 +438,13 @@ def disabled_excepthook(): """Run code with the exception hook temporarily disabled.""" old_excepthook = sys.excepthook sys.excepthook = sys.__excepthook__ - yield - # If the code we did run did change sys.excepthook, we leave it - # unchanged. Otherwise, we reset it. - if sys.excepthook is sys.__excepthook__: - sys.excepthook = old_excepthook + try: + yield + finally: + # If the code we did run did change sys.excepthook, we leave it + # unchanged. Otherwise, we reset it. + if sys.excepthook is sys.__excepthook__: + sys.excepthook = old_excepthook class prevent_exceptions: # pylint: disable=invalid-name From b43d8b13d8fc5def27da0fee32debf2b6918ee07 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 15 Jun 2015 06:21:14 +0200 Subject: [PATCH 080/146] tox: Update mccabe to 0.3.1. Upstream changelog: - Include test_mccabe.py in releases. - Always coerce the max_complexity value from Flake8's entry-point to an integer. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index b72dc3bf4..e53117d97 100644 --- a/tox.ini +++ b/tox.ini @@ -102,7 +102,7 @@ deps = -r{toxinidir}/requirements.txt py==1.4.28 pytest==2.7.1 - mccabe==0.3 + mccabe==0.3.1 pytest-mccabe==0.1 commands = {envpython} scripts/link_pyqt.py --tox {envdir} From a545b919f7f8b4383319c710ad948dca251e76ad Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 2 Jun 2015 23:34:55 +0200 Subject: [PATCH 081/146] Do history loading after qutebrowser has started. --- qutebrowser/app.py | 21 +++++++- qutebrowser/browser/history.py | 61 +++++++++++++++++----- qutebrowser/completion/models/instances.py | 5 ++ qutebrowser/completion/models/urlmodel.py | 2 +- 4 files changed, 74 insertions(+), 15 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 2e8f7ea6a..c4ebef460 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -50,7 +50,7 @@ from qutebrowser.mainwindow import mainwindow from qutebrowser.misc import readline, ipc, savemanager, sessions, crashsignal from qutebrowser.misc import utilcmds # pylint: disable=unused-import from qutebrowser.utils import (log, version, message, utils, qtutils, urlutils, - objreg, usertypes, standarddir, error) + objreg, usertypes, standarddir, error, debug) # We import utilcmds to run the cmdutils.register decorators. @@ -148,7 +148,9 @@ def init(args, crash_handler): error.handle_fatal_exc(e, args, "Error while initializing!", pre_text="Error while initializing") sys.exit(usertypes.Exit.err_init) + QTimer.singleShot(0, functools.partial(_process_args, args)) + QTimer.singleShot(10, functools.partial(_init_late_modules, args)) log.init.debug("Initializing eventfilter...") event_filter = EventFilter(qApp) @@ -428,6 +430,23 @@ def _init_modules(args, crash_handler): objreg.get('config').changed.connect(_maybe_hide_mouse_cursor) +def _init_late_modules(args): + """Initialize modules which can be inited after the window is shown.""" + try: + log.init.debug("Reading web history...") + reader = objreg.get('web-history').async_read() + with debug.log_time(log.init, 'Reading history'): + while True: + QApplication.processEvents() + next(reader) + except StopIteration: + pass + except (OSError, UnicodeDecodeError) as e: + error.handle_fatal_exc(e, args, "Error while initializing!", + pre_text="Error while initializing") + sys.exit(usertypes.Exit.err_init) + + class Quitter: """Utility class to quit/restart the QApplication. diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py index 2ffbf70c1..72f5aa639 100644 --- a/qutebrowser/browser/history.py +++ b/qutebrowser/browser/history.py @@ -67,23 +67,30 @@ class WebHistory(QWebHistoryInterface): _history_dict: An OrderedDict of URLs read from the on-disk history. _new_history: A list of HistoryEntry items of the current session. _saved_count: How many HistoryEntries have been written to disk. + _initial_read_started: Whether async_read was called. + _initial_read_done: Whether async_read has completed. + _temp_history: OrderedDict of temporary history entries before + async_read was called. Signals: - item_about_to_be_added: Emitted before a new HistoryEntry is added. - arg: The new HistoryEntry. + add_completion_item: Emitted before a new HistoryEntry is added. + arg: The new HistoryEntry. item_added: Emitted after a new HistoryEntry is added. arg: The new HistoryEntry. """ - item_about_to_be_added = pyqtSignal(HistoryEntry) + add_completion_item = pyqtSignal(HistoryEntry) item_added = pyqtSignal(HistoryEntry) + async_read_done = pyqtSignal() def __init__(self, parent=None): super().__init__(parent) + self._initial_read_started = False + self._initial_read_done = False self._lineparser = lineparser.AppendLineParser( standarddir.data(), 'history', parent=self) self._history_dict = collections.OrderedDict() - self._read_history() + self._temp_history = collections.OrderedDict() self._new_history = [] self._saved_count = 0 objreg.get('save-manager').add_saveable( @@ -101,12 +108,21 @@ class WebHistory(QWebHistoryInterface): def __len__(self): return len(self._history_dict) - def _read_history(self): + def async_read(self): """Read the initial history.""" - if standarddir.data() is None: + if self._initial_read_started: + log.init.debug("Ignoring async_read() because reading is started.") return + self._initial_read_started = True + + if standarddir.data() is None: + self._initial_read_done = True + self.async_read_done.emit() + return + with self._lineparser.open(): for line in self._lineparser: + yield data = line.rstrip().split(maxsplit=1) if not data: # empty line @@ -128,8 +144,24 @@ class WebHistory(QWebHistoryInterface): # information about previous hits change the items in # old_urls to be lists or change HistoryEntry to have a # list of atimes. - self._history_dict[url] = HistoryEntry(atime, url) - self._history_dict.move_to_end(url) + entry = HistoryEntry(atime, url) + self._add_entry(entry) + + self._initial_read_done = True + self.async_read_done.emit() + + for url, entry in self._temp_history.items(): + entry = HistoryEntry(atime, url) + self._new_history.append(entry) + self._add_entry(entry) + self.add_completion_item.emit(entry) + + def _add_entry(self, entry, target=None): + """Add an entry to self._history_dict or another given OrderedDict.""" + if target is None: + target = self._history_dict + target[entry.url_string] = entry + target.move_to_end(entry.url_string) def get_recent(self): """Get the most recent history entries.""" @@ -151,13 +183,16 @@ class WebHistory(QWebHistoryInterface): """ if not url_string: return - if not config.get('general', 'private-browsing'): - entry = HistoryEntry(time.time(), url_string) - self.item_about_to_be_added.emit(entry) + if config.get('general', 'private-browsing'): + return + entry = HistoryEntry(time.time(), url_string) + if self._initial_read_done: + self.add_completion_item.emit(entry) self._new_history.append(entry) - self._history_dict[url_string] = entry - self._history_dict.move_to_end(url_string) + self._add_entry(entry) self.item_added.emit(entry) + else: + self._add_entry(entry, target=self._temp_history) def historyContains(self, url_string): """Called by WebKit to determine if an URL is contained in the history. diff --git a/qutebrowser/completion/models/instances.py b/qutebrowser/completion/models/instances.py index 85998357f..e39ca1692 100644 --- a/qutebrowser/completion/models/instances.py +++ b/qutebrowser/completion/models/instances.py @@ -165,6 +165,11 @@ def init(): quickmark_manager.changed.connect( functools.partial(update, [usertypes.Completion.quickmark_by_url, usertypes.Completion.quickmark_by_name])) + session_manager = objreg.get('session-manager') session_manager.update_completion.connect( functools.partial(update, [usertypes.Completion.sessions])) + + history = objreg.get('web-history') + history.async_read_done.connect( + functools.partial(update, [usertypes.Completion.url])) diff --git a/qutebrowser/completion/models/urlmodel.py b/qutebrowser/completion/models/urlmodel.py index e8898ec85..1eb7e13d2 100644 --- a/qutebrowser/completion/models/urlmodel.py +++ b/qutebrowser/completion/models/urlmodel.py @@ -54,7 +54,7 @@ class UrlCompletionModel(base.BaseCompletionModel): history = utils.newest_slice(self._history, max_history) for entry in history: self._add_history_entry(entry) - self._history.item_about_to_be_added.connect( + self._history.add_completion_item.connect( self.on_history_item_added) objreg.get('config').changed.connect(self.reformat_timestamps) From 0119cf510f0694cbd55360a4cfffbf74422b091b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 16 Jun 2015 07:16:02 +0200 Subject: [PATCH 082/146] Fix loading of _temp_history. --- qutebrowser/browser/history.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py index 72f5aa639..ed9c55e7f 100644 --- a/qutebrowser/browser/history.py +++ b/qutebrowser/browser/history.py @@ -151,7 +151,6 @@ class WebHistory(QWebHistoryInterface): self.async_read_done.emit() for url, entry in self._temp_history.items(): - entry = HistoryEntry(atime, url) self._new_history.append(entry) self._add_entry(entry) self.add_completion_item.emit(entry) From e7b84d40892597126a3d97555dfe0093af2132eb Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 16 Jun 2015 07:16:32 +0200 Subject: [PATCH 083/146] Update changelog. --- CHANGELOG.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 8db54d7bf..70da716b4 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -55,6 +55,7 @@ Changed - `:scroll` now takes a direction argument (`up`/`down`/`left`/`right`/`top`/`bottom`/`page-up`/`page-down`) instead of two pixel arguments (`dx`/`dy`). The old form still works but is deprecated. - The `ui -> user-stylesheet` setting now also takes file paths relative to the config directory. - The `content -> cookies-accept` setting now has new `no-3rdparty` (default) and `no-unknown-3rdparty` values to block third-party cookies. The `default` value got renamed to `all`. +- Improved startup time by reading the webpage history while qutebrowser is open. Deprecated ~~~~~~~~~~ From a024c14dd65c7297122f86666b94d796e6ef729a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 16 Jun 2015 07:37:08 +0200 Subject: [PATCH 084/146] tox: Add smoke test to the default envlist. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index e53117d97..2013d1460 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = unittests,misc,pep257,pyflakes,pep8,mccabe,pylint,pyroma,check-manifest +envlist = smoke,unittests,misc,pep257,pyflakes,pep8,mccabe,pylint,pyroma,check-manifest [testenv] passenv = PYTHON From dfe98d10534cea30eccb97fa4bba13d01aba709b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 16 Jun 2015 13:22:55 +0200 Subject: [PATCH 085/146] completion: Fix initial _cursor_part value. Fixes #749. --- qutebrowser/completion/completer.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py index 197c62ce1..ebcaed67d 100644 --- a/qutebrowser/completion/completer.py +++ b/qutebrowser/completion/completer.py @@ -328,7 +328,7 @@ class Completer(QObject): cursor_pos)) skip = 0 for i, part in enumerate(parts): - log.completion.vdebug("Checking part {}: {}".format(i, parts[i])) + log.completion.vdebug("Checking part {}: {!r}".format(i, parts[i])) if not part: skip += 1 continue @@ -350,7 +350,11 @@ class Completer(QObject): "Removing len({!r}) -> {} from cursor_pos -> {}".format( part, len(part), cursor_pos)) else: - self._cursor_part = i - skip + if i == 0: + # Initial `:` press without any text. + self._cursor_part = 0 + else: + self._cursor_part = i - skip if spaces: self._empty_item_idx = i - skip else: From b1334bcc22a0ccebbfecc3a944d4b57d2cca86d8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Jun 2015 06:46:03 +0200 Subject: [PATCH 086/146] Use repr() for unknown objects in utils.qualname. --- qutebrowser/utils/utils.py | 2 +- tests/utils/test_utils.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index b5f9ac25c..526a2a5e0 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -553,7 +553,7 @@ def qualname(obj): elif hasattr(obj, '__name__'): name = obj.__name__ else: - name = '' + name = repr(obj) if inspect.isclass(obj) or inspect.isfunction(obj): module = obj.__module__ diff --git a/tests/utils/test_utils.py b/tests/utils/test_utils.py index 04291dfdd..cc3d1817e 100644 --- a/tests/utils/test_utils.py +++ b/tests/utils/test_utils.py @@ -721,8 +721,11 @@ def qualname_func(_blah): pass +QUALNAME_OBJ = QualnameObj() + + @pytest.mark.parametrize('obj, expected', [ - (QualnameObj(), ''), # instance - unknown + (QUALNAME_OBJ, repr(QUALNAME_OBJ)), # instance - unknown (QualnameObj, 'test_utils.QualnameObj'), # class (QualnameObj.func, 'test_utils.QualnameObj.func'), # unbound method (QualnameObj().func, 'test_utils.QualnameObj.func'), # bound method From 6dbdea0ee3fa52d08cc135dba65924792b38395c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Jun 2015 07:57:38 +0200 Subject: [PATCH 087/146] Set maxsplit=0 for :spawn and split manually. Fixes #759. --- CHANGELOG.asciidoc | 1 + doc/help/commands.asciidoc | 8 ++++++-- qutebrowser/browser/commands.py | 18 ++++++++++++------ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 70da716b4..234ebee8c 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -56,6 +56,7 @@ Changed - The `ui -> user-stylesheet` setting now also takes file paths relative to the config directory. - The `content -> cookies-accept` setting now has new `no-3rdparty` (default) and `no-unknown-3rdparty` values to block third-party cookies. The `default` value got renamed to `all`. - Improved startup time by reading the webpage history while qutebrowser is open. +- The way `:spawn` splits its commandline has been changed slightly to allow commands with flags. Deprecated ~~~~~~~~~~ diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index fe71e8eb0..7fccc2117 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -529,20 +529,24 @@ Preset the statusbar to some text. [[spawn]] === spawn -Syntax: +:spawn [*--userscript*] [*--verbose*] [*--detach*] 'args' ['args' ...]+ +Syntax: +:spawn [*--userscript*] [*--verbose*] [*--detach*] 'cmdline'+ Spawn a command in a shell. Note the {url} variable which gets replaced by the current URL might be useful here. ==== positional arguments -* +'args'+: The commandline to execute. +* +'cmdline'+: The commandline to execute. ==== optional arguments * +*-u*+, +*--userscript*+: Run the command as an userscript. * +*-v*+, +*--verbose*+: Show notifications when the command started/exited. * +*-d*+, +*--detach*+: Whether the command should be detached from qutebrowser. +==== note +* This command does not split arguments after the last argument and handles quotes literally. +* With this command, +;;+ is interpreted literally instead of splitting off a second command. + [[stop]] === stop Stop loading in the current/[count]th tab. diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index af8815a78..4abb50710 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -21,6 +21,7 @@ import re import os +import shlex import posixpath import functools import xml.etree.ElementTree @@ -919,8 +920,9 @@ class CommandDispatcher: finally: self._tabbed_browser.setUpdatesEnabled(True) - @cmdutils.register(instance='command-dispatcher', scope='window') - def spawn(self, userscript=False, verbose=False, detach=False, *args): + @cmdutils.register(instance='command-dispatcher', scope='window', + maxsplit=0) + def spawn(self, cmdline, userscript=False, verbose=False, detach=False): """Spawn a command in a shell. Note the {url} variable which gets replaced by the current URL might be @@ -930,11 +932,15 @@ class CommandDispatcher: userscript: Run the command as an userscript. verbose: Show notifications when the command started/exited. detach: Whether the command should be detached from qutebrowser. - *args: The commandline to execute. + cmdline: The commandline to execute. """ - log.procs.debug("Executing: {}, userscript={}".format( - args, userscript)) - cmd, *args = args + try: + cmd, *args = shlex.split(cmdline) + except ValueError as e: + raise cmdexc.CommandError("Error while splitting command: " + "{}".format(e)) + log.procs.debug("Executing {} with args {}, userscript={}".format( + cmd, args, userscript)) if userscript: self.run_userscript(cmd, *args, verbose=verbose) else: From 703b0043dbab36c8e98a98aacc438dc1b03fbd93 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Jun 2015 17:48:25 +0200 Subject: [PATCH 088/146] tox: Update pyflakes to 0.9.2. Upstream changelog: - Fix a traceback when a global is defined in one scope, and used in another. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 2013d1460..574e0a941 100644 --- a/tox.ini +++ b/tox.ini @@ -80,7 +80,7 @@ deps = -r{toxinidir}/requirements.txt py==1.4.28 pytest==2.7.1 - pyflakes==0.9.1 + pyflakes==0.9.2 pytest-flakes==1.0.0 commands = {envpython} scripts/link_pyqt.py --tox {envdir} From 59cdbd780c8911d3ada47c3010730067c91db414 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Jun 2015 07:01:30 +0200 Subject: [PATCH 089/146] Fix {url} substitution with :spawn. See #759. This is a regression introduced in 6dbdea0ee3fa52d08cc135dba65924792b38395c. --- qutebrowser/browser/commands.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 4abb50710..625ceb05f 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -36,7 +36,7 @@ import pygments import pygments.lexers import pygments.formatters -from qutebrowser.commands import userscripts, cmdexc, cmdutils +from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners from qutebrowser.config import config, configexc from qutebrowser.browser import webelem, inspector from qutebrowser.keyinput import modeman @@ -939,6 +939,9 @@ class CommandDispatcher: except ValueError as e: raise cmdexc.CommandError("Error while splitting command: " "{}".format(e)) + + args = runners.replace_variables(self._win_id, args) + log.procs.debug("Executing {} with args {}, userscript={}".format( cmd, args, userscript)) if userscript: From c64d9520fff878a49055058669840d857b3a63fc Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Jun 2015 08:09:45 +0200 Subject: [PATCH 090/146] Fix lint. Thanks to @Carpetsmoker for spotting this in #705. --- qutebrowser/browser/network/qutescheme.py | 2 +- qutebrowser/completion/completionwidget.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/network/qutescheme.py b/qutebrowser/browser/network/qutescheme.py index 0edbf1032..7a9f3bf8d 100644 --- a/qutebrowser/browser/network/qutescheme.py +++ b/qutebrowser/browser/network/qutescheme.py @@ -159,7 +159,7 @@ def qute_help(win_id, request): url=request.url().toDisplayString(), error="This most likely means the documentation was not generated " "properly. If you are running qutebrowser from the git " - "repository, please run scripts/asciidoc2html.py." + "repository, please run scripts/asciidoc2html.py. " "If you're running a released version this is a bug, please " "use :report to report it.", icon='') diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index 0bd6b04c9..58532058d 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -101,7 +101,7 @@ class CompletionView(QTreeView): self.enabled = config.get('completion', 'show') objreg.get('config').changed.connect(self.set_enabled) # FIXME handle new aliases. - #objreg.get('config').changed.connect(self.init_command_completion) + # objreg.get('config').changed.connect(self.init_command_completion) self._delegate = completiondelegate.CompletionItemDelegate(self) self.setItemDelegate(self._delegate) From 0350d19bd39a566516171a59382d627945ecd0d7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Jun 2015 10:32:07 +0200 Subject: [PATCH 091/146] Load geometry after completion is initialized. Fixes #766. --- qutebrowser/mainwindow/mainwindow.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 4bafd76e5..d9e58802a 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -120,14 +120,6 @@ class MainWindow(QWidget): window=self.win_id) self.setWindowTitle('qutebrowser') - if geometry is not None: - self._load_geometry(geometry) - elif self.win_id == 0: - self._load_state_geometry() - else: - self._set_default_geometry() - log.init.debug("Initial main window geometry: {}".format( - self.geometry())) self._vbox = QVBoxLayout(self) self._vbox.setContentsMargins(0, 0, 0, 0) self._vbox.setSpacing(0) @@ -165,6 +157,15 @@ class MainWindow(QWidget): log.init.debug("Initializing modes...") modeman.init(self.win_id, self) + if geometry is not None: + self._load_geometry(geometry) + elif self.win_id == 0: + self._load_state_geometry() + else: + self._set_default_geometry() + log.init.debug("Initial main window geometry: {}".format( + self.geometry())) + self._connect_signals() # When we're here the statusbar might not even really exist yet, so From c3c52220f62c22b9e324f7428e95d4978d344b1f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Jun 2015 11:57:55 +0200 Subject: [PATCH 092/146] Update changelog. --- CHANGELOG.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 234ebee8c..3c1fc21cb 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -74,6 +74,7 @@ Fixed - Scrolling should now work more reliably on some pages where arrow keys worked but `hjkl` didn't. - Small improvements when checking if an input is an URL or not. +- Fixed wrong cursor position when completing the first item in the completion. v0.2.2 (unreleased) ------------------- From a5c078516b91eae1ef10a7b528879e96ce128138 Mon Sep 17 00:00:00 2001 From: Lamar Pavel Date: Thu, 18 Jun 2015 13:15:00 +0200 Subject: [PATCH 093/146] Fix link to contribution guidelines in README --- README.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.asciidoc b/README.asciidoc index 03aefa777..a1ba8fa90 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -68,7 +68,7 @@ Contributions / Bugs -------------------- You want to contribute to qutebrowser? Awesome! Please read -link:doc/CONTRIBUTING.asciidoc[the contribution guidelines] for details and +link:CONTRIBUTING.asciidoc[the contribution guidelines] for details and useful hints. If you found a bug or have a feature request, you can report it in several From 1cc6a6669b24d22cd70681c9b6cb0b5d7f531b5b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Jun 2015 15:02:30 +0200 Subject: [PATCH 094/146] Bind to rl-unix-word-rubout. --- qutebrowser/config/configdata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index a19a4fcdb..a39439d7c 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1348,7 +1348,7 @@ KEY_DATA = collections.OrderedDict([ ('rl-unix-line-discard', ['']), ('rl-kill-line', ['']), ('rl-kill-word', ['']), - ('rl-unix-word-rubout', ['']), + ('rl-unix-word-rubout', ['', '']), ('rl-yank', ['']), ('rl-delete-char', ['']), ('rl-backward-delete-char', ['']), From c72da37916153868a4037cf9a434422e12eaa7be Mon Sep 17 00:00:00 2001 From: Lamar Pavel Date: Thu, 18 Jun 2015 13:15:00 +0200 Subject: [PATCH 095/146] Fix link to contribution guidelines in README --- README.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.asciidoc b/README.asciidoc index 03aefa777..a1ba8fa90 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -68,7 +68,7 @@ Contributions / Bugs -------------------- You want to contribute to qutebrowser? Awesome! Please read -link:doc/CONTRIBUTING.asciidoc[the contribution guidelines] for details and +link:CONTRIBUTING.asciidoc[the contribution guidelines] for details and useful hints. If you found a bug or have a feature request, you can report it in several From 9ca06ecfa2fdd3f676b3077340e7a7dabb495afd Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Jun 2015 18:45:51 +0200 Subject: [PATCH 096/146] Use pkg_resources instead of distutils for version Fixes #767. See #770. --- qutebrowser/misc/crashdialog.py | 7 +++---- qutebrowser/utils/qtutils.py | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py index dbb473b4e..64cccde83 100644 --- a/qutebrowser/misc/crashdialog.py +++ b/qutebrowser/misc/crashdialog.py @@ -24,9 +24,8 @@ import sys import html import getpass import traceback -import distutils.version # pylint: disable=no-name-in-module,import-error -# https://bitbucket.org/logilab/pylint/issue/73/ +import pkg_resources from PyQt5.QtCore import pyqtSlot, Qt, QSize, qVersion from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton, QVBoxLayout, QHBoxLayout, QCheckBox, @@ -328,8 +327,8 @@ class _CrashDialog(QDialog): """ # pylint: disable=no-member # https://bitbucket.org/logilab/pylint/issue/73/ - new_version = distutils.version.StrictVersion(newest) - cur_version = distutils.version.StrictVersion(qutebrowser.__version__) + new_version = pkg_resources.parse_version(newest) + cur_version = pkg_resources.parse_version(qutebrowser.__version__) lines = ['The report has been sent successfully. Thanks!'] if new_version > cur_version: lines.append("Note: The newest available version is v{}, " diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 6573306ab..8073b4bbb 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -31,10 +31,9 @@ import io import os import sys import operator -import distutils.version # pylint: disable=no-name-in-module,import-error -# https://bitbucket.org/logilab/pylint/issue/73/ import contextlib +import pkg_resources from PyQt5.QtCore import (qVersion, QEventLoop, QDataStream, QByteArray, QIODevice, QSaveFile) from PyQt5.QtWidgets import QApplication @@ -60,8 +59,8 @@ def version_check(version, op=operator.ge): """ # pylint: disable=no-member # https://bitbucket.org/logilab/pylint/issue/73/ - return op(distutils.version.StrictVersion(qVersion()), - distutils.version.StrictVersion(version)) + return op(pkg_resources.parse_version(qVersion()), + pkg_resources.parse_version(version)) def check_overflow(arg, ctype, fatal=True): From 0d59a1cba815eab7373ea7b82dcc6765c4b387c1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Jun 2015 21:08:04 +0200 Subject: [PATCH 097/146] Include javascript folder when freezing. See #770. --- scripts/freeze.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/freeze.py b/scripts/freeze.py index c5f13cdbb..e33290551 100755 --- a/scripts/freeze.py +++ b/scripts/freeze.py @@ -51,6 +51,7 @@ build_exe_options = { 'include_files': [ ('qutebrowser/html', 'html'), ('qutebrowser/html/doc', 'html/doc'), + ('qutebrowser/javascript', 'javascript'), ('qutebrowser/git-commit-id', 'git-commit-id'), ], 'include_msvcr': True, From 3de1299650373217b1bc871e24d8190758fe97e3 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Jun 2015 21:08:30 +0200 Subject: [PATCH 098/146] tests: Use utils.read_file to get javascript files. This will make those tests pass when frozen. See #770. --- tests/javascript/conftest.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/javascript/conftest.py b/tests/javascript/conftest.py index 85b56a577..3bc65aff1 100644 --- a/tests/javascript/conftest.py +++ b/tests/javascript/conftest.py @@ -28,7 +28,7 @@ import jinja2 from PyQt5.QtWebKit import QWebSettings from PyQt5.QtWebKitWidgets import QWebView, QWebPage -import qutebrowser +from qutebrowser.utils import utils class TestWebPage(QWebPage): @@ -107,11 +107,7 @@ class JSTester: Return: The javascript return value. """ - base_path = os.path.join(os.path.dirname(qutebrowser.__file__), - 'javascript') - full_path = os.path.join(base_path, filename) - with open(full_path, 'r', encoding='utf-8') as f: - source = f.read() + source = utils.read_file(os.path.join('javascript', filename)) return self.run(source) def run(self, source): From 2f59abaf1371a98f0462b8cd73cd1acc690c2f79 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Jun 2015 22:37:56 +0200 Subject: [PATCH 099/146] Add empty includes=[] to freeze.py. This makes freeze_tests.py easier. --- scripts/freeze.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/freeze.py b/scripts/freeze.py index e33290551..f2b979036 100755 --- a/scripts/freeze.py +++ b/scripts/freeze.py @@ -55,6 +55,7 @@ build_exe_options = { ('qutebrowser/git-commit-id', 'git-commit-id'), ], 'include_msvcr': True, + 'includes': [], 'excludes': ['tkinter'], 'packages': ['pygments'], } From 425a6d33cfb558d3de7c58aebc7de265e89c2685 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 19 Jun 2015 06:37:12 +0200 Subject: [PATCH 100/146] Add __name__ == '__main__' block in freeze.py. freeze.py now gets imported from freeze_tests.py, and shouldn't run its own setup in that case. --- scripts/freeze.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/scripts/freeze.py b/scripts/freeze.py index f2b979036..888127d5b 100755 --- a/scripts/freeze.py +++ b/scripts/freeze.py @@ -94,19 +94,21 @@ executable = cx.Executable('qutebrowser/__main__.py', base=base, icon=os.path.join(BASEDIR, 'icons', 'qutebrowser.ico')) -try: - setupcommon.write_git_file() - cx.setup( - executables=[executable], - options={ - 'build_exe': build_exe_options, - 'bdist_msi': bdist_msi_options, - 'bdist_mac': bdist_mac_options, - 'bdist_dmg': bdist_dmg_options, - }, - **setupcommon.setupdata - ) -finally: - path = os.path.join(BASEDIR, 'qutebrowser', 'git-commit-id') - if os.path.exists(path): - os.remove(path) + +if __name__ == '__main__': + try: + setupcommon.write_git_file() + cx.setup( + executables=[executable], + options={ + 'build_exe': build_exe_options, + 'bdist_msi': bdist_msi_options, + 'bdist_mac': bdist_mac_options, + 'bdist_dmg': bdist_dmg_options, + }, + **setupcommon.setupdata + ) + finally: + path = os.path.join(BASEDIR, 'qutebrowser', 'git-commit-id') + if os.path.exists(path): + os.remove(path) From 3ba63128da2cb2b8cc5991e5f2c949096f99d0df Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Jun 2015 20:44:22 +0200 Subject: [PATCH 101/146] Add a smoke-frozen testenv. See #770. --- .appveyor.yml | 1 + tox.ini | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/.appveyor.yml b/.appveyor.yml index d2488cfe4..03374aedd 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -11,6 +11,7 @@ install: test_script: - C:\Python34\Scripts\tox -e smoke + - C:\Python34\Scripts\tox -e smoke-frozen - C:\Python34\Scripts\tox -e unittests - C:\Python34\Scripts\tox -e pyflakes - C:\Python34\Scripts\tox -e pylint diff --git a/tox.ini b/tox.ini index 574e0a941..2a502f715 100644 --- a/tox.ini +++ b/tox.ini @@ -146,6 +146,18 @@ commands = {envpython} scripts/link_pyqt.py --tox {envdir} {envpython} -m qutebrowser --no-err-windows --nowindow --temp-basedir about:blank ":later 500 quit" +[testenv:smoke-frozen] +setenv = {[testenv:smoke]setenv} +passenv = {[testenv:smoke]passenv} +skip_install = true +deps = + {[testenv:smoke]deps} + cx_Freeze==4.3.4 +commands = + {envpython} scripts/link_pyqt.py --tox {envdir} + {envpython} scripts/freeze.py build_exe -b {envdir}/build + {envdir}/build/qutebrowser --no-err-windows --nowindow --temp-basedir about:blank ":later 500 quit" + [pytest] norecursedirs = .tox .venv markers = From 42b5ee831e0751485bc40c0b49e1feac62b523b0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Jun 2015 11:07:23 +0200 Subject: [PATCH 102/146] Add a unittests-frozen testenv. See #770. --- .appveyor.yml | 1 + scripts/freeze_tests.py | 82 +++++++++++++++++++++++++++++++++++++ scripts/run_frozen_tests.py | 33 +++++++++++++++ tox.ini | 12 ++++++ 4 files changed, 128 insertions(+) create mode 100755 scripts/freeze_tests.py create mode 100644 scripts/run_frozen_tests.py diff --git a/.appveyor.yml b/.appveyor.yml index 03374aedd..3ab4cda97 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -13,5 +13,6 @@ test_script: - C:\Python34\Scripts\tox -e smoke - C:\Python34\Scripts\tox -e smoke-frozen - C:\Python34\Scripts\tox -e unittests + - C:\Python34\Scripts\tox -e unittests-frozen - C:\Python34\Scripts\tox -e pyflakes - C:\Python34\Scripts\tox -e pylint diff --git a/scripts/freeze_tests.py b/scripts/freeze_tests.py new file mode 100755 index 000000000..b87e26a65 --- /dev/null +++ b/scripts/freeze_tests.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2015 Florian Bruhin (The Compiler) +# +# 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 . + +"""cx_Freeze script to freeze qutebrowser and its tests.""" + + +import os +import os.path +import sys +import contextlib + +import cx_Freeze as cx # pylint: disable=import-error +# cx_Freeze is hard to install (needs C extensions) so we don't check for it. +import pytest + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) +from scripts import setupcommon +from scripts.freeze import build_exe_options as normal_build_exe_options + + +@contextlib.contextmanager +def temp_git_commit_file(): + """Context manager to temporarily create a fake git-commit-id file.""" + basedir = os.path.join(os.path.dirname(os.path.realpath(__file__)), + os.path.pardir) + path = os.path.join(basedir, 'qutebrowser', 'git-commit-id') + with open(path, 'wb') as f: + f.write(b'fake-frozen-git-commit') + yield + os.remove(path) + + +def get_build_exe_options(): + """Get build_exe options with additional includes.""" + opts = dict(normal_build_exe_options) + # Remove documentation from include_files. + skipped_include_files = [ + os.path.join('qutebrowser', 'html'), + os.path.join('qutebrowser', 'html', 'doc') + ] + include_files = [e for e in normal_build_exe_options['include_files'] + if e[0] not in skipped_include_files] + opts['include_files'] = include_files + + opts['includes'] += pytest.freeze_includes() # pylint: disable=no-member + opts['includes'] += ['unittest.mock', 'PyQt5.QtTest'] + + opts['packages'].append('qutebrowser') + + return opts + + +def main(): + """Main entry point.""" + with temp_git_commit_file(): + cx.setup( + executables=[cx.Executable('scripts/run_frozen_tests.py', + targetName='run-frozen-tests')], + options={'build_exe': get_build_exe_options()}, + **setupcommon.setupdata + ) + + +if __name__ == '__main__': + main() diff --git a/scripts/run_frozen_tests.py b/scripts/run_frozen_tests.py new file mode 100644 index 000000000..dd70c0505 --- /dev/null +++ b/scripts/run_frozen_tests.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2015 Florian Bruhin (The Compiler) +# +# 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 . + +# pylint: disable=import-error,no-member + +"""cx_Freeze script to run qutebrowser tests on the frozen executable.""" + +import sys + +import pytest +import pytestqt.plugin +import pytest_mock +import pytest_capturelog + +sys.exit(pytest.main(plugins=[pytestqt.plugin, pytest_mock, + pytest_capturelog])) diff --git a/tox.ini b/tox.ini index 2a502f715..0ae696cb3 100644 --- a/tox.ini +++ b/tox.ini @@ -31,6 +31,18 @@ commands = {envpython} scripts/link_pyqt.py --tox {envdir} {envpython} -m py.test --strict -rfEsw {posargs} +[testenv:unittests-frozen] +setenv = {[testenv:unittests]setenv} +passenv = {[testenv:unittests]passenv} +skip_install = true +deps = + {[testenv:unittests]deps} + cx_Freeze==4.3.4 +commands = + {envpython} scripts/link_pyqt.py --tox {envdir} + {envpython} scripts/freeze_tests.py build_exe -b {envdir}/build + {envdir}/build/run-frozen-tests --strict -rfEsw {posargs} + [testenv:coverage] passenv = PYTHON DISPLAY XAUTHORITY HOME deps = From 894ec7d66a0a2f91612a7cc1f9e487d0ed879863 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 19 Jun 2015 07:45:37 +0200 Subject: [PATCH 103/146] Use a function for build_exe_options in freeze.py. --- scripts/freeze.py | 37 +++++++++++++++++++++++-------------- scripts/freeze_tests.py | 16 ++-------------- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/scripts/freeze.py b/scripts/freeze.py index 888127d5b..750ab94e8 100755 --- a/scripts/freeze.py +++ b/scripts/freeze.py @@ -47,22 +47,31 @@ def get_egl_path(): return os.path.join(distutils.sysconfig.get_python_lib(), r'PyQt5\libEGL.dll') -build_exe_options = { - 'include_files': [ - ('qutebrowser/html', 'html'), - ('qutebrowser/html/doc', 'html/doc'), + +def get_build_exe_options(skip_html=False): + include_files = [ ('qutebrowser/javascript', 'javascript'), ('qutebrowser/git-commit-id', 'git-commit-id'), - ], - 'include_msvcr': True, - 'includes': [], - 'excludes': ['tkinter'], - 'packages': ['pygments'], -} + ] + + if not skip_html: + include_files += [ + ('qutebrowser/html', 'html'), + ('qutebrowser/html/doc', 'html/doc'), + ] + + egl_path = get_egl_path() + if egl_path is not None: + include_files.append((egl_path, 'libEGL.dll')) + + return { + 'include_files': include_files, + 'include_msvcr': True, + 'includes': [], + 'excludes': ['tkinter'], + 'packages': ['pygments'], + } -egl_path = get_egl_path() -if egl_path is not None: - build_exe_options['include_files'].append((egl_path, 'libEGL.dll')) bdist_msi_options = { # random GUID generated by uuid.uuid4() @@ -101,7 +110,7 @@ if __name__ == '__main__': cx.setup( executables=[executable], options={ - 'build_exe': build_exe_options, + 'build_exe': get_build_exe_options(), 'bdist_msi': bdist_msi_options, 'bdist_mac': bdist_mac_options, 'bdist_dmg': bdist_dmg_options, diff --git a/scripts/freeze_tests.py b/scripts/freeze_tests.py index b87e26a65..7cd7022a9 100755 --- a/scripts/freeze_tests.py +++ b/scripts/freeze_tests.py @@ -31,8 +31,7 @@ import cx_Freeze as cx # pylint: disable=import-error import pytest sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) -from scripts import setupcommon -from scripts.freeze import build_exe_options as normal_build_exe_options +from scripts import setupcommon, freeze @contextlib.contextmanager @@ -49,21 +48,10 @@ def temp_git_commit_file(): def get_build_exe_options(): """Get build_exe options with additional includes.""" - opts = dict(normal_build_exe_options) - # Remove documentation from include_files. - skipped_include_files = [ - os.path.join('qutebrowser', 'html'), - os.path.join('qutebrowser', 'html', 'doc') - ] - include_files = [e for e in normal_build_exe_options['include_files'] - if e[0] not in skipped_include_files] - opts['include_files'] = include_files - + opts = freeze.get_build_exe_options(skip_html=True) opts['includes'] += pytest.freeze_includes() # pylint: disable=no-member opts['includes'] += ['unittest.mock', 'PyQt5.QtTest'] - opts['packages'].append('qutebrowser') - return opts From b0012fd4102716c0f1d98a7f6d6fa4ff38b1057c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 19 Jun 2015 08:45:22 +0200 Subject: [PATCH 104/146] Freeze utils/testfile when freezing tests. --- scripts/freeze.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/freeze.py b/scripts/freeze.py index 750ab94e8..435c9c6e5 100755 --- a/scripts/freeze.py +++ b/scripts/freeze.py @@ -52,6 +52,7 @@ def get_build_exe_options(skip_html=False): include_files = [ ('qutebrowser/javascript', 'javascript'), ('qutebrowser/git-commit-id', 'git-commit-id'), + ('qutebrowser/utils/testfile', 'utils/testfile'), ] if not skip_html: From 08c8a5f7dd1c742e535a170942630140bf83ee39 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Jun 2015 22:36:12 +0200 Subject: [PATCH 105/146] Skip tests which need sys.executable when frozen. See #770 --- tests/misc/test_guiprocess.py | 10 +++++++++- tests/utils/test_qtutils.py | 2 ++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/misc/test_guiprocess.py b/tests/misc/test_guiprocess.py index a975c53f2..66166a7c7 100644 --- a/tests/misc/test_guiprocess.py +++ b/tests/misc/test_guiprocess.py @@ -33,6 +33,10 @@ from qutebrowser.misc import guiprocess # FIXME check statusbar messages +no_frozen = pytest.mark.skipif( + getattr(sys, 'frozen', False), reason="Can't be executed when frozen.") + + def _py_proc(code): """Get a python executable and args list which executes the given code.""" return (sys.executable, ['-c', textwrap.dedent(code.strip('\n'))]) @@ -64,6 +68,7 @@ def fake_proc(monkeypatch, stubs): return p +@no_frozen def test_start(proc, qtbot): """Test simply starting a process.""" with qtbot.waitSignals([proc.started, proc.finished], raising=True, @@ -75,7 +80,7 @@ def test_start(proc, qtbot): @pytest.mark.parametrize('argv', [ - _py_proc('import sys; sys.exit(0)'), + no_frozen(_py_proc('import sys; sys.exit(0)')), ('does_not', 'exist'), ]) def test_start_detached(fake_proc, argv): @@ -85,6 +90,7 @@ def test_start_detached(fake_proc, argv): fake_proc._proc.startDetached.assert_called_with(*list(argv) + [None]) +@no_frozen def test_double_start(qtbot, proc): """Test starting a GUIProcess twice.""" with qtbot.waitSignal(proc.started, raising=True, timeout=2000): @@ -94,6 +100,7 @@ def test_double_start(qtbot, proc): proc.start('', []) +@no_frozen def test_double_start_finished(qtbot, proc): """Test starting a GUIProcess twice (with the first call finished).""" with qtbot.waitSignals([proc.started, proc.finished], raising=True, @@ -120,6 +127,7 @@ def test_error(qtbot, proc): proc.start('this_does_not_exist_either', []) +@no_frozen def test_exit_unsuccessful(qtbot, proc): with qtbot.waitSignal(proc.finished, raising=True, timeout=2000): proc.start(*_py_proc('import sys; sys.exit(0)')) diff --git a/tests/utils/test_qtutils.py b/tests/utils/test_qtutils.py index 25bf728c2..63ce3bfda 100644 --- a/tests/utils/test_qtutils.py +++ b/tests/utils/test_qtutils.py @@ -757,6 +757,8 @@ class TestPyQIODevice: with pytest.raises(io.UnsupportedOperation): pyqiodev.seek(0, whence) + @pytest.mark.skipif(getattr(sys, 'frozen', False), + reason="Can't be executed when frozen.") def test_qprocess(self): """Test PyQIODevice with a QProcess which is non-sequential. From 85f6b3c6df922d59e5e59bea5fc43465cc9d00a7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Jun 2015 23:15:08 +0200 Subject: [PATCH 106/146] Fix TestGitStr when frozen. --- tests/utils/test_version.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/utils/test_version.py b/tests/utils/test_version.py index 7978c06a7..c2fe19e83 100644 --- a/tests/utils/test_version.py +++ b/tests/utils/test_version.py @@ -111,11 +111,20 @@ class TestGitStr: commit_file_mock.side_effect = OSError assert version._git_str() is None + @pytest.mark.skipif(getattr(sys, 'frozen', False), + reason="Can't be executed when frozen!") def test_normal_successful(self, git_str_subprocess_fake): """Test with git returning a successful result.""" git_str_subprocess_fake.retval = 'c0ffeebabe' assert version._git_str() == 'c0ffeebabe' + @pytest.mark.skipif(not getattr(sys, 'frozen', False), + reason="Can only executed when frozen!") + def test_normal_successful_frozen(self, git_str_subprocess_fake): + """Test with git returning a successful result.""" + # The value is defined in scripts/freeze_tests.py. + assert version._git_str() == 'fake-frozen-git-commit' + def test_normal_error(self, commit_file_mock, git_str_subprocess_fake): """Test without repo (but git-commit-id).""" git_str_subprocess_fake.retval = None @@ -130,6 +139,8 @@ class TestGitStr: side_effect=OSError) assert version._git_str() is None + @pytest.mark.skipif(getattr(sys, 'frozen', False), + reason="Can't be executed when frozen!") def test_normal_path_nofile(self, monkeypatch, caplog, git_str_subprocess_fake, commit_file_mock): """Test with undefined __file__ but available git-commit-id.""" From a5610fd6da50e4d37f751cd887203740133d5999 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 19 Jun 2015 07:15:25 +0200 Subject: [PATCH 107/146] Fix TestReadFile when frozen. --- tests/utils/test_utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/utils/test_utils.py b/tests/utils/test_utils.py index cc3d1817e..ed2028d86 100644 --- a/tests/utils/test_utils.py +++ b/tests/utils/test_utils.py @@ -98,9 +98,12 @@ class TestReadFile: @pytest.fixture(autouse=True, params=[True, False]) def freezer(self, request, monkeypatch): - if request.param: + if request.param and not getattr(sys, 'frozen', False): monkeypatch.setattr(sys, 'frozen', True, raising=False) monkeypatch.setattr('sys.executable', qutebrowser.__file__) + elif not request.param and getattr(sys, 'frozen', False): + # Want to test unfrozen tests, but we are frozen + pytest.skip("Can't run with sys.frozen = True!") def test_readfile(self): """Read a test file.""" From fd8258721349ab075b990c1d2055c56746fab22e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 19 Jun 2015 07:48:12 +0200 Subject: [PATCH 108/146] Skip documentation when freezing for smoke-frozen. --- scripts/freeze.py | 4 ++++ tox.ini | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/freeze.py b/scripts/freeze.py index 435c9c6e5..67242cb77 100755 --- a/scripts/freeze.py +++ b/scripts/freeze.py @@ -49,6 +49,10 @@ def get_egl_path(): def get_build_exe_options(skip_html=False): + if '--qute-skip-html' in sys.argv: + skip_html = True + sys.argv.remove('--qute-skip-html') + include_files = [ ('qutebrowser/javascript', 'javascript'), ('qutebrowser/git-commit-id', 'git-commit-id'), diff --git a/tox.ini b/tox.ini index 0ae696cb3..d8605a2d0 100644 --- a/tox.ini +++ b/tox.ini @@ -167,7 +167,7 @@ deps = cx_Freeze==4.3.4 commands = {envpython} scripts/link_pyqt.py --tox {envdir} - {envpython} scripts/freeze.py build_exe -b {envdir}/build + {envpython} scripts/freeze.py build_exe --qute-skip-html -b {envdir}/build {envdir}/build/qutebrowser --no-err-windows --nowindow --temp-basedir about:blank ":later 500 quit" [pytest] From e2c375b87409fc42757be782188a4450bd560fb8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 19 Jun 2015 07:58:45 +0200 Subject: [PATCH 109/146] Add missing docstring for get_build_exe_options(). --- scripts/freeze.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/freeze.py b/scripts/freeze.py index 67242cb77..6185634ec 100755 --- a/scripts/freeze.py +++ b/scripts/freeze.py @@ -49,6 +49,11 @@ def get_egl_path(): def get_build_exe_options(skip_html=False): + """Get the options passed as build_exe_options to cx_Freeze. + + If either skip_html or --qute-skip-html as argument is given, doesn't + freeze the documentation. + """ if '--qute-skip-html' in sys.argv: skip_html = True sys.argv.remove('--qute-skip-html') From 94d394001e42d168f74b72ecf8e65fb0f5b524d9 Mon Sep 17 00:00:00 2001 From: Artur Shaik Date: Thu, 18 Jun 2015 19:47:33 +0600 Subject: [PATCH 110/146] Don't position caret if there is selection on page --- qutebrowser/browser/webview.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/webview.py b/qutebrowser/browser/webview.py index 5a4fc3b69..cb6181b7a 100644 --- a/qutebrowser/browser/webview.py +++ b/qutebrowser/browser/webview.py @@ -467,8 +467,9 @@ class WebView(QWebView): # and refocusing it fixes that. self.clearFocus() self.setFocus(Qt.OtherFocusReason) - self.page().currentFrame().evaluateJavaScript( - utils.read_file('javascript/position_caret.js')) + if len(self.page().selectedText()) == 0: + self.page().currentFrame().evaluateJavaScript( + utils.read_file('javascript/position_caret.js')) @pyqtSlot(usertypes.KeyMode) def on_mode_left(self, mode): From 5cf1dce89ed0b44be1f5c5b70e4b642d50246b7f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 20 Jun 2015 13:06:32 +0200 Subject: [PATCH 111/146] Simplify condition and add comment. --- qutebrowser/browser/webview.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/webview.py b/qutebrowser/browser/webview.py index cb6181b7a..7f866e732 100644 --- a/qutebrowser/browser/webview.py +++ b/qutebrowser/browser/webview.py @@ -467,7 +467,13 @@ class WebView(QWebView): # and refocusing it fixes that. self.clearFocus() self.setFocus(Qt.OtherFocusReason) - if len(self.page().selectedText()) == 0: + + # Move the caret to the first element in the viewport if there + # isn't any text which is already selected. + # + # Note: We can't use hasSelection() here, as that's always + # true in caret mode. + if not self.page().selectedText(): self.page().currentFrame().evaluateJavaScript( utils.read_file('javascript/position_caret.js')) From e459ac52cc277e06b6596477b6bcb491390e0198 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 20 Jun 2015 13:09:00 +0200 Subject: [PATCH 112/146] Use existing selection when entering caret mode. --- qutebrowser/browser/webview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/browser/webview.py b/qutebrowser/browser/webview.py index 7f866e732..f9c838c70 100644 --- a/qutebrowser/browser/webview.py +++ b/qutebrowser/browser/webview.py @@ -460,7 +460,7 @@ class WebView(QWebView): elif mode == usertypes.KeyMode.caret: settings = self.settings() settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True) - self.selection_enabled = False + self.selection_enabled = bool(self.page().selectedText()) if self.isVisible(): # Sometimes the caret isn't immediately visible, but unfocusing From f2d739197434d61b6b6c50c10619e22e5dec9b6e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 23 Jun 2015 17:19:48 +0200 Subject: [PATCH 113/146] tox: Update pytest to 2.7.2 and pylib to 1.4.29. pytest upstream changelog ========================= - fix issue767: pytest.raises value attribute does not contain the exception instance on Python 2.6. Thanks Eric Siegerman for providing the test case and Bruno Oliveira for PR. - Automatically create directory for junitxml and results log. Thanks Aron Curzon. - fix issue713: JUnit XML reports for doctest failures. Thanks Punyashloka Biswal. - fix issue735: assertion failures on debug versions of Python 3.4+ Thanks Benjamin Peterson. - fix issue114: skipif marker reports to internal skipping plugin; Thanks Floris Bruynooghe for reporting and Bruno Oliveira for the PR. - fix issue748: unittest.SkipTest reports to internal pytest unittest plugin. Thanks Thomas De Schampheleire for reporting and Bruno Oliveira for the PR. - fix issue718: failed to create representation of sets containing unsortable elements in python 2. Thanks Edison Gustavo Muenz - fix issue756, fix issue752 (and similar issues): depend on py-1.4.29 which has a refined algorithm for traceback generation. py upstream changelog ===================== - fix issue55: revert a change to the statement finding algorithm which is used by pytest for generating tracebacks. Thanks Daniel Hahler for initial analysis. - fix pytest issue254 for when traceback rendering can't find valid source code. Thanks Ionel Cristian Maries. --- tox.ini | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tox.ini b/tox.ini index d8605a2d0..f2f5a08c9 100644 --- a/tox.ini +++ b/tox.ini @@ -21,8 +21,8 @@ setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/pl passenv = PYTHON DISPLAY XAUTHORITY HOME deps = -r{toxinidir}/requirements.txt - py==1.4.28 - pytest==2.7.1 + py==1.4.29 + pytest==2.7.2 pytest-capturelog==0.7 pytest-qt==1.4.0 pytest-mock==0.6.0 @@ -90,8 +90,8 @@ commands = {envpython} -m pep257 scripts tests qutebrowser --ignore=D102,D103,D2 setenv = LANG=en_US.UTF-8 deps = -r{toxinidir}/requirements.txt - py==1.4.28 - pytest==2.7.1 + py==1.4.29 + pytest==2.7.2 pyflakes==0.9.2 pytest-flakes==1.0.0 commands = @@ -101,8 +101,8 @@ commands = [testenv:pep8] deps = -r{toxinidir}/requirements.txt - py==1.4.28 - pytest==2.7.1 + py==1.4.29 + pytest==2.7.2 pep8==1.6.2 pytest-pep8==1.0.6 commands = @@ -112,8 +112,8 @@ commands = [testenv:mccabe] deps = -r{toxinidir}/requirements.txt - py==1.4.28 - pytest==2.7.1 + py==1.4.29 + pytest==2.7.2 mccabe==0.3.1 pytest-mccabe==0.1 commands = From d02b63a847c78729514c4d815c128cf8d2353027 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 20 Jun 2015 19:48:40 +0200 Subject: [PATCH 114/146] tox: Use --ignore=tests for pep8/pyflakes/mccabe. --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index f2f5a08c9..5af03e938 100644 --- a/tox.ini +++ b/tox.ini @@ -96,7 +96,7 @@ deps = pytest-flakes==1.0.0 commands = {envpython} scripts/link_pyqt.py --tox {envdir} - {envpython} -m py.test -q --flakes -m flakes + {envpython} -m py.test -q --flakes --ignore=tests [testenv:pep8] deps = @@ -107,7 +107,7 @@ deps = pytest-pep8==1.0.6 commands = {envpython} scripts/link_pyqt.py --tox {envdir} - {envpython} -m py.test -q --pep8 -m pep8 + {envpython} -m py.test -q --pep8 --ignore=tests [testenv:mccabe] deps = @@ -118,7 +118,7 @@ deps = pytest-mccabe==0.1 commands = {envpython} scripts/link_pyqt.py --tox {envdir} - {envpython} -m py.test -q --mccabe -m mccabe + {envpython} -m py.test -q --mccabe --ignore=tests [testenv:pyroma] skip_install = true From 3dbf3f9e0ab69bfe33d3f8ead90026df6a3c57b0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 23 Jun 2015 18:36:10 +0200 Subject: [PATCH 115/146] Use tox/virtualenv to build Windows packages. This makes sure we have all needed dependencies installed in the version which is in requirements.txt. Fixes #776. --- scripts/build_release.py | 26 ++++++++++++++++++++------ tox.ini | 10 ++++++++++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/scripts/build_release.py b/scripts/build_release.py index 9f051ff18..f76f6bf27 100755 --- a/scripts/build_release.py +++ b/scripts/build_release.py @@ -46,6 +46,20 @@ def call_script(name, *args, python=sys.executable): subprocess.check_call([python, path] + list(args)) +def call_freeze(*args, python=sys.executable): + """Call freeze.py via tox. + + Args: + *args: The arguments to pass. + python: The python interpreter to use. + """ + env = os.environ.copy() + env['PYTHON'] = python + subprocess.check_call( + [sys.executable, '-m', 'tox', '-e', 'cxfreeze-windows'] + list(args), + env=env) + + def build_common(args): """Common buildsteps used for all OS'.""" utils.print_title("Running asciidoc2html.py") @@ -69,17 +83,17 @@ def build_windows(): parts = str(sys.version_info.major), str(sys.version_info.minor) ver = ''.join(parts) dotver = '.'.join(parts) - python_x86 = r'C:\Python{}_x32\python.exe'.format(ver) - python_x64 = r'C:\Python{}\python.exe'.format(ver) + python_x86 = r'C:\Python{}_x32'.format(ver) + python_x64 = r'C:\Python{}'.format(ver) utils.print_title("Running 32bit freeze.py build_exe") - call_script('freeze.py', 'build_exe', python=python_x86) + call_freeze('build_exe', python=python_x86) utils.print_title("Running 64bit freeze.py build_exe") - call_script('freeze.py', 'build_exe', python=python_x64) + call_freeze('build_exe', python=python_x64) utils.print_title("Running 32bit freeze.py bdist_msi") - call_script('freeze.py', 'bdist_msi', python=python_x86) + call_freeze('bdist_msi', python=python_x86) utils.print_title("Running 64bit freeze.py bdist_msi") - call_script('freeze.py', 'bdist_msi', python=python_x64) + call_freeze('bdist_msi', python=python_x64) destdir = os.path.join('dist', 'zip') _maybe_remove(destdir) diff --git a/tox.ini b/tox.ini index 5af03e938..af44b9b00 100644 --- a/tox.ini +++ b/tox.ini @@ -170,6 +170,16 @@ commands = {envpython} scripts/freeze.py build_exe --qute-skip-html -b {envdir}/build {envdir}/build/qutebrowser --no-err-windows --nowindow --temp-basedir about:blank ":later 500 quit" +[testenv:cxfreeze-windows] +# PYTHON is actually required when using this env, but the entire tox.ini would +# fail if we didn't have a fallback defined. +basepython = {env:PYTHON:}/python.exe +skip_install = true +deps = {[testenv:smoke-frozen]deps} +commands = + {envpython} scripts/link_pyqt.py --tox {envdir} + {envpython} scripts/freeze.py {posargs} + [pytest] norecursedirs = .tox .venv markers = From b337cfe4c6f49079e51951bba25242f64cfc54c6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 23 Jun 2015 23:34:30 +0200 Subject: [PATCH 116/146] Enforce a 32bit Python in build_release.py. --- scripts/build_release.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/build_release.py b/scripts/build_release.py index f76f6bf27..e18dfab57 100755 --- a/scripts/build_release.py +++ b/scripts/build_release.py @@ -140,6 +140,14 @@ def main(): args = parser.parse_args() utils.change_cwd() if os.name == 'nt': + if sys.maxsize > 2**32: + # WORKAROUND + print("Due to a python/Windows bug, this script needs to be run ") + print("with a 32bit Python.") + print() + print("See http://bugs.python.org/issue24493 and ") + print("https://github.com/pypa/virtualenv/issues/774") + sys.exit(1) build_common(args) build_windows() else: From 10b00da1aedcdd970e8da0a13042574c2f60af6e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 23 Jun 2015 23:35:20 +0200 Subject: [PATCH 117/146] Reorder commands in build_release.py. This should be slightly faster as the venv is only recreated once. --- scripts/build_release.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/build_release.py b/scripts/build_release.py index e18dfab57..a9371ab53 100755 --- a/scripts/build_release.py +++ b/scripts/build_release.py @@ -88,10 +88,10 @@ def build_windows(): utils.print_title("Running 32bit freeze.py build_exe") call_freeze('build_exe', python=python_x86) - utils.print_title("Running 64bit freeze.py build_exe") - call_freeze('build_exe', python=python_x64) utils.print_title("Running 32bit freeze.py bdist_msi") call_freeze('bdist_msi', python=python_x86) + utils.print_title("Running 64bit freeze.py build_exe") + call_freeze('build_exe', python=python_x64) utils.print_title("Running 64bit freeze.py bdist_msi") call_freeze('bdist_msi', python=python_x64) From b21b4377a88098c72b93936b84e904efd6ef2661 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 23 Jun 2015 23:50:14 +0200 Subject: [PATCH 118/146] Add a smoke test to build_release.py. --- scripts/build_release.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/scripts/build_release.py b/scripts/build_release.py index a9371ab53..a37ecf426 100755 --- a/scripts/build_release.py +++ b/scripts/build_release.py @@ -78,6 +78,12 @@ def _maybe_remove(path): pass +def smoke_test(executable): + """Try starting the given qutebrowser executable.""" + subprocess.check_call([executable, '--no-err-windows', '--nowindow', + '--temp-basedir', 'about:blank', ':later 500 quit']) + + def build_windows(): """Build windows executables/setups.""" parts = str(sys.version_info.major), str(sys.version_info.minor) @@ -95,6 +101,11 @@ def build_windows(): utils.print_title("Running 64bit freeze.py bdist_msi") call_freeze('bdist_msi', python=python_x64) + utils.print_title("Running 32bit smoke test") + smoke_test('build/exe.win32-{}/qutebrowser.exe'.format(dotver)) + utils.print_title("Running 64bit smoke test") + smoke_test('build/exe.win-amd64-{}/qutebrowser.exe'.format(dotver)) + destdir = os.path.join('dist', 'zip') _maybe_remove(destdir) os.makedirs(destdir) From b7c3e7b959d720282366a582e6a48cf637b36253 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 24 Jun 2015 07:46:15 +0200 Subject: [PATCH 119/146] Disallow {foo} in search engine URLs. This causes an KeyError otherwise when trying to use str.format to insert the search term. --- qutebrowser/config/configtypes.py | 7 +++++++ tests/config/test_configtypes.py | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 23909b9c1..d3f0b0d8d 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -1110,8 +1110,15 @@ class SearchEngineUrl(BaseType): return else: raise configexc.ValidationError(value, "may not be empty!") + if '{}' not in value: raise configexc.ValidationError(value, "must contain \"{}\"") + try: + value.format("") + except KeyError: + raise configexc.ValidationError( + value, "may not contain {...} (use {{ and }} for literal {/})") + url = QUrl(value.replace('{}', 'foobar')) if not url.isValid(): raise configexc.ValidationError(value, "invalid url, {}".format( diff --git a/tests/config/test_configtypes.py b/tests/config/test_configtypes.py index 6b6a08594..edb0bb8cb 100644 --- a/tests/config/test_configtypes.py +++ b/tests/config/test_configtypes.py @@ -1881,6 +1881,11 @@ class TestSearchEngineUrl: with pytest.raises(configexc.ValidationError): self.t.validate(':{}') + def test_validate_format_string(self): + """Test validate with a {foo} format string.""" + with pytest.raises(configexc.ValidationError): + self.t.validate('foo{bar}baz{}') + def test_transform_empty(self): """Test transform with an empty value.""" assert self.t.transform('') is None From 4c7c38efcb133df305af9ad90da5cabc221282d4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 24 Jun 2015 08:10:16 +0200 Subject: [PATCH 120/146] Update changelog. --- CHANGELOG.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 3c1fc21cb..d8bbd4745 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -75,6 +75,7 @@ Fixed - Scrolling should now work more reliably on some pages where arrow keys worked but `hjkl` didn't. - Small improvements when checking if an input is an URL or not. - Fixed wrong cursor position when completing the first item in the completion. +- Fix exception when using search engines with {foo} in the name. v0.2.2 (unreleased) ------------------- From 0f1ba4739c60810cd5f89026b2c945d29be70e7d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 23 Jun 2015 19:02:43 +0200 Subject: [PATCH 121/146] Mark OS X on Travis as expected failure. Travis currently has a bug where OS X builds are routed to Ubuntu Trusty when "dist: trusty" and "os: osx" is given. --- .travis.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.travis.yml b/.travis.yml index 9ed28743e..686dbbdaa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,3 +20,9 @@ script: - tox -e pylint - tox -e pyroma - tox -e check-manifest + +# Travis bug - OS X builds get routed to Ubuntu Trusty if "dist: trusty" is +# given. +matrix: + allow_failures: + - os: osx From f8d66f3fe1123bd2d0448ec95ce48b8030f77896 Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Wed, 24 Jun 2015 18:16:59 +0200 Subject: [PATCH 122/146] Use zoom_perc instead of setZoomFactor to set zoom. When using setZoomFactor, the NeighborList's fuzzyval doesn't get updated, which means the next -/+ press would do something weird. --- qutebrowser/browser/webpage.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/webpage.py b/qutebrowser/browser/webpage.py index 15659f56f..071e627d9 100644 --- a/qutebrowser/browser/webpage.py +++ b/qutebrowser/browser/webpage.py @@ -241,7 +241,7 @@ class BrowserPage(QWebPage): if cur_data is not None: frame = self.mainFrame() if 'zoom' in cur_data: - frame.setZoomFactor(cur_data['zoom']) + frame.page().view().zoom_perc(cur_data['zoom'] * 100) if ('scroll-pos' in cur_data and frame.scrollPosition() == QPoint(0, 0)): QTimer.singleShot(0, functools.partial( @@ -418,7 +418,7 @@ class BrowserPage(QWebPage): if data is None: return if 'zoom' in data: - frame.setZoomFactor(data['zoom']) + frame.page().view().zoom_perc(data['zoom'] * 100) if 'scroll-pos' in data and frame.scrollPosition() == QPoint(0, 0): frame.setScrollPosition(data['scroll-pos']) From d5d85bd9c762cbdafbe991e662d6a1f27e701f5f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 24 Jun 2015 18:23:37 +0200 Subject: [PATCH 123/146] Regenerate authors --- README.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.asciidoc b/README.asciidoc index a1ba8fa90..4b09f2546 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -137,8 +137,8 @@ Contributors, sorted by the number of commits in descending order: * Bruno Oliveira * Raphael Pierzina * Joel Torstensson -* Claude * Martin Tournoij +* Claude * Lamar Pavel * Austin Anderson * Artur Shaik From db267ae195370cf7c603a2677bc2758b90ac06fa Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 24 Jun 2015 18:32:56 +0200 Subject: [PATCH 124/146] tests: Increase timeout for starting processes. Windows can be slow... --- tests/misc/test_guiprocess.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/misc/test_guiprocess.py b/tests/misc/test_guiprocess.py index 66166a7c7..9c28ab9f0 100644 --- a/tests/misc/test_guiprocess.py +++ b/tests/misc/test_guiprocess.py @@ -72,7 +72,7 @@ def fake_proc(monkeypatch, stubs): def test_start(proc, qtbot): """Test simply starting a process.""" with qtbot.waitSignals([proc.started, proc.finished], raising=True, - timeout=2000): + timeout=10000): argv = _py_proc("import sys; print('test'); sys.exit(0)") proc.start(*argv) From 24424a0486921a5442d1901f4062082674c2bbe1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 24 Jun 2015 20:30:26 +0200 Subject: [PATCH 125/146] Fix expected qWarning messages for Qt 5.5. --- tests/utils/test_qtutils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/utils/test_qtutils.py b/tests/utils/test_qtutils.py index 63ce3bfda..aaec77870 100644 --- a/tests/utils/test_qtutils.py +++ b/tests/utils/test_qtutils.py @@ -339,7 +339,7 @@ class TestSerializeStream: assert src_obj == dest_obj - @pytest.mark.qt_log_ignore('^QIODevice::write: ReadOnly device') + @pytest.mark.qt_log_ignore('^QIODevice::write.*: ReadOnly device') def test_serialize_readonly_stream(self): """Test serialize_stream with a read-only stream.""" data = QByteArray() @@ -349,7 +349,7 @@ class TestSerializeStream: assert str(excinfo.value) == ("The data stream cannot write to the " "underlying device.") - @pytest.mark.qt_log_ignore('QIODevice::read: WriteOnly device') + @pytest.mark.qt_log_ignore('QIODevice::read.*: WriteOnly device') def test_deserialize_writeonly_stream(self): """Test deserialize_stream with a write-only stream.""" data = QByteArray() From 220ac021f01fdfa454460f0b33734489cc0f8a0a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 24 Jun 2015 20:37:48 +0200 Subject: [PATCH 126/146] Print style name in version info. --- qutebrowser/utils/version.py | 3 +++ tests/utils/test_version.py | 21 +++++++++++++-------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 88c9f0683..3d1b37125 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -30,6 +30,7 @@ import collections from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, qVersion from PyQt5.QtWebKit import qWebKitVersion from PyQt5.QtNetwork import QSslSocket +from PyQt5.QtWidgets import QApplication import qutebrowser from qutebrowser.utils import log, utils @@ -189,12 +190,14 @@ def version(): gitver = _git_str() if gitver is not None: lines.append("Git commit: {}".format(gitver)) + style = QApplication.instance().style() lines += [ '', '{}: {}'.format(platform.python_implementation(), platform.python_version()), 'Qt: {}, runtime: {}'.format(QT_VERSION_STR, qVersion()), 'PyQt: {}'.format(PYQT_VERSION_STR), + 'Style: {}'.format(style.metaObject().className()), ] lines += _module_versions() diff --git a/tests/utils/test_version.py b/tests/utils/test_version.py index c2fe19e83..edfd9c539 100644 --- a/tests/utils/test_version.py +++ b/tests/utils/test_version.py @@ -607,38 +607,43 @@ class TestVersion: lines = version.version().splitlines() assert lines[4] == 'PyQt: 78.9' + def test_style(self, monkeypatch): + """Test the style in the output.""" + lines = version.version().splitlines() + assert lines[5].startswith('Style: ') + def test_module_versions(self, monkeypatch): """Test module versions in the output.""" monkeypatch.setattr('qutebrowser.utils.version._module_versions', lambda: ['Hello', 'World']) lines = version.version().splitlines() - assert (lines[5], lines[6]) == ('Hello', 'World') + assert (lines[6], lines[7]) == ('Hello', 'World') def test_webkit_version(self, monkeypatch): """Test the webkit version in the output.""" monkeypatch.setattr('qutebrowser.utils.version.qWebKitVersion', lambda: '567.1') lines = version.version().splitlines() - assert lines[5] == 'Webkit: 567.1' + assert lines[6] == 'Webkit: 567.1' def test_harfbuzz_none(self, monkeypatch): """Test harfbuzz output with QT_HARFBUZZ unset.""" monkeypatch.delenv('QT_HARFBUZZ', raising=False) lines = version.version().splitlines() - assert lines[6] == 'Harfbuzz: system' + assert lines[7] == 'Harfbuzz: system' def test_harfbuzz_set(self, monkeypatch): """Test harfbuzz output with QT_HARFBUZZ set.""" monkeypatch.setenv('QT_HARFBUZZ', 'new') lines = version.version().splitlines() - assert lines[6] == 'Harfbuzz: new' + assert lines[7] == 'Harfbuzz: new' def test_ssl(self, monkeypatch): """Test SSL version in the output.""" monkeypatch.setattr('qutebrowser.utils.version.QSslSocket', FakeQSslSocket('1.0.1')) lines = version.version().splitlines() - assert lines[7] == 'SSL: 1.0.1' + assert lines[8] == 'SSL: 1.0.1' @pytest.mark.parametrize('frozen, expected', [(True, 'Frozen: True'), (False, 'Frozen: False')]) @@ -649,7 +654,7 @@ class TestVersion: else: monkeypatch.delattr(sys, 'frozen', raising=False) lines = version.version().splitlines() - assert lines[9] == expected + assert lines[10] == expected def test_platform(self, monkeypatch): """Test platform in the version output.""" @@ -658,11 +663,11 @@ class TestVersion: monkeypatch.setattr('qutebrowser.utils.version.platform.architecture', lambda: ('64bit', '')) lines = version.version().splitlines() - assert lines[10] == 'Platform: toaster, 64bit' + assert lines[11] == 'Platform: toaster, 64bit' def test_os_info(self, monkeypatch): """Test OS info in the output.""" monkeypatch.setattr('qutebrowser.utils.version._os_info', lambda: ['Hello', 'World']) lines = version.version().splitlines() - assert (lines[11], lines[12]) == ('Hello', 'World') + assert (lines[12], lines[13]) == ('Hello', 'World') From 75b894a1863561b5112cc091bc1b47b1857bbc86 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 24 Jun 2015 20:54:37 +0200 Subject: [PATCH 127/146] Include DESKTOP_SESSION in qute:version. --- qutebrowser/utils/version.py | 1 + tests/utils/test_version.py | 22 ++++++++++++++-------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 3d1b37125..b89a17292 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -198,6 +198,7 @@ def version(): 'Qt: {}, runtime: {}'.format(QT_VERSION_STR, qVersion()), 'PyQt: {}'.format(PYQT_VERSION_STR), 'Style: {}'.format(style.metaObject().className()), + 'Desktop: {}'.format(os.environ.get('DESKTOP_SESSION')), ] lines += _module_versions() diff --git a/tests/utils/test_version.py b/tests/utils/test_version.py index edfd9c539..c1c081605 100644 --- a/tests/utils/test_version.py +++ b/tests/utils/test_version.py @@ -612,38 +612,44 @@ class TestVersion: lines = version.version().splitlines() assert lines[5].startswith('Style: ') + def test_desktop_environment(self, monkeypatch): + """Test the desktop environment in the output.""" + monkeypatch.setenv('DESKTOP_SESSION', 'Blah') + lines = version.version().splitlines() + assert lines[6] == 'Desktop: Blah' + def test_module_versions(self, monkeypatch): """Test module versions in the output.""" monkeypatch.setattr('qutebrowser.utils.version._module_versions', lambda: ['Hello', 'World']) lines = version.version().splitlines() - assert (lines[6], lines[7]) == ('Hello', 'World') + assert (lines[7], lines[8]) == ('Hello', 'World') def test_webkit_version(self, monkeypatch): """Test the webkit version in the output.""" monkeypatch.setattr('qutebrowser.utils.version.qWebKitVersion', lambda: '567.1') lines = version.version().splitlines() - assert lines[6] == 'Webkit: 567.1' + assert lines[7] == 'Webkit: 567.1' def test_harfbuzz_none(self, monkeypatch): """Test harfbuzz output with QT_HARFBUZZ unset.""" monkeypatch.delenv('QT_HARFBUZZ', raising=False) lines = version.version().splitlines() - assert lines[7] == 'Harfbuzz: system' + assert lines[8] == 'Harfbuzz: system' def test_harfbuzz_set(self, monkeypatch): """Test harfbuzz output with QT_HARFBUZZ set.""" monkeypatch.setenv('QT_HARFBUZZ', 'new') lines = version.version().splitlines() - assert lines[7] == 'Harfbuzz: new' + assert lines[8] == 'Harfbuzz: new' def test_ssl(self, monkeypatch): """Test SSL version in the output.""" monkeypatch.setattr('qutebrowser.utils.version.QSslSocket', FakeQSslSocket('1.0.1')) lines = version.version().splitlines() - assert lines[8] == 'SSL: 1.0.1' + assert lines[9] == 'SSL: 1.0.1' @pytest.mark.parametrize('frozen, expected', [(True, 'Frozen: True'), (False, 'Frozen: False')]) @@ -654,7 +660,7 @@ class TestVersion: else: monkeypatch.delattr(sys, 'frozen', raising=False) lines = version.version().splitlines() - assert lines[10] == expected + assert lines[11] == expected def test_platform(self, monkeypatch): """Test platform in the version output.""" @@ -663,11 +669,11 @@ class TestVersion: monkeypatch.setattr('qutebrowser.utils.version.platform.architecture', lambda: ('64bit', '')) lines = version.version().splitlines() - assert lines[11] == 'Platform: toaster, 64bit' + assert lines[12] == 'Platform: toaster, 64bit' def test_os_info(self, monkeypatch): """Test OS info in the output.""" monkeypatch.setattr('qutebrowser.utils.version._os_info', lambda: ['Hello', 'World']) lines = version.version().splitlines() - assert (lines[12], lines[13]) == ('Hello', 'World') + assert (lines[13], lines[14]) == ('Hello', 'World') From 534a85cf8f4710c81f7c853c1cf1c67f17862837 Mon Sep 17 00:00:00 2001 From: Franz Fellner Date: Wed, 24 Jun 2015 21:27:37 +0200 Subject: [PATCH 128/146] Create a new QStyleOptionTab object for each tab. It seems under some circumstances (on Gentoo?), the existing QStyleOptionTab object was reused, causing subsequent tabs to have the same title as the first one. Fixes #778. --- qutebrowser/mainwindow/tabwidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index bbbfdf045..a5612a8b5 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -391,9 +391,9 @@ class TabBar(QTabBar): def paintEvent(self, _e): """Override paintEvent to draw the tabs like we want to.""" p = QStylePainter(self) - tab = QStyleOptionTab() selected = self.currentIndex() for idx in range(self.count()): + tab = QStyleOptionTab() self.initStyleOption(tab, idx) if idx == selected: bg_color = config.get('colors', 'tabs.bg.selected') From 5d490a4e22d544ff4daf13969ccf2780facdc34f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 24 Jun 2015 21:31:10 +0200 Subject: [PATCH 129/146] Update authors --- README.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.asciidoc b/README.asciidoc index 4b09f2546..b2194add8 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -161,6 +161,7 @@ Contributors, sorted by the number of commits in descending order: * Mathias Fussenegger * Larry Hynes * Fritz V155 Reichwald +* Franz Fellner * error800 * Thorsten Wißmann * Thiago Barroso Perrotta @@ -168,7 +169,6 @@ Contributors, sorted by the number of commits in descending order: * Helen Sherwood-Taylor * HalosGhost * Gregor Pohl -* Franz Fellner * Eivind Uggedal * Andreas Fischer // QUTE_AUTHORS_END From 4652843b38d03521e7a46b4d00c5f2327400585c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 24 Jun 2015 23:06:55 +0200 Subject: [PATCH 130/146] Move command-related zoom logic out of WebView. After f8d66f3fe1123bd2d0448ec95ce48b8030f77896 loading a session showed the zoom percentage of all tabs. This logic doesn't really belong into webview.py anyways, so it gets moved to browser/commands.py here. --- qutebrowser/browser/commands.py | 19 ++++++++++++++++--- qutebrowser/browser/webview.py | 8 +++++--- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 625ceb05f..e29350d1b 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -737,7 +737,11 @@ class CommandDispatcher: count: How many steps to zoom in. """ tab = self._current_widget() - tab.zoom(count) + try: + perc = tab.zoom(count) + except ValueError as e: + raise cmdexc.CommandError(e) + message.info(self._win_id, "Zoom level: {}%".format(perc)) @cmdutils.register(instance='command-dispatcher', scope='window', count='count') @@ -748,7 +752,11 @@ class CommandDispatcher: count: How many steps to zoom out. """ tab = self._current_widget() - tab.zoom(-count) + try: + perc = tab.zoom(-count) + except ValueError as e: + raise cmdexc.CommandError(e) + message.info(self._win_id, "Zoom level: {}%".format(perc)) @cmdutils.register(instance='command-dispatcher', scope='window', count='count') @@ -768,7 +776,12 @@ class CommandDispatcher: except ValueError as e: raise cmdexc.CommandError(e) tab = self._current_widget() - tab.zoom_perc(level) + + try: + tab.zoom_perc(level) + except ValueError as e: + raise cmdexc.CommandError(e) + message.info(self._win_id, "Zoom level: {}%".format(level)) @cmdutils.register(instance='command-dispatcher', scope='window') def tab_only(self, left=False, right=False): diff --git a/qutebrowser/browser/webview.py b/qutebrowser/browser/webview.py index f9c838c70..cfabee80c 100644 --- a/qutebrowser/browser/webview.py +++ b/qutebrowser/browser/webview.py @@ -33,7 +33,6 @@ from qutebrowser.config import config from qutebrowser.keyinput import modeman from qutebrowser.utils import message, log, usertypes, utils, qtutils, objreg from qutebrowser.browser import webpage, hints, webelem -from qutebrowser.commands import cmdexc LoadStatus = usertypes.enum('LoadStatus', ['none', 'success', 'error', 'warn', @@ -369,9 +368,8 @@ class WebView(QWebView): if fuzzyval: self._zoom.fuzzyval = int(perc) if perc < 0: - raise cmdexc.CommandError("Can't zoom {}%!".format(perc)) + raise ValueError("Can't zoom {}%!".format(perc)) self.setZoomFactor(float(perc) / 100) - message.info(self.win_id, "Zoom level: {}%".format(perc)) self._default_zoom_changed = True def zoom(self, offset): @@ -379,9 +377,13 @@ class WebView(QWebView): Args: offset: The offset in the zoom level list. + + Return: + The new zoom percentage. """ level = self._zoom.getitem(offset) self.zoom_perc(level, fuzzyval=False) + return level @pyqtSlot('QUrl') def on_url_changed(self, url): From f61aaa9053d12edb9d6c048ddbe8ab857c7ad109 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 26 Jun 2015 09:47:34 +0200 Subject: [PATCH 131/146] Don't install scripts package. See #783. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b62a75ba2..1ede9999e 100755 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ except NameError: try: common.write_git_file() setuptools.setup( - packages=setuptools.find_packages(exclude=['qutebrowser.test']), + packages=setuptools.find_packages(include=['qutebrowser']), include_package_data=True, entry_points={'gui_scripts': ['qutebrowser = qutebrowser.qutebrowser:main']}, From 345d048f43cd92032b30cefc452e8e1314f91bc6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 26 Jun 2015 10:00:36 +0200 Subject: [PATCH 132/146] Revert "Don't install scripts package." This reverts commit f61aaa9053d12edb9d6c048ddbe8ab857c7ad109. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1ede9999e..b62a75ba2 100755 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ except NameError: try: common.write_git_file() setuptools.setup( - packages=setuptools.find_packages(include=['qutebrowser']), + packages=setuptools.find_packages(exclude=['qutebrowser.test']), include_package_data=True, entry_points={'gui_scripts': ['qutebrowser = qutebrowser.qutebrowser:main']}, From 97cc90b49f792f61ea93c5f01b83ee85a393f78a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 26 Jun 2015 19:25:58 +0200 Subject: [PATCH 133/146] Revert "Revert "Don't install scripts package."" This reverts commit 345d048f43cd92032b30cefc452e8e1314f91bc6. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b62a75ba2..1ede9999e 100755 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ except NameError: try: common.write_git_file() setuptools.setup( - packages=setuptools.find_packages(exclude=['qutebrowser.test']), + packages=setuptools.find_packages(include=['qutebrowser']), include_package_data=True, entry_points={'gui_scripts': ['qutebrowser = qutebrowser.qutebrowser:main']}, From f10b9f1172a90633896bdde01c7a4f3ef5dd0afa Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 26 Jun 2015 19:36:54 +0200 Subject: [PATCH 134/146] Adjust MANIFEST.in to include all needed scripts. --- MANIFEST.in | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 340fc796c..3a3e21d60 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,12 +1,13 @@ global-exclude __pycache__ *.pyc *.pyo +recursive-include qutebrowser *.py recursive-include qutebrowser/html *.html recursive-include qutebrowser/test *.py recursive-include qutebrowser/javascript *.js graft icons -graft scripts/pylint_checkers graft doc/img graft misc +graft scripts include qutebrowser/utils/testfile include qutebrowser/git-commit-id include COPYING doc/* README.asciidoc CONTRIBUTING.asciidoc FAQ.asciidoc INSTALL.asciidoc CHANGELOG.asciidoc @@ -15,13 +16,15 @@ include requirements.txt include tox.ini include qutebrowser.py -exclude scripts/cleanup.py -exclude scripts/minimal_webkit_testbrowser.py -exclude scripts/run_profile.py -exclude scripts/src2asciidoc.sh -exclude scripts/gen_resources.sh -exclude scripts/quit_segfault_test.sh -exclude scripts/segfault_test.sh +include scripts/__init__.py +include scripts/hostblock_blame.py +include scripts/importer.py +include scripts/keytester.py +include scripts/link_pyqt.py +include scripts/minimal_webkit_testbrowser.py +include scripts/setupcommon.py +include scripts/utils.py + exclude doc/notes recursive-exclude doc *.asciidoc include doc/qutebrowser.1.asciidoc From 956baed76ba886d55d5337f5b31ccd585411d2d1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 26 Jun 2015 19:45:56 +0200 Subject: [PATCH 135/146] Use exclude= instead of include= for find_packages. It seems the old setuptool version in Ubuntu Trusty doesn't have include=... --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1ede9999e..1cb99e8ab 100755 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ except NameError: try: common.write_git_file() setuptools.setup( - packages=setuptools.find_packages(include=['qutebrowser']), + packages=setuptools.find_packages(exclude=['scripts', 'scripts.*']), include_package_data=True, entry_points={'gui_scripts': ['qutebrowser = qutebrowser.qutebrowser:main']}, From e9b5c355d255d79d576cc2816dbbd88ed68e3903 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 26 Jun 2015 22:33:29 +0200 Subject: [PATCH 136/146] Add a short=True argument to version.version(). Fixes #787. --- qutebrowser/app.py | 2 +- qutebrowser/utils/version.py | 40 +++++++++++++++++++++--------------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index c4ebef460..20bf0f757 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -61,7 +61,7 @@ def run(args): """Initialize everthing and run the application.""" # pylint: disable=too-many-statements if args.version: - print(version.version()) + print(version.version(short=True)) print() print() print(qutebrowser.__copyright__) diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index b89a17292..e0e966615 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -184,33 +184,41 @@ def _os_info(): return lines -def version(): - """Return a string with various version informations.""" +def version(short=False): + """Return a string with various version informations. + + Args: + short: Return a shortened output. + """ lines = ["qutebrowser v{}".format(qutebrowser.__version__)] gitver = _git_str() if gitver is not None: lines.append("Git commit: {}".format(gitver)) - style = QApplication.instance().style() lines += [ '', '{}: {}'.format(platform.python_implementation(), platform.python_version()), 'Qt: {}, runtime: {}'.format(QT_VERSION_STR, qVersion()), 'PyQt: {}'.format(PYQT_VERSION_STR), - 'Style: {}'.format(style.metaObject().className()), - 'Desktop: {}'.format(os.environ.get('DESKTOP_SESSION')), ] - lines += _module_versions() + if not short: + style = QApplication.instance().style() + lines += [ + 'Style: {}'.format(style.metaObject().className()), + 'Desktop: {}'.format(os.environ.get('DESKTOP_SESSION')), + ] - lines += [ - 'Webkit: {}'.format(qWebKitVersion()), - 'Harfbuzz: {}'.format(os.environ.get('QT_HARFBUZZ', 'system')), - 'SSL: {}'.format(QSslSocket.sslLibraryVersionString()), - '', - 'Frozen: {}'.format(hasattr(sys, 'frozen')), - 'Platform: {}, {}'.format(platform.platform(), - platform.architecture()[0]), - ] - lines += _os_info() + lines += _module_versions() + + lines += [ + 'Webkit: {}'.format(qWebKitVersion()), + 'Harfbuzz: {}'.format(os.environ.get('QT_HARFBUZZ', 'system')), + 'SSL: {}'.format(QSslSocket.sslLibraryVersionString()), + '', + 'Frozen: {}'.format(hasattr(sys, 'frozen')), + 'Platform: {}, {}'.format(platform.platform(), + platform.architecture()[0]), + ] + lines += _os_info() return '\n'.join(lines) From 6dbac1c04771f9d099b2e749907321b51b797463 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 26 Jun 2015 22:37:00 +0200 Subject: [PATCH 137/146] Rewrite version.version() tests and test short arg. --- tests/stubs.py | 5 +- tests/utils/test_version.py | 189 +++++++++++++----------------------- 2 files changed, 73 insertions(+), 121 deletions(-) diff --git a/tests/stubs.py b/tests/stubs.py index ab642160f..4345a42f6 100644 --- a/tests/stubs.py +++ b/tests/stubs.py @@ -27,6 +27,7 @@ from unittest import mock from PyQt5.QtCore import pyqtSignal, QPoint, QProcess, QObject from PyQt5.QtNetwork import QNetworkRequest +from PyQt5.QtWidgets import QCommonStyle class FakeKeyEvent: @@ -72,8 +73,10 @@ class FakeQApplication: """Stub to insert as QApplication module.""" - def __init__(self): + def __init__(self, style=None): self.instance = mock.Mock(return_value=self) + self.style = mock.Mock(spec=QCommonStyle) + self.style().metaObject().className.return_value = style class FakeUrl: diff --git a/tests/utils/test_version.py b/tests/utils/test_version.py index c1c081605..3e21a57c1 100644 --- a/tests/utils/test_version.py +++ b/tests/utils/test_version.py @@ -30,6 +30,7 @@ import builtins import types import importlib import logging +import textwrap import pytest @@ -548,132 +549,80 @@ class FakeQSslSocket: return self._version -class TestVersion: +@pytest.mark.parametrize('git_commit, harfbuzz, frozen, short', [ + (True, True, False, False), # normal + (False, True, False, False), # no git commit + (True, False, False, False), # HARFBUZZ unset + (True, True, True, False), # frozen + (True, True, False, True), # short + (False, True, False, True), # short and no git commit +]) +def test_version_output(git_commit, harfbuzz, frozen, short, stubs, + monkeypatch): + """Test version.version().""" + patches = { + 'qutebrowser.__version__': 'VERSION', + '_git_str': lambda: ('GIT COMMIT' if git_commit else None), + 'platform.python_implementation': lambda: 'PYTHON IMPLEMENTATION', + 'platform.python_version': lambda: 'PYTHON VERSION', + 'QT_VERSION_STR': 'QT VERSION', + 'qVersion': lambda: 'QT RUNTIME VERSION', + 'PYQT_VERSION_STR': 'PYQT VERSION', + '_module_versions': lambda: ['MODULE VERSION 1', 'MODULE VERSION 2'], + 'qWebKitVersion': lambda: 'WEBKIT VERSION', + 'QSslSocket': FakeQSslSocket('SSL VERSION'), + 'platform.platform': lambda: 'PLATFORM', + 'platform.architecture': lambda: ('ARCHITECTURE', ''), + '_os_info': lambda: ['OS INFO 1', 'OS INFO 2'], + 'QApplication': stubs.FakeQApplication(style='STYLE'), + } - """Tests for version.""" + for attr, val in patches.items(): + monkeypatch.setattr('qutebrowser.utils.version.' + attr, val) - @pytest.fixture(autouse=True) - def patch(self, monkeypatch): - """Patch some sub-functions we're not interested in.""" - monkeypatch.setattr('qutebrowser.utils.version._git_str', lambda: None) - monkeypatch.setattr('qutebrowser.utils.version._module_versions', - lambda: []) - monkeypatch.setattr('qutebrowser.utils.version._os_info', lambda: []) + monkeypatch.setenv('DESKTOP_SESSION', 'DESKTOP') - def test_qutebrowser_version(self, monkeypatch): - """Test the qutebrowser version in the output.""" - monkeypatch.setattr( - 'qutebrowser.utils.version.qutebrowser.__version__', '23.42') - lines = version.version().splitlines() - assert lines[0] == 'qutebrowser v23.42' - - def test_git_commit(self, monkeypatch): - """Test the git commit in the output.""" - monkeypatch.setattr('qutebrowser.utils.version._git_str', - lambda: 'deadbeef') - lines = version.version().splitlines() - assert lines[1] == 'Git commit: deadbeef' - - def test_no_git_commit(self, monkeypatch): - """Test the git commit with _git_str returning None.""" - monkeypatch.setattr('qutebrowser.utils.version._git_str', - lambda: None) - lines = version.version().splitlines() - assert not lines[1].startswith('Git commit:') - - def test_python_version(self, monkeypatch): - """Test the python version in the output.""" - monkeypatch.setattr( - 'qutebrowser.utils.version.platform.python_implementation', - lambda: 'python_implementation') - monkeypatch.setattr( - 'qutebrowser.utils.version.platform.python_version', - lambda: 'python_version') - lines = version.version().splitlines() - assert lines[2] == 'python_implementation: python_version' - - def test_qt_version(self, monkeypatch): - """Test the python version in the output.""" - monkeypatch.setattr('qutebrowser.utils.version.QT_VERSION_STR', '12.3') - monkeypatch.setattr('qutebrowser.utils.version.qVersion', - lambda: '45.6') - lines = version.version().splitlines() - assert lines[3] == 'Qt: 12.3, runtime: 45.6' - - def test_pyqt_version(self, monkeypatch): - """Test the PyQt version in the output.""" - monkeypatch.setattr('qutebrowser.utils.version.PYQT_VERSION_STR', - '78.9') - lines = version.version().splitlines() - assert lines[4] == 'PyQt: 78.9' - - def test_style(self, monkeypatch): - """Test the style in the output.""" - lines = version.version().splitlines() - assert lines[5].startswith('Style: ') - - def test_desktop_environment(self, monkeypatch): - """Test the desktop environment in the output.""" - monkeypatch.setenv('DESKTOP_SESSION', 'Blah') - lines = version.version().splitlines() - assert lines[6] == 'Desktop: Blah' - - def test_module_versions(self, monkeypatch): - """Test module versions in the output.""" - monkeypatch.setattr('qutebrowser.utils.version._module_versions', - lambda: ['Hello', 'World']) - lines = version.version().splitlines() - assert (lines[7], lines[8]) == ('Hello', 'World') - - def test_webkit_version(self, monkeypatch): - """Test the webkit version in the output.""" - monkeypatch.setattr('qutebrowser.utils.version.qWebKitVersion', - lambda: '567.1') - lines = version.version().splitlines() - assert lines[7] == 'Webkit: 567.1' - - def test_harfbuzz_none(self, monkeypatch): - """Test harfbuzz output with QT_HARFBUZZ unset.""" + if harfbuzz: + monkeypatch.setenv('QT_HARFBUZZ', 'HARFBUZZ') + else: monkeypatch.delenv('QT_HARFBUZZ', raising=False) - lines = version.version().splitlines() - assert lines[8] == 'Harfbuzz: system' - def test_harfbuzz_set(self, monkeypatch): - """Test harfbuzz output with QT_HARFBUZZ set.""" - monkeypatch.setenv('QT_HARFBUZZ', 'new') - lines = version.version().splitlines() - assert lines[8] == 'Harfbuzz: new' + if frozen: + monkeypatch.setattr(sys, 'frozen', True, raising=False) + else: + monkeypatch.delattr(sys, 'frozen', raising=False) - def test_ssl(self, monkeypatch): - """Test SSL version in the output.""" - monkeypatch.setattr('qutebrowser.utils.version.QSslSocket', - FakeQSslSocket('1.0.1')) - lines = version.version().splitlines() - assert lines[9] == 'SSL: 1.0.1' + template = textwrap.dedent(""" + qutebrowser vVERSION + {git_commit} + PYTHON IMPLEMENTATION: PYTHON VERSION + Qt: QT VERSION, runtime: QT RUNTIME VERSION + PyQt: PYQT VERSION + """.lstrip('\n')) - @pytest.mark.parametrize('frozen, expected', [(True, 'Frozen: True'), - (False, 'Frozen: False')]) - def test_frozen(self, monkeypatch, frozen, expected): - """Test "Frozen: ..." in the version output.""" - if frozen: - monkeypatch.setattr(sys, 'frozen', True, raising=False) - else: - monkeypatch.delattr(sys, 'frozen', raising=False) - lines = version.version().splitlines() - assert lines[11] == expected + if git_commit: + substitutions = {'git_commit': 'Git commit: GIT COMMIT\n'} + else: + substitutions = {'git_commit': ''} - def test_platform(self, monkeypatch): - """Test platform in the version output.""" - monkeypatch.setattr('qutebrowser.utils.version.platform.platform', - lambda: 'toaster') - monkeypatch.setattr('qutebrowser.utils.version.platform.architecture', - lambda: ('64bit', '')) - lines = version.version().splitlines() - assert lines[12] == 'Platform: toaster, 64bit' + if not short: + template += textwrap.dedent(""" + Style: STYLE + Desktop: DESKTOP + MODULE VERSION 1 + MODULE VERSION 2 + Webkit: WEBKIT VERSION + Harfbuzz: {harfbuzz} + SSL: SSL VERSION - def test_os_info(self, monkeypatch): - """Test OS info in the output.""" - monkeypatch.setattr('qutebrowser.utils.version._os_info', - lambda: ['Hello', 'World']) - lines = version.version().splitlines() - assert (lines[13], lines[14]) == ('Hello', 'World') + Frozen: {frozen} + Platform: PLATFORM, ARCHITECTURE + OS INFO 1 + OS INFO 2 + """.lstrip('\n')) + + substitutions['harfbuzz'] = 'HARFBUZZ' if harfbuzz else 'system' + substitutions['frozen'] = str(frozen) + + expected = template.rstrip('\n').format(**substitutions) + assert version.version(short=short) == expected From 086010d81e61c9e2f804c9ee87cc533d05cd487a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 26 Jun 2015 22:38:58 +0200 Subject: [PATCH 138/146] tox: Update py (pylib) to 1.4.30. Upstream changelog: fix issue68 an assert with a multiline list comprehension was not reported correctly. Thanks Henrik Heibuerger. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index af44b9b00..83945f9e8 100644 --- a/tox.ini +++ b/tox.ini @@ -21,7 +21,7 @@ setenv = QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/pl passenv = PYTHON DISPLAY XAUTHORITY HOME deps = -r{toxinidir}/requirements.txt - py==1.4.29 + py==1.4.30 pytest==2.7.2 pytest-capturelog==0.7 pytest-qt==1.4.0 From ddf7f202d8fcabeeec5a84de02596797c89e28ba Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 26 Jun 2015 22:40:16 +0200 Subject: [PATCH 139/146] Set default for new-instance-open-target to tab. --- qutebrowser/config/configdata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index a39439d7c..3c57ae43d 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -208,7 +208,7 @@ def data(readonly=False): "be used."), ('new-instance-open-target', - SettingValue(typ.NewInstanceOpenTarget(), 'window'), + SettingValue(typ.NewInstanceOpenTarget(), 'tab'), "How to open links in an existing instance if a new one is " "launched."), From 93f5e30a0057dc95ff2e66d1f8f713d712c7607d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 26 Jun 2015 22:41:30 +0200 Subject: [PATCH 140/146] tox: Whoops, update all py-lines. --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 83945f9e8..6676915ee 100644 --- a/tox.ini +++ b/tox.ini @@ -90,7 +90,7 @@ commands = {envpython} -m pep257 scripts tests qutebrowser --ignore=D102,D103,D2 setenv = LANG=en_US.UTF-8 deps = -r{toxinidir}/requirements.txt - py==1.4.29 + py==1.4.30 pytest==2.7.2 pyflakes==0.9.2 pytest-flakes==1.0.0 @@ -101,7 +101,7 @@ commands = [testenv:pep8] deps = -r{toxinidir}/requirements.txt - py==1.4.29 + py==1.4.30 pytest==2.7.2 pep8==1.6.2 pytest-pep8==1.0.6 @@ -112,7 +112,7 @@ commands = [testenv:mccabe] deps = -r{toxinidir}/requirements.txt - py==1.4.29 + py==1.4.30 pytest==2.7.2 mccabe==0.3.1 pytest-mccabe==0.1 From 9eaa0d09681a5b4d3838280764299b5d8e3c68fa Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 26 Jun 2015 22:44:01 +0200 Subject: [PATCH 141/146] Update changelog. --- CHANGELOG.asciidoc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index d8bbd4745..6dc725b45 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -57,6 +57,7 @@ Changed - The `content -> cookies-accept` setting now has new `no-3rdparty` (default) and `no-unknown-3rdparty` values to block third-party cookies. The `default` value got renamed to `all`. - Improved startup time by reading the webpage history while qutebrowser is open. - The way `:spawn` splits its commandline has been changed slightly to allow commands with flags. +- The default for the `new-instance-open-target` setting has been changed to `tab`. Deprecated ~~~~~~~~~~ @@ -76,6 +77,8 @@ Fixed - Small improvements when checking if an input is an URL or not. - Fixed wrong cursor position when completing the first item in the completion. - Fix exception when using search engines with {foo} in the name. +- Fix a bug where the same title was shown for all tabs on some systems. +- Don't install the scripts package when installing qutebrowser. v0.2.2 (unreleased) ------------------- From 58b738ca5b3a3a08ed0b3ecd04e2e57dd4785e8d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 26 Jun 2015 22:44:52 +0200 Subject: [PATCH 142/146] Regenerate docs. --- doc/help/settings.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 2d0f9364c..1186f73c3 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -418,7 +418,7 @@ Valid values: * +tab-bg-silent+: Open a new background tab in the existing window without activating the window. * +window+: Open in a new window. -Default: +pass:[window]+ +Default: +pass:[tab]+ [[general-log-javascript-console]] === log-javascript-console From b3869fe42b3a68651626a21f325109767a289a3a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 27 Jun 2015 18:06:18 +0200 Subject: [PATCH 143/146] sessions: Store zoom/scroll-pos per history entry. Also fixes #728. --- qutebrowser/misc/sessions.py | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index bd75c634b..89fba7bcd 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -145,21 +145,23 @@ class SessionManager(QObject): if item.originalUrl() != item.url(): encoded = item.originalUrl().toEncoded() item_data['original-url'] = bytes(encoded).decode('ascii') - user_data = item.userData() + if history.currentItemIndex() == idx: item_data['active'] = True - if user_data is None: - pos = tab.page().mainFrame().scrollPosition() - data['zoom'] = tab.zoomFactor() - data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()} - data['history'].append(item_data) - if user_data is not None: + user_data = item.userData() + if history.currentItemIndex() == idx: + pos = tab.page().mainFrame().scrollPosition() + item_data['zoom'] = tab.zoomFactor() + item_data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()} + elif user_data is not None: if 'zoom' in user_data: - data['zoom'] = user_data['zoom'] + item_data['zoom'] = user_data['zoom'] if 'scroll-pos' in user_data: pos = user_data['scroll-pos'] - data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()} + item_data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()} + + data['history'].append(item_data) return data def _save_all(self): @@ -235,11 +237,25 @@ class SessionManager(QObject): entries = [] for histentry in data['history']: user_data = {} + if 'zoom' in data: + # The zoom was accidentally stored in 'data' instead of per-tab + # earlier. + # See https://github.com/The-Compiler/qutebrowser/issues/728 user_data['zoom'] = data['zoom'] + elif 'zoom' in histentry: + user_data['zoom'] = histentry['zoom'] + if 'scroll-pos' in data: + # The scroll position was accidentally stored in 'data' instead + # of per-tab earlier. + # See https://github.com/The-Compiler/qutebrowser/issues/728 pos = data['scroll-pos'] user_data['scroll-pos'] = QPoint(pos['x'], pos['y']) + elif 'scroll-pos' in histentry: + pos = histentry['scroll-pos'] + user_data['scroll-pos'] = QPoint(pos['x'], pos['y']) + active = histentry.get('active', False) url = QUrl.fromEncoded(histentry['url'].encode('ascii')) if 'original-url' in histentry: From bf4e968c6754bc4b185f6f469487b9ff92734a2a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Jun 2015 07:58:15 +0200 Subject: [PATCH 144/146] Add new completion -> auto-open option. Closes #557. --- qutebrowser/completion/completer.py | 82 +++++++++++++++++++++- qutebrowser/completion/completionwidget.py | 21 ++---- qutebrowser/config/configdata.py | 4 ++ 3 files changed, 89 insertions(+), 18 deletions(-) diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py index ebcaed67d..945f3636f 100644 --- a/qutebrowser/completion/completer.py +++ b/qutebrowser/completion/completer.py @@ -19,7 +19,7 @@ """Completer attached to a CompletionView.""" -from PyQt5.QtCore import pyqtSlot, QObject, QTimer +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer from qutebrowser.config import config from qutebrowser.commands import cmdutils, runners @@ -40,14 +40,22 @@ class Completer(QObject): _last_cursor_pos: The old cursor position so we avoid double completion updates. _last_text: The old command text so we avoid double completion updates. + _signals_connected: Whether the signals are connected to update the + completion when the command widget requests that. + + Signals: + next_prev_item: Emitted to select the next/previous item in the + completion. + arg0: True for the previous item, False for the next. """ + next_prev_item = pyqtSignal(bool) + def __init__(self, cmd, win_id, parent=None): super().__init__(parent) self._win_id = win_id self._cmd = cmd - self._cmd.update_completion.connect(self.schedule_completion_update) - self._cmd.textEdited.connect(self.on_text_edited) + self._signals_connected = False self._ignore_change = False self._empty_item_idx = None self._timer = QTimer() @@ -58,9 +66,63 @@ class Completer(QObject): self._last_cursor_pos = None self._last_text = None + objreg.get('config').changed.connect(self.on_auto_open_changed) + self.handle_signal_connections() + self._cmd.clear_completion_selection.connect( + self.handle_signal_connections) + def __repr__(self): return utils.get_repr(self) + @config.change_filter('completion', 'auto-open') + def on_auto_open_changed(self): + self.handle_signal_connections() + + @pyqtSlot() + def handle_signal_connections(self): + self._connect_signals(config.get('completion', 'auto-open')) + + def _connect_signals(self, connect=True): + """Connect or disconnect the completion signals. + + Args: + connect: Whether to connect (True) or disconnect (False) the + signals. + + Return: + True if the signals were connected (connect=True and aren't + connected yet) - otherwise False. + """ + connections = [ + (self._cmd.update_completion, self.schedule_completion_update), + (self._cmd.textChanged, self.on_text_edited), + ] + + if connect and not self._signals_connected: + for sender, receiver in connections: + sender.connect(receiver) + self._signals_connected = True + return True + elif not connect: + for sender, receiver in connections: + try: + sender.disconnect(receiver) + except TypeError: + # Don't fail if not connected + pass + self._signals_connected = False + return False + + def _open_completion_if_needed(self): + """If auto-open is false, temporarily connect signals. + + Also opens the completion. + """ + if not config.get('completion', 'auto-open'): + connected = self._connect_signals(True) + if connected: + self.update_completion() + def _model(self): """Convienience method to get the current completion model.""" completion = objreg.get('completion', scope='window', @@ -405,3 +467,17 @@ class Completer(QObject): # We also want to update the cursor part and emit update_completion # here, but that's already done for us by cursorPositionChanged # anyways, so we don't need to do it twice. + + @cmdutils.register(instance='completer', hide=True, + modes=[usertypes.KeyMode.command], scope='window') + def completion_item_prev(self): + """Select the previous completion item.""" + self._open_completion_if_needed() + self.next_prev_item.emit(True) + + @cmdutils.register(instance='completer', hide=True, + modes=[usertypes.KeyMode.command], scope='window') + def completion_item_next(self): + """Select the next completion item.""" + self._open_completion_if_needed() + self.next_prev_item.emit(False) diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index 58532058d..a3bea931a 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -26,10 +26,9 @@ subclasses to provide completions. from PyQt5.QtWidgets import QStyle, QTreeView, QSizePolicy from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel -from qutebrowser.commands import cmdutils from qutebrowser.config import config, style from qutebrowser.completion import completiondelegate, completer -from qutebrowser.utils import usertypes, qtutils, objreg, utils +from qutebrowser.utils import qtutils, objreg, utils class CompletionView(QTreeView): @@ -96,6 +95,7 @@ class CompletionView(QTreeView): objreg.register('completion', self, scope='window', window=win_id) cmd = objreg.get('status-command', scope='window', window=win_id) completer_obj = completer.Completer(cmd, win_id, self) + completer_obj.next_prev_item.connect(self.on_next_prev_item) objreg.register('completer', completer_obj, scope='window', window=win_id) self.enabled = config.get('completion', 'show') @@ -168,12 +168,15 @@ class CompletionView(QTreeView): # Item is a real item, not a category header -> success return idx - def _next_prev_item(self, prev): + @pyqtSlot(bool) + def on_next_prev_item(self, prev): """Handle a tab press for the CompletionView. Select the previous/next item and write the new text to the statusbar. + Called from the Completer's next_prev_item signal. + Args: prev: True for prev item, False for next one. """ @@ -233,18 +236,6 @@ class CompletionView(QTreeView): selmod.clearSelection() selmod.clearCurrentIndex() - @cmdutils.register(instance='completion', hide=True, - modes=[usertypes.KeyMode.command], scope='window') - def completion_item_prev(self): - """Select the previous completion item.""" - self._next_prev_item(prev=True) - - @cmdutils.register(instance='completion', hide=True, - modes=[usertypes.KeyMode.command], scope='window') - def completion_item_next(self): - """Select the next completion item.""" - self._next_prev_item(prev=False) - def selectionChanged(self, selected, deselected): """Extend selectionChanged to call completers selection_changed.""" super().selectionChanged(selected, deselected) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 3c57ae43d..15c41d5ef 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -352,6 +352,10 @@ def data(readonly=False): )), ('completion', sect.KeyValue( + ('auto-open', + SettingValue(typ.Bool(), 'true'), + "Automatically open completion when typing."), + ('download-path-suggestion', SettingValue(typ.DownloadPathSuggestion(), 'path'), "What to display in the download filename input."), From 63dee327c9d69c85b7b39a4a36570204edb77233 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 27 Jun 2015 19:59:06 +0200 Subject: [PATCH 145/146] Update docs. --- doc/help/settings.asciidoc | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 1186f73c3..c6e9c6923 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -65,6 +65,7 @@ [options="header",width="75%",cols="25%,75%"] |============== |Setting|Description +|<>|Automatically open completion when typing. |<>|What to display in the download filename input. |<>|How to format timestamps (e.g. for history) |<>|Whether to show the autocompletion window. @@ -694,6 +695,17 @@ Default: +pass:[true]+ == completion Options related to completion and command history. +[[completion-auto-open]] +=== auto-open +Automatically open completion when typing. + +Valid values: + + * +true+ + * +false+ + +Default: +pass:[true]+ + [[completion-download-path-suggestion]] === download-path-suggestion What to display in the download filename input. From f31f254d9bf3ffd4ef95089f4924e5c45d8c0f78 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 27 Jun 2015 20:27:23 +0200 Subject: [PATCH 146/146] Update changelog for v0.3. --- CHANGELOG.asciidoc | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 6dc725b45..feed0fed2 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -21,33 +21,33 @@ Added ~~~~~ - New commands `:message-info`, `:message-error` and `:message-warning` to show messages in the statusbar, e.g. from an userscript. -- There are now some example userscripts in `misc/userscripts`. - New command `:scroll-px` which replaces `:scroll` for pixel-exact scrolling. +- New command `:jseval` to run a javascript snippet on the current page. +- New (hidden) command `:follow-selected` (bound to `Enter`/`Ctrl-Enter` by default) to follow the link which is currently selected (e.g. after searching via `/`). +- New (hidden) command `:clear-keychain` to clear a partially entered keychain (bound to `` by default, in addition to clearing search). - New setting `ui -> smooth-scrolling`. - New setting `content -> webgl` to enable/disable https://www.khronos.org/webgl/[WebGL]. - New setting `content -> css-regions` to enable/disable support for http://dev.w3.org/csswg/css-regions/[CSS Regions]. - New setting `content -> hyperlink-auditing` to enable/disable support for https://html.spec.whatwg.org/multipage/semantics.html#hyperlink-auditing[hyperlink auditing]. -- Support for Qt 5.5 and tox 2.0 +- New setting `tabs -> mousewheel-tab-switching` to control mousewheel behavior on the tab bar. - New arguments `--datadir` and `--cachedir` to set the data/cache location. - New arguments `--basedir` and `--temp-basedir` (intended for debugging) to set a different base directory for all data, which allows multiple invocations. - New argument `--no-err-windows` to suppress all error windows. -- New visual/caret mode (bound to `v`) to select text by keyboard. -- New setting `tabs -> mousewheel-tab-switching` to control mousewheel behavior on the tab bar. - New arguments `--top-navigate` and `--bottom-navigate` (`-t`/`-b`) for `:scroll-page` to specify a navigation action (e.g. automatically go to the next page when arriving at the bottom). - New flag `-d`/`--detach` for `:spawn` to detach the spawned process so it's not closed when qutebrowser is. - New flag `-v`/`--verbose` for `:spawn` to print informations when the process started/exited successfully. -- New (hidden) command `:follow-selected` (bound to `Enter`/`Ctrl-Enter` by default) to follow the link which is currently selected (e.g. after searching via `/`). +- Many new color settings (foreground setting for every background setting). - New setting `ui -> modal-js-dialog` to use the standard modal dialogs for javascript questions instead of using the statusbar. - New setting `colors -> webpage.bg` to set the background color to use for websites which don't set one. -- New (hidden) command `:clear-keychain` to clear a partially entered keychain (bound to `` by default, in addition to clearing search). -- Many new color settings (foreground setting for every background setting). -- New command `:jseval` to run a javascript snippet on the current page. +- New setting `completion -> auto-open` to only open the completion when tab is pressed (if set to false). +- New visual/caret mode (bound to `v`) to select text by keyboard. +- There are now some example userscripts in `misc/userscripts`. +- Support for Qt 5.5 and tox 2.0 Changed ~~~~~~~ -- `QUTE_HTML` and `QUTE_TEXT` for userscripts now don't store the contents directly, and instead contain a filename. -- `:spawn` now shows the command being executed in the statusbar, use `-q`/`--quiet` for the old behavior. +- *Breaking change for userscripts:* `QUTE_HTML` and `QUTE_TEXT` for userscripts now don't store the contents directly, and instead contain a filename. - The `content -> geolocation` and `notifications` settings now support a `true` value to always allow those. However, this is *not recommended*. - New bindings `` (rapid), `` (foreground) and `` (background) to switch hint modes while hinting. - `` and numpad-enter are now bound by default for bindings where `` was bound. @@ -58,6 +58,7 @@ Changed - Improved startup time by reading the webpage history while qutebrowser is open. - The way `:spawn` splits its commandline has been changed slightly to allow commands with flags. - The default for the `new-instance-open-target` setting has been changed to `tab`. +- Sessions now store zoom/scroll-position separately for each entry. Deprecated ~~~~~~~~~~ @@ -76,26 +77,20 @@ Fixed - Scrolling should now work more reliably on some pages where arrow keys worked but `hjkl` didn't. - Small improvements when checking if an input is an URL or not. - Fixed wrong cursor position when completing the first item in the completion. -- Fix exception when using search engines with {foo} in the name. -- Fix a bug where the same title was shown for all tabs on some systems. +- Fixed exception when using search engines with {foo} in their name. +- Fixed a bug where the same title was shown for all tabs on some systems. - Don't install the scripts package when installing qutebrowser. - -v0.2.2 (unreleased) -------------------- - -Fixed -~~~~~ - - Fixed searching for terms starting with a hyphen (e.g. `/-foo`) - Proxy authentication credentials are now remembered between different tabs. - Fixed updating of the tab title on pages without title. - Fixed AssertionError when closing many windows quickly. - Various fixes for deprecated key bindings and auto-migrations. -- Workaround for qutebrowser not starting when there are NUL-bytes in the history (because of a currently unknown bug) +- Workaround for qutebrowser not starting when there are NUL-bytes in the history (because of a currently unknown bug). - Fixed handling of keybindings containing Ctrl/Meta on OS X. - Fixed crash when downloading an URL without filename (e.g. magnet links) via "Save as...". - Fixed exception when starting qutebrowser with `:set` as argument. - Fixed horrible completion performance when the `shrink` option was set. +- Sessions now store zoom/scroll-position correctly. https://github.com/The-Compiler/qutebrowser/releases/tag/v0.2.1[v0.2.1] -----------------------------------------------------------------------