From 6f610e9c44e381e11aeee678b32455bef9119d59 Mon Sep 17 00:00:00 2001 From: thuck Date: Sun, 6 Nov 2016 15:52:23 +0100 Subject: [PATCH 001/825] Initial development to support pin tabs #926 Done so far: Two new commands pin/unpin, both accept a index to help the organization (maybe this should be more a flag and not exactly two commands) Crtl+p to pin, Crtl+O to unpin (not sure which should a good default shortcut) If user tries to close a pinned tab it's asked to confirm If user tries to open a URL in a pinned tab it receives a message with a information that the tab is pinned and ignore the openurl command Preserve the pinned information across restart if session is activated Missing: Visual indication of the tab being pinned Tab appearance being distinct over other tabs Make pinned tabs to be the firsts on the tab bar This is not ready, but it would be good to get some feedback earlier --- qutebrowser/browser/browsertab.py | 1 + qutebrowser/browser/commands.py | 34 +++++++++++++++++++++++++++++++ qutebrowser/config/configdata.py | 2 ++ qutebrowser/misc/sessions.py | 6 ++++++ 4 files changed, 43 insertions(+) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 22f1c7e51..67d2bee44 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -560,6 +560,7 @@ class AbstractTab(QWidget): self._mouse_event_filter = mouse.MouseEventFilter( self, widget_class=self.WIDGET_CLASS, parent=self) self.backend = None + self.pin = False # FIXME:qtwebengine Should this be public api via self.hints? # Also, should we get it out of objreg? diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 2c0e01b5f..3ee28493e 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -227,6 +227,14 @@ class CommandDispatcher: tab = self._cntwidget(count) if tab is None: return + + if tab.pin is True: + result = message.ask("Are you sure you want to close a pinned tab?", + mode=usertypes.PromptMode.yesno, default=False) + + if result is False or result is None: + return + tabbar = self._tabbed_browser.tabBar() selection_override = self._get_selection_override(left, right, opposite) @@ -238,6 +246,29 @@ class CommandDispatcher: self._tabbed_browser.close_tab(tab) tabbar.setSelectionBehaviorOnRemove(old_selection_behavior) + @cmdutils.register(instance='command-dispatcher', scope='window', name='pin') + @cmdutils.argument('index') + @cmdutils.argument('count', count=True) + def tab_pin(self, index=1, count=None): + tab = self._cntwidget(count) + if tab is None: + return + tab.pin = True + self.tab_move(int(index)) + + @cmdutils.register(instance='command-dispatcher', scope='window', name='unpin') + @cmdutils.argument('index') + @cmdutils.argument('count', count=True) + def tab_unpin(self, index=None, count=None): + tab = self._cntwidget(count) + if tab is None: + return + tab.pin = False + if index is not None: + self.tab_move(int(index)) + else: + self.tab_move(self._count()) + @cmdutils.register(instance='command-dispatcher', name='open', maxsplit=0, scope='window') @cmdutils.argument('url', completion=usertypes.Completion.url) @@ -281,6 +312,9 @@ class CommandDispatcher: else: # Explicit count with a tab that doesn't exist. return + elif curtab.pin is True: + message.info("Tab is pinned!") + else: curtab.openurl(cur_url) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index bad6fa51a..3219b5c9b 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1652,6 +1652,8 @@ KEY_DATA = collections.OrderedDict([ ('follow-selected', RETURN_KEYS), ('follow-selected -t', ['', '']), ('repeat-command', ['.']), + ('pin', ['']), + ('unpin', ['']), ])), ('insert', collections.OrderedDict([ diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index c3a3a8e34..d7c844c77 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -205,6 +205,9 @@ class SessionManager(QObject): if 'scroll-pos' in user_data: pos = user_data['scroll-pos'] data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()} + + data['pin'] = tab.pin + return data def _save_tab(self, tab, active): @@ -332,6 +335,9 @@ class SessionManager(QObject): pos = histentry['scroll-pos'] user_data['scroll-pos'] = QPoint(pos['x'], pos['y']) + if 'pin' in histentry: + new_tab.pin = histentry['pin'] + active = histentry.get('active', False) url = QUrl.fromEncoded(histentry['url'].encode('ascii')) if 'original-url' in histentry: From 22133beb724ba8289d3c0c417d3aec4124d3aa73 Mon Sep 17 00:00:00 2001 From: thuck Date: Sun, 6 Nov 2016 18:24:33 +0100 Subject: [PATCH 002/825] Fix small bug because result was not declared --- qutebrowser/browser/commands.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 3ee28493e..5511dfeed 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -225,6 +225,7 @@ class CommandDispatcher: count: The tab index to close, or None """ tab = self._cntwidget(count) + result = True if tab is None: return From 6d7a6db130367b82bb014ea4165e254191148951 Mon Sep 17 00:00:00 2001 From: thuck Date: Sun, 6 Nov 2016 19:04:32 +0100 Subject: [PATCH 003/825] Proper title and size for pinned tab As I'm using self.count() without taking in consideration the number of pinned tabs the end result is a lot of empty space. --- qutebrowser/mainwindow/tabwidget.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index 657a38dc3..be77f3631 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -107,7 +107,12 @@ class TabWidget(QTabWidget): fields['index'] = idx + 1 fmt = config.get('tabs', 'title-format') - title = '' if fmt is None else fmt.format(**fields) + + if fields['pin'] is True: + title = '{index}'.format(**fields) + else: + title = '' if fmt is None else fmt.format(**fields) + self.tabBar().setTabText(idx, title) def get_tab_fields(self, idx): @@ -120,6 +125,7 @@ class TabWidget(QTabWidget): fields['title'] = page_title fields['title_sep'] = ' - ' if page_title else '' fields['perc_raw'] = tab.progress() + fields['pin'] = tab.pin if tab.load_status() == usertypes.LoadStatus.loading: fields['perc'] = '[{}%] '.format(tab.progress()) @@ -439,11 +445,21 @@ class TabBar(QTabBar): # get scroll buttons as soon as needed. size = minimum_size else: + #TODO: relative size and/or configured one + tab = objreg.get('tab', scope='tab', window=self._win_id, tab=index) + if tab.pin is True: + size = QSize(40, height) + qtutils.ensure_valid(size) + return size # If we *do* have enough space, tabs should occupy the whole window # width. + #looks like this generates high cpu usage + #need to register the number of pin tabs in advance + #nb_of_pins = len([None for item in range(self.count()) if objreg.get('tab', scope='tab', window=self._win_id, tab=item).pin is True]) + #width = (self.width() + 40*nb_of_pins) / self.count() width = self.width() / self.count() - # If width is not divisible by count, add a pixel to some tabs so - # that there is no ugly leftover space. + ## If width is not divisible by count, add a pixel to some tabs so + ## that there is no ugly leftover space. if index < self.width() % self.count(): width += 1 size = QSize(width, height) From d592651c504b57901b5c33c23041da9063a3a80b Mon Sep 17 00:00:00 2001 From: thuck Date: Sun, 6 Nov 2016 23:24:24 +0100 Subject: [PATCH 004/825] Change command from pin/unpin to tab-pin --- qutebrowser/browser/commands.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 5511dfeed..4fb076d80 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -247,28 +247,22 @@ class CommandDispatcher: self._tabbed_browser.close_tab(tab) tabbar.setSelectionBehaviorOnRemove(old_selection_behavior) - @cmdutils.register(instance='command-dispatcher', scope='window', name='pin') + @cmdutils.register(instance='command-dispatcher', scope='window', name='tab-pin') @cmdutils.argument('index') @cmdutils.argument('count', count=True) - def tab_pin(self, index=1, count=None): + def tab_pin(self, index=None, count=None): tab = self._cntwidget(count) if tab is None: return - tab.pin = True - self.tab_move(int(index)) - @cmdutils.register(instance='command-dispatcher', scope='window', name='unpin') - @cmdutils.argument('index') - @cmdutils.argument('count', count=True) - def tab_unpin(self, index=None, count=None): - tab = self._cntwidget(count) - if tab is None: - return - tab.pin = False - if index is not None: - self.tab_move(int(index)) + tab.pin = not tab.pin + + if tab.pin is True: + index = 1 if index is None else int(index) else: - self.tab_move(self._count()) + index = self._count() if index is None else int(index) + + self.tab_move(index) @cmdutils.register(instance='command-dispatcher', name='open', maxsplit=0, scope='window') From d7a1a542b6ccc81badc8ea871c5f1c1a2c20325d Mon Sep 17 00:00:00 2001 From: thuck Date: Sun, 6 Nov 2016 23:25:36 +0100 Subject: [PATCH 005/825] Change shortcut to tab-pin --- qutebrowser/config/configdata.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 3219b5c9b..6664e4b8e 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1652,8 +1652,7 @@ KEY_DATA = collections.OrderedDict([ ('follow-selected', RETURN_KEYS), ('follow-selected -t', ['', '']), ('repeat-command', ['.']), - ('pin', ['']), - ('unpin', ['']), + ('tab-pin', ['']), ])), ('insert', collections.OrderedDict([ From 29d1c0d68b48608ba577873935dd9751d5bd3548 Mon Sep 17 00:00:00 2001 From: thuck Date: Sun, 6 Nov 2016 23:27:06 +0100 Subject: [PATCH 006/825] Small fix for situations where we cannot find the tab for the index Need to investigate better why and when this is excatly happening --- qutebrowser/mainwindow/tabwidget.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index be77f3631..4f1d45842 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -446,11 +446,14 @@ class TabBar(QTabBar): size = minimum_size else: #TODO: relative size and/or configured one - tab = objreg.get('tab', scope='tab', window=self._win_id, tab=index) - if tab.pin is True: - size = QSize(40, height) - qtutils.ensure_valid(size) - return size + try: + tab = objreg.get('tab', scope='tab', window=self._win_id, tab=index) + if tab.pin is True: + size = QSize(40, height) + qtutils.ensure_valid(size) + return size + except KeyError: + pass # If we *do* have enough space, tabs should occupy the whole window # width. #looks like this generates high cpu usage From f8dffb4e5c56b663eb4678ee82aa56043899377d Mon Sep 17 00:00:00 2001 From: thuck Date: Mon, 7 Nov 2016 08:02:25 +0100 Subject: [PATCH 007/825] Some modifications from initial feedback Moved pin information from BrowserTab to TabData. Changed attribute from pin to pinned. Changed "ifs" to implicit check boolen value. Removed blancked line on before else statement. --- qutebrowser/browser/browsertab.py | 3 ++- qutebrowser/browser/commands.py | 15 ++++++++++----- qutebrowser/mainwindow/tabwidget.py | 4 ++-- qutebrowser/misc/sessions.py | 4 ++-- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 67d2bee44..843447825 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -86,6 +86,7 @@ class TabData: viewing_source: Set if we're currently showing a source view. open_target: How the next clicked link should be opened. override_target: Override for open_target for fake clicks (like hints). + pinned: Flag to pin the tab """ def __init__(self): @@ -94,6 +95,7 @@ class TabData: self.inspector = None self.open_target = usertypes.ClickTarget.normal self.override_target = None + self.pinned = False def combined_target(self): if self.override_target is not None: @@ -560,7 +562,6 @@ class AbstractTab(QWidget): self._mouse_event_filter = mouse.MouseEventFilter( self, widget_class=self.WIDGET_CLASS, parent=self) self.backend = None - self.pin = False # FIXME:qtwebengine Should this be public api via self.hints? # Also, should we get it out of objreg? diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 4fb076d80..d6ab0771a 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -229,7 +229,7 @@ class CommandDispatcher: if tab is None: return - if tab.pin is True: + if tab.data.pinned: result = message.ask("Are you sure you want to close a pinned tab?", mode=usertypes.PromptMode.yesno, default=False) @@ -251,13 +251,19 @@ class CommandDispatcher: @cmdutils.argument('index') @cmdutils.argument('count', count=True) def tab_pin(self, index=None, count=None): + """Pin/Unpin the current tab. + + Args: + index: Location where the tab should be pinned/unpinned. + count: The tab index to pin or unpin + """ tab = self._cntwidget(count) if tab is None: return - tab.pin = not tab.pin + tab.data.pinned = not tab.data.pinned - if tab.pin is True: + if tab.data.pinned: index = 1 if index is None else int(index) else: index = self._count() if index is None else int(index) @@ -307,9 +313,8 @@ class CommandDispatcher: else: # Explicit count with a tab that doesn't exist. return - elif curtab.pin is True: + elif curtab.data.pinned: message.info("Tab is pinned!") - else: curtab.openurl(cur_url) diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index 4f1d45842..a6d56014b 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -125,7 +125,7 @@ class TabWidget(QTabWidget): fields['title'] = page_title fields['title_sep'] = ' - ' if page_title else '' fields['perc_raw'] = tab.progress() - fields['pin'] = tab.pin + fields['pin'] = tab.data.pinned if tab.load_status() == usertypes.LoadStatus.loading: fields['perc'] = '[{}%] '.format(tab.progress()) @@ -448,7 +448,7 @@ class TabBar(QTabBar): #TODO: relative size and/or configured one try: tab = objreg.get('tab', scope='tab', window=self._win_id, tab=index) - if tab.pin is True: + if tab.data.pinned: size = QSize(40, height) qtutils.ensure_valid(size) return size diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index d7c844c77..ce1582413 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -206,7 +206,7 @@ class SessionManager(QObject): pos = user_data['scroll-pos'] data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()} - data['pin'] = tab.pin + data['pin'] = tab.data.pinned return data @@ -336,7 +336,7 @@ class SessionManager(QObject): user_data['scroll-pos'] = QPoint(pos['x'], pos['y']) if 'pin' in histentry: - new_tab.pin = histentry['pin'] + new_tab.data.pinned = histentry['pin'] active = histentry.get('active', False) url = QUrl.fromEncoded(histentry['url'].encode('ascii')) From 20eae4d671642413aa5008ae1d3e185d69b89a10 Mon Sep 17 00:00:00 2001 From: thuck Date: Mon, 7 Nov 2016 08:11:47 +0100 Subject: [PATCH 008/825] Modifed exception structure --- qutebrowser/mainwindow/tabwidget.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index a6d56014b..bfdf53e45 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -448,12 +448,14 @@ class TabBar(QTabBar): #TODO: relative size and/or configured one try: tab = objreg.get('tab', scope='tab', window=self._win_id, tab=index) + except KeyError: + pass + else: if tab.data.pinned: size = QSize(40, height) qtutils.ensure_valid(size) return size - except KeyError: - pass + # If we *do* have enough space, tabs should occupy the whole window # width. #looks like this generates high cpu usage From 4ed046d5e71811da7659a820c4b5882935c06ddc Mon Sep 17 00:00:00 2001 From: thuck Date: Mon, 7 Nov 2016 21:12:34 +0100 Subject: [PATCH 009/825] Everything is pinned instead of pin, and one if corrected --- qutebrowser/browser/commands.py | 2 +- qutebrowser/mainwindow/tabwidget.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index d6ab0771a..c3de57387 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -255,7 +255,7 @@ class CommandDispatcher: Args: index: Location where the tab should be pinned/unpinned. - count: The tab index to pin or unpin + count: The tab index to pin or unpin, or None """ tab = self._cntwidget(count) if tab is None: diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index bfdf53e45..8d99d5596 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -108,7 +108,7 @@ class TabWidget(QTabWidget): fmt = config.get('tabs', 'title-format') - if fields['pin'] is True: + if fields['pinned']: title = '{index}'.format(**fields) else: title = '' if fmt is None else fmt.format(**fields) @@ -125,7 +125,7 @@ class TabWidget(QTabWidget): fields['title'] = page_title fields['title_sep'] = ' - ' if page_title else '' fields['perc_raw'] = tab.progress() - fields['pin'] = tab.data.pinned + fields['pinned'] = tab.data.pinned if tab.load_status() == usertypes.LoadStatus.loading: fields['perc'] = '[{}%] '.format(tab.progress()) From ec50d395781cd99c6e7b8ff95c220aed7a18e2ee Mon Sep 17 00:00:00 2001 From: thuck Date: Mon, 7 Nov 2016 21:25:05 +0100 Subject: [PATCH 010/825] Some fixes for the pylint --- qutebrowser/browser/commands.py | 5 +++-- qutebrowser/mainwindow/tabwidget.py | 7 +++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index c3de57387..11dbb5288 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -247,7 +247,8 @@ class CommandDispatcher: self._tabbed_browser.close_tab(tab) tabbar.setSelectionBehaviorOnRemove(old_selection_behavior) - @cmdutils.register(instance='command-dispatcher', scope='window', name='tab-pin') + @cmdutils.register(instance='command-dispatcher', scope='window', + name='tab-pin') @cmdutils.argument('index') @cmdutils.argument('count', count=True) def tab_pin(self, index=None, count=None): @@ -267,7 +268,7 @@ class CommandDispatcher: index = 1 if index is None else int(index) else: index = self._count() if index is None else int(index) - + self.tab_move(index) @cmdutils.register(instance='command-dispatcher', name='open', diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index 8d99d5596..ac9c0ede3 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -447,7 +447,8 @@ class TabBar(QTabBar): else: #TODO: relative size and/or configured one try: - tab = objreg.get('tab', scope='tab', window=self._win_id, tab=index) + tab = objreg.get('tab', scope='tab', + window=self._win_id, tab=index) except KeyError: pass else: @@ -460,7 +461,9 @@ class TabBar(QTabBar): # width. #looks like this generates high cpu usage #need to register the number of pin tabs in advance - #nb_of_pins = len([None for item in range(self.count()) if objreg.get('tab', scope='tab', window=self._win_id, tab=item).pin is True]) + #nb_of_pins = len([None for item in range(self.count()) + # if objreg.get('tab', scope='tab', + # window=self._win_id, tab=item).pin is True]) #width = (self.width() + 40*nb_of_pins) / self.count() width = self.width() / self.count() ## If width is not divisible by count, add a pixel to some tabs so From f10284b04afa39994cc7fd2ad240155470a8f027 Mon Sep 17 00:00:00 2001 From: thuck Date: Mon, 7 Nov 2016 22:28:05 +0100 Subject: [PATCH 011/825] Initial work on message.confirm_async Creation of _tab_close and usage of partial. --- qutebrowser/browser/commands.py | 37 ++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 11dbb5288..32eb80c69 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -212,6 +212,19 @@ class CommandDispatcher: "{!r}!".format(conf_selection)) return None + def _tab_close(self, tab, left=False, right=False, opposite=False, count=None): + tabbar = self._tabbed_browser.tabBar() + selection_override = self._get_selection_override(left, right, + opposite) + if selection_override is None: + self._tabbed_browser.close_tab(tab) + else: + old_selection_behavior = tabbar.selectionBehaviorOnRemove() + tabbar.setSelectionBehaviorOnRemove(selection_override) + self._tabbed_browser.close_tab(tab) + tabbar.setSelectionBehaviorOnRemove(old_selection_behavior) + + @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('count', count=True) def tab_close(self, left=False, right=False, opposite=False, count=None): @@ -229,23 +242,17 @@ class CommandDispatcher: if tab is None: return + close = functools.partial(self._tab_close, tab, left, + right, opposite, count) + if tab.data.pinned: - result = message.ask("Are you sure you want to close a pinned tab?", - mode=usertypes.PromptMode.yesno, default=False) - - if result is False or result is None: - return - - tabbar = self._tabbed_browser.tabBar() - selection_override = self._get_selection_override(left, right, - opposite) - if selection_override is None: - self._tabbed_browser.close_tab(tab) + message.confirm_async(title='Pinned Tab', + text="Are you sure you want to close a pinned tab?", + yes_action=close, default=False) else: - old_selection_behavior = tabbar.selectionBehaviorOnRemove() - tabbar.setSelectionBehaviorOnRemove(selection_override) - self._tabbed_browser.close_tab(tab) - tabbar.setSelectionBehaviorOnRemove(old_selection_behavior) + close() + + @cmdutils.register(instance='command-dispatcher', scope='window', name='tab-pin') From f9b1d998d43cb0c4ea2eb887389d0489b9d29ffb Mon Sep 17 00:00:00 2001 From: thuck Date: Mon, 7 Nov 2016 22:32:42 +0100 Subject: [PATCH 012/825] Last configuration as pin changed to pinned --- qutebrowser/misc/sessions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index ce1582413..f6c6d97b2 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -206,7 +206,7 @@ class SessionManager(QObject): pos = user_data['scroll-pos'] data['scroll-pos'] = {'x': pos.x(), 'y': pos.y()} - data['pin'] = tab.data.pinned + data['pinned'] = tab.data.pinned return data @@ -335,8 +335,8 @@ class SessionManager(QObject): pos = histentry['scroll-pos'] user_data['scroll-pos'] = QPoint(pos['x'], pos['y']) - if 'pin' in histentry: - new_tab.data.pinned = histentry['pin'] + if 'pinned' in histentry: + new_tab.data.pinned = histentry['pinned'] active = histentry.get('active', False) url = QUrl.fromEncoded(histentry['url'].encode('ascii')) From b24ac0ae785cd3d7aa8bd53eef8de934c04d0e7a Mon Sep 17 00:00:00 2001 From: thuck Date: Tue, 8 Nov 2016 04:45:07 +0100 Subject: [PATCH 013/825] More small fixes Removed unsed variables. Removed some empty lines. Inncluded docstring. --- qutebrowser/browser/commands.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 32eb80c69..76e1075a1 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -212,7 +212,18 @@ class CommandDispatcher: "{!r}!".format(conf_selection)) return None - def _tab_close(self, tab, left=False, right=False, opposite=False, count=None): + def _tab_close(self, tab, left=False, right=False, opposite=False): + """Helper function for tab_close be able to handle message.async. + + Args: + tab: Tab select to be closed. + left: Force selecting the tab to the left of the current tab. + right: Force selecting the tab to the right of the current tab. + opposite: Force selecting the tab in the opposite direction of + what's configured in 'tabs->select-on-remove'. + count: The tab index to close, or None + + """ tabbar = self._tabbed_browser.tabBar() selection_override = self._get_selection_override(left, right, opposite) @@ -224,7 +235,6 @@ class CommandDispatcher: self._tabbed_browser.close_tab(tab) tabbar.setSelectionBehaviorOnRemove(old_selection_behavior) - @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('count', count=True) def tab_close(self, left=False, right=False, opposite=False, count=None): @@ -238,12 +248,11 @@ class CommandDispatcher: count: The tab index to close, or None """ tab = self._cntwidget(count) - result = True if tab is None: return close = functools.partial(self._tab_close, tab, left, - right, opposite, count) + right, opposite) if tab.data.pinned: message.confirm_async(title='Pinned Tab', @@ -252,8 +261,6 @@ class CommandDispatcher: else: close() - - @cmdutils.register(instance='command-dispatcher', scope='window', name='tab-pin') @cmdutils.argument('index') From 4f0034911a49c0c4f4ff06fe013666e649e8f2cb Mon Sep 17 00:00:00 2001 From: thuck Date: Tue, 8 Nov 2016 07:56:13 +0100 Subject: [PATCH 014/825] title-format-pinned initial work Created configuration configdata. Load and use template defined on configdata. TODO: ability to conserve information between restart TODO: ability to update title on configuration change --- qutebrowser/config/configdata.py | 17 +++++++++++++++++ qutebrowser/mainwindow/tabwidget.py | 3 ++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 6664e4b8e..1dadb4fb1 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -679,6 +679,23 @@ def data(readonly=False): "* `{scroll_pos}`: The page scroll position.\n" "* `{host}`: The host of the current web page."), + ('title-format-pinned', + SettingValue(typ.FormatString( + fields=['perc', 'perc_raw', 'title', 'title_sep', 'index', + 'id', 'scroll_pos', 'host'], none_ok=True), + '{index}'), + "The format to use for the pinned tab title." + " The following placeholders are defined:\n\n" + "* `{perc}`: The percentage as a string like `[10%]`.\n" + "* `{perc_raw}`: The raw percentage, e.g. `10`\n" + "* `{title}`: The title of the current web page\n" + "* `{title_sep}`: The string ` - ` if a title is set, empty " + "otherwise.\n" + "* `{index}`: The index of this tab.\n" + "* `{id}`: The internal tab ID of this tab.\n" + "* `{scroll_pos}`: The page scroll position.\n" + "* `{host}`: The host of the current web page."), + ('title-alignment', SettingValue(typ.TextAlignment(), 'left'), "Alignment of the text inside of tabs"), diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index ac9c0ede3..e22c7a75f 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -107,9 +107,10 @@ class TabWidget(QTabWidget): fields['index'] = idx + 1 fmt = config.get('tabs', 'title-format') + fmt_pinned = config.get('tabs', 'title-format-pinned') if fields['pinned']: - title = '{index}'.format(**fields) + title = fmt_pinned.format(**fields) else: title = '' if fmt is None else fmt.format(**fields) From 931b008f892e4d6541bca3a0982635973be8680b Mon Sep 17 00:00:00 2001 From: thuck Date: Tue, 8 Nov 2016 08:12:40 +0100 Subject: [PATCH 015/825] Update title when title-format-pinned is modified --- qutebrowser/mainwindow/tabbedbrowser.py | 1 + qutebrowser/mainwindow/tabwidget.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 6efec0851..a7083b36a 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -120,6 +120,7 @@ class TabbedBrowser(tabwidget.TabWidget): objreg.get('config').changed.connect(self.update_favicons) objreg.get('config').changed.connect(self.update_window_title) objreg.get('config').changed.connect(self.update_tab_titles) + objreg.get('config').changed.connect(self.update_tab_titles_pinned) def __repr__(self): return utils.get_repr(self, count=self.count()) diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index e22c7a75f..3d4151fbd 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -157,6 +157,12 @@ class TabWidget(QTabWidget): for idx in range(self.count()): self.update_tab_title(idx) + @config.change_filter('tabs', 'title-format-pinned') + def update_tab_titles_pinned(self): + """Update all texts.""" + for idx in range(self.count()): + self.update_tab_title(idx) + def tabInserted(self, idx): """Update titles when a tab was inserted.""" super().tabInserted(idx) From 6f8aaccc2b0ba365f620494b22ed9093ddfc91a9 Mon Sep 17 00:00:00 2001 From: thuck Date: Tue, 8 Nov 2016 21:12:20 +0100 Subject: [PATCH 016/825] Attach pin information to tabwidget Simple access to pin information on tab widget. Some change for the fmt_pin to not use fields as cheap trick --- qutebrowser/browser/commands.py | 1 + qutebrowser/mainwindow/tabwidget.py | 26 ++++++++++++++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 76e1075a1..4462e56e2 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -284,6 +284,7 @@ class CommandDispatcher: index = self._count() if index is None else int(index) self.tab_move(index) + self._tabbed_browser.set_tab_pinned(self._current_index(), tab.data.pinned) @cmdutils.register(instance='command-dispatcher', name='open', maxsplit=0, scope='window') diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index 3d4151fbd..b42ff0f3f 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -91,6 +91,17 @@ class TabWidget(QTabWidget): bar.set_tab_data(idx, 'indicator-color', color) bar.update(bar.tabRect(idx)) + def set_tab_pinned(self, idx, pinned): + """Set the tab status as pinned. + + Args: + idx: The tab index. + pinned: Pinned tab state. + """ + bar = self.tabBar() + bar.set_tab_data(idx, 'pinned',pinned) + bar.update(bar.tabRect(idx)) + def set_page_title(self, idx, title): """Set the tab title user data.""" self.tabBar().set_tab_data(idx, 'page-title', title) @@ -102,6 +113,7 @@ class TabWidget(QTabWidget): def update_tab_title(self, idx): """Update the tab text for the given tab.""" + tab = self.widget(idx) fields = self.get_tab_fields(idx) fields['title'] = fields['title'].replace('&', '&&') fields['index'] = idx + 1 @@ -109,8 +121,8 @@ class TabWidget(QTabWidget): fmt = config.get('tabs', 'title-format') fmt_pinned = config.get('tabs', 'title-format-pinned') - if fields['pinned']: - title = fmt_pinned.format(**fields) + if tab.data.pinned: + title = '' if fmt_pinned is None else fmt_pinned.format(**fields) else: title = '' if fmt is None else fmt.format(**fields) @@ -126,7 +138,10 @@ class TabWidget(QTabWidget): fields['title'] = page_title fields['title_sep'] = ' - ' if page_title else '' fields['perc_raw'] = tab.progress() - fields['pinned'] = tab.data.pinned + + #TODO: Move this to a proper place + if tab.data.pinned: + self.set_tab_pinned(idx, tab.data.pinned) if tab.load_status() == usertypes.LoadStatus.loading: fields['perc'] = '[{}%] '.format(tab.progress()) @@ -454,12 +469,11 @@ class TabBar(QTabBar): else: #TODO: relative size and/or configured one try: - tab = objreg.get('tab', scope='tab', - window=self._win_id, tab=index) + pinned = self.tab_data(index, 'pinned') except KeyError: pass else: - if tab.data.pinned: + if pinned: size = QSize(40, height) qtutils.ensure_valid(size) return size From d7f5f61f03ff55fc58dbb18947a91f97b54669ac Mon Sep 17 00:00:00 2001 From: thuck Date: Wed, 9 Nov 2016 23:50:41 +0100 Subject: [PATCH 017/825] Implemented counter for total number of tabs With this counter we can better control the space on the tabbar. --- qutebrowser/browser/commands.py | 4 ++++ qutebrowser/mainwindow/tabwidget.py | 32 +++++++++++++++++------------ qutebrowser/misc/sessions.py | 2 ++ 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 4462e56e2..1ead1e363 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -227,6 +227,10 @@ class CommandDispatcher: tabbar = self._tabbed_browser.tabBar() selection_override = self._get_selection_override(left, right, opposite) + + if tab.data.pinned: + tabbar.pinned -= 1 + if selection_override is None: self._tabbed_browser.close_tab(tab) else: diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index b42ff0f3f..c12b88952 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -99,9 +99,16 @@ class TabWidget(QTabWidget): pinned: Pinned tab state. """ bar = self.tabBar() - bar.set_tab_data(idx, 'pinned',pinned) + bar.set_tab_data(idx, 'pinned', pinned) bar.update(bar.tabRect(idx)) + if pinned: + bar.pinned += 1 + else: + bar.pinned -= 1 + + bar.refresh() + def set_page_title(self, idx, title): """Set the tab title user data.""" self.tabBar().set_tab_data(idx, 'page-title', title) @@ -139,10 +146,6 @@ class TabWidget(QTabWidget): fields['title_sep'] = ' - ' if page_title else '' fields['perc_raw'] = tab.progress() - #TODO: Move this to a proper place - if tab.data.pinned: - self.set_tab_pinned(idx, tab.data.pinned) - if tab.load_status() == usertypes.LoadStatus.loading: fields['perc'] = '[{}%] '.format(tab.progress()) else: @@ -298,6 +301,7 @@ class TabBar(QTabBar): self._auto_hide_timer.timeout.connect(self._tabhide) self.setAutoFillBackground(True) self.set_colors() + self.pinned = 0 config_obj.changed.connect(self.set_colors) QTimer.singleShot(0, self._tabhide) config_obj.changed.connect(self.on_tab_colors_changed) @@ -479,14 +483,16 @@ class TabBar(QTabBar): return size # If we *do* have enough space, tabs should occupy the whole window - # width. - #looks like this generates high cpu usage - #need to register the number of pin tabs in advance - #nb_of_pins = len([None for item in range(self.count()) - # if objreg.get('tab', scope='tab', - # window=self._win_id, tab=item).pin is True]) - #width = (self.width() + 40*nb_of_pins) / self.count() - width = self.width() / self.count() + # width. Also taken in consideration the reduced space necessary for + # the pinned tabs. + #TODO: During shutdown the self.count goes down, but the self.pinned not + #this generates some odd bahavior. + #To avoid this we compare self.count against self.pinned. + if self.pinned > 0 and self.count() > self.pinned: + width = (self.width() - 40*self.pinned) / (self.count() - self.pinned) + else: + width = self.width() / self.count() + ## If width is not divisible by count, add a pixel to some tabs so ## that there is no ugly leftover space. if index < self.width() % self.count(): diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index f6c6d97b2..31335f33b 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -381,6 +381,8 @@ class SessionManager(QObject): self._load_tab(new_tab, tab) if tab.get('active', False): tab_to_focus = i + if new_tab.data.pinned: + tabbed_browser.set_tab_pinned(i, new_tab.data.pinned) if tab_to_focus is not None: tabbed_browser.setCurrentIndex(tab_to_focus) if win.get('active', False): From 9beb097c5398b59b3b0025b664fd2a55bf3cf598 Mon Sep 17 00:00:00 2001 From: thuck Date: Wed, 9 Nov 2016 23:52:56 +0100 Subject: [PATCH 018/825] Corrected some unecessary spaces --- qutebrowser/browser/commands.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 1ead1e363..0e43cbf17 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -220,9 +220,8 @@ class CommandDispatcher: left: Force selecting the tab to the left of the current tab. right: Force selecting the tab to the right of the current tab. opposite: Force selecting the tab in the opposite direction of - what's configured in 'tabs->select-on-remove'. + what's configured in 'tabs->select-on-remove'. count: The tab index to close, or None - """ tabbar = self._tabbed_browser.tabBar() selection_override = self._get_selection_override(left, right, From 19cc721eb111ba80eb592a385ba3a17e05ce7829 Mon Sep 17 00:00:00 2001 From: thuck Date: Fri, 11 Nov 2016 12:05:04 +0100 Subject: [PATCH 019/825] Changed behavior on location of tab being pinned Now when a tab is pinned it goes to the end of all pinned tabs. Before it went to the index 1. --- qutebrowser/browser/commands.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index d9122f869..3230daa46 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -271,6 +271,7 @@ class CommandDispatcher: index: Location where the tab should be pinned/unpinned. count: The tab index to pin or unpin, or None """ + tabbar = self._tabbed_browser.tabBar() tab = self._cntwidget(count) if tab is None: return @@ -278,7 +279,7 @@ class CommandDispatcher: tab.data.pinned = not tab.data.pinned if tab.data.pinned: - index = 1 if index is None else int(index) + index = tabbar.pinned + 1 if index is None else int(index) else: index = self._count() if index is None else int(index) From 25b69fe76ad5ae995dea071f29842b5ee780a494 Mon Sep 17 00:00:00 2001 From: thuck Date: Fri, 11 Nov 2016 13:57:01 +0100 Subject: [PATCH 020/825] Configuration for the size of a pinned tab --- qutebrowser/config/configdata.py | 5 +++++ qutebrowser/mainwindow/tabwidget.py | 15 +++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index a5b21e8e2..cefda8857 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -654,6 +654,11 @@ def data(readonly=False): "The width of the tab bar if it's vertical, in px or as " "percentage of the window."), + ('pinned-width', + SettingValue(typ.Int(minval=10), + '43'), + "The width of the pinned tab if it's horizontal, in px."), + ('indicator-width', SettingValue(typ.Int(minval=0), '3'), "Width of the progress indicator (0 to disable)."), diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index 50ab0af92..484cd8b5d 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -185,6 +185,11 @@ class TabWidget(QTabWidget): for idx in range(self.count()): self.update_tab_title(idx) + @config.change_filter('tabs', 'pinned-width') + def update_tab_pinned_width(self): + """Refresh bar""" + self.tabBar().refresh() + def tabInserted(self, idx): """Update titles when a tab was inserted.""" super().tabInserted(idx) @@ -482,25 +487,27 @@ class TabBar(QTabBar): # get scroll buttons as soon as needed. size = minimum_size else: - #TODO: relative size and/or configured one + tab_width_pinned_conf = config.get('tabs', 'pinned-width') + try: pinned = self.tab_data(index, 'pinned') except KeyError: pass else: if pinned: - size = QSize(40, height) + size = QSize(tab_width_pinned_conf, height) qtutils.ensure_valid(size) return size # If we *do* have enough space, tabs should occupy the whole window # width. Also taken in consideration the reduced space necessary for # the pinned tabs. - #TODO: During shutdown the self.count goes down, but the self.pinned not + #WORKAROUND: During shutdown the self.count goes down, but the self.pinned not #this generates some odd bahavior. #To avoid this we compare self.count against self.pinned. if self.pinned > 0 and self.count() > self.pinned: - width = (self.width() - 40*self.pinned) / (self.count() - self.pinned) + pinned_width = tab_width_pinned_conf * self.pinned + width = (self.width() - pinned_width) / (self.count() - self.pinned) else: width = self.width() / self.count() From 9eb0a85bae3ae7cbc2d199fbce106fdc8f03541b Mon Sep 17 00:00:00 2001 From: thuck Date: Fri, 11 Nov 2016 17:10:46 +0100 Subject: [PATCH 021/825] Some fixes for pyflake, pylint and remove useless function --- qutebrowser/browser/commands.py | 7 +++++-- qutebrowser/mainwindow/tabwidget.py | 16 ++++++---------- tests/unit/mainwindow/test_tabwidget.py | 2 ++ 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 3230daa46..484e9ebeb 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -213,6 +213,7 @@ class CommandDispatcher: def _tab_close(self, tab, prev=False, next_=False, opposite=False): """Helper function for tab_close be able to handle message.async. + Args: tab: Tab select to be closed. prev: Force selecting the tab before the current tab. @@ -240,6 +241,7 @@ class CommandDispatcher: @cmdutils.argument('count', count=True) def tab_close(self, prev=False, next_=False, opposite=False, count=None): """Close the current/[count]th tab. + Args: prev: Force selecting the tab before the current tab. next_: Force selecting the tab after the current tab. @@ -251,7 +253,7 @@ class CommandDispatcher: if tab is None: return close = functools.partial(self._tab_close, tab, prev, - next_, opposite) + next_, opposite) if tab.data.pinned: message.confirm_async(title='Pinned Tab', @@ -284,7 +286,8 @@ class CommandDispatcher: index = self._count() if index is None else int(index) self.tab_move(index) - self._tabbed_browser.set_tab_pinned(self._current_index(), tab.data.pinned) + self._tabbed_browser.set_tab_pinned(self._current_index(), + tab.data.pinned) @cmdutils.register(instance='command-dispatcher', name='open', maxsplit=0, scope='window') diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index 484cd8b5d..f89a0f5c6 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -185,11 +185,6 @@ class TabWidget(QTabWidget): for idx in range(self.count()): self.update_tab_title(idx) - @config.change_filter('tabs', 'pinned-width') - def update_tab_pinned_width(self): - """Refresh bar""" - self.tabBar().refresh() - def tabInserted(self, idx): """Update titles when a tab was inserted.""" super().tabInserted(idx) @@ -500,14 +495,15 @@ class TabBar(QTabBar): return size # If we *do* have enough space, tabs should occupy the whole window - # width. Also taken in consideration the reduced space necessary for - # the pinned tabs. - #WORKAROUND: During shutdown the self.count goes down, but the self.pinned not - #this generates some odd bahavior. + # width. Also taken in consideration the reduced space necessary + #for the pinned tabs. + #WORKAROUND: During shutdown the self.count goes down, + #but the self.pinned not this generates some odd bahavior. #To avoid this we compare self.count against self.pinned. if self.pinned > 0 and self.count() > self.pinned: pinned_width = tab_width_pinned_conf * self.pinned - width = (self.width() - pinned_width) / (self.count() - self.pinned) + no_pinned_width = self.width() - pinned_width + width = no_pinned_width / (self.count() - self.pinned) else: width = self.width() / self.count() diff --git a/tests/unit/mainwindow/test_tabwidget.py b/tests/unit/mainwindow/test_tabwidget.py index b9a95c810..4329f12db 100644 --- a/tests/unit/mainwindow/test_tabwidget.py +++ b/tests/unit/mainwindow/test_tabwidget.py @@ -46,6 +46,8 @@ class TestTabWidget: 'indicator-width': 3, 'indicator-padding': configtypes.PaddingValues(2, 2, 0, 4), 'title-format': '{index}: {title}', + 'title-format-pinned': '{index}', + 'pinned-width': 43, 'title-alignment': Qt.AlignLeft, }, 'colors': { From a8ccfe050d345734cd46e251b69f32599887da7c Mon Sep 17 00:00:00 2001 From: thuck Date: Sun, 13 Nov 2016 08:56:43 +0100 Subject: [PATCH 022/825] Remove unecessary empty line --- qutebrowser/browser/commands.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 484e9ebeb..35ceb7e01 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -279,7 +279,6 @@ class CommandDispatcher: return tab.data.pinned = not tab.data.pinned - if tab.data.pinned: index = tabbar.pinned + 1 if index is None else int(index) else: From 84c41c964b89940a884e515d6b7c4deeeb253292 Mon Sep 17 00:00:00 2001 From: thuck Date: Sun, 13 Nov 2016 09:40:07 +0100 Subject: [PATCH 023/825] First test for tab-pin --- tests/end2end/features/tabs.feature | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature index 1663dd8d7..cede08933 100644 --- a/tests/end2end/features/tabs.feature +++ b/tests/end2end/features/tabs.feature @@ -5,6 +5,18 @@ Feature: Tab management Given I clean up open tabs And I set tabs -> tabs-are-windows to false + # :tab-pin + + Scenario: :tab-pin + When I open data/numbers/1.txt + And I open data/numbers/2.txt in a new tab + And I open data/numbers/3.txt in a new tab + And I run :tab-pin + Then the following tabs should be open: + - data/numbers/3.txt (active) + - data/numbers/1.txt + - data/numbers/2.txt + # :tab-close Scenario: :tab-close From e2a6f97c07227b64b53225a2c3fed640dd126592 Mon Sep 17 00:00:00 2001 From: thuck Date: Wed, 16 Nov 2016 07:48:12 +0100 Subject: [PATCH 024/825] Initial tests --- tests/end2end/features/tabs.feature | 45 +++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature index cede08933..2e8a00790 100644 --- a/tests/end2end/features/tabs.feature +++ b/tests/end2end/features/tabs.feature @@ -5,18 +5,6 @@ Feature: Tab management Given I clean up open tabs And I set tabs -> tabs-are-windows to false - # :tab-pin - - Scenario: :tab-pin - When I open data/numbers/1.txt - And I open data/numbers/2.txt in a new tab - And I open data/numbers/3.txt in a new tab - And I run :tab-pin - Then the following tabs should be open: - - data/numbers/3.txt (active) - - data/numbers/1.txt - - data/numbers/2.txt - # :tab-close Scenario: :tab-close @@ -1009,3 +997,36 @@ Feature: Tab management And I run :tab-close ;; tab-prev Then qutebrowser should quit And no crash should happen + + # :tab-pin + + Scenario: :tab-pin command + When I open data/numbers/1.txt + And I open data/numbers/2.txt in a new tab + And I open data/numbers/3.txt in a new tab + And I run :tab-pin + Then the following tabs should be open: + - data/numbers/3.txt (active) + - data/numbers/1.txt + - data/numbers/2.txt + + Scenario: :tab-pin unpin + When I run :tab-pin + Then the following tabs should be open: + - data/numbers/1.txt + - data/numbers/2.txt + - data/numbers/3.txt (active) + + Scenario: :tab-pin to index 2 + When I run :tab-pin 2 + Then the following tabs should be open: + - data/numbers/1.txt + - data/numbers/3.txt (active) + - data/numbers/2.txt + + Scenario: :tab-pin unpin to index 1 + When I run :tab-pin 1 + Then the following tabs should be open: + - data/numbers/3.txt (active) + - data/numbers/1.txt + - data/numbers/2.txt From e514b0d58eeaf6f7de911ade7da0854c5c4a5769 Mon Sep 17 00:00:00 2001 From: thuck Date: Wed, 16 Nov 2016 08:18:08 +0100 Subject: [PATCH 025/825] Included --force option for tab-close This makes possible to close pinned tabs without any confirmation. --- qutebrowser/browser/commands.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 8dbe4fe86..567e45a9f 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -239,7 +239,8 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('count', count=True) - def tab_close(self, prev=False, next_=False, opposite=False, count=None): + def tab_close(self, prev=False, next_=False, opposite=False, + force=False, count=None): """Close the current/[count]th tab. Args: @@ -247,6 +248,7 @@ class CommandDispatcher: next_: Force selecting the tab after the current tab. opposite: Force selecting the tab in the opposite direction of what's configured in 'tabs->select-on-remove'. + force: Avoid confirmation for pinned tabs. count: The tab index to close, or None """ tab = self._cntwidget(count) @@ -255,7 +257,7 @@ class CommandDispatcher: close = functools.partial(self._tab_close, tab, prev, next_, opposite) - if tab.data.pinned: + if tab.data.pinned and not force: message.confirm_async(title='Pinned Tab', text="Are you sure you want to close a pinned tab?", yes_action=close, default=False) From 41adafdec49262ec24c4d5b8a854d1eccf235500 Mon Sep 17 00:00:00 2001 From: thuck Date: Wed, 16 Nov 2016 08:19:21 +0100 Subject: [PATCH 026/825] Fix initial tests --- tests/end2end/features/conftest.py | 2 +- tests/end2end/features/tabs.feature | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/end2end/features/conftest.py b/tests/end2end/features/conftest.py index 7dd42c2d0..517d77144 100644 --- a/tests/end2end/features/conftest.py +++ b/tests/end2end/features/conftest.py @@ -160,7 +160,7 @@ def clean_open_tabs(quteproc): quteproc.set_setting('tabs', 'last-close', 'blank') quteproc.send_cmd(':window-only') quteproc.send_cmd(':tab-only') - quteproc.send_cmd(':tab-close') + quteproc.send_cmd(':tab-close --force') @bdd.given('pdfjs is available') diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature index 2e8a00790..546eae425 100644 --- a/tests/end2end/features/tabs.feature +++ b/tests/end2end/features/tabs.feature @@ -1011,21 +1011,32 @@ Feature: Tab management - data/numbers/2.txt Scenario: :tab-pin unpin - When I run :tab-pin + When I open data/numbers/1.txt + And I open data/numbers/2.txt in a new tab + And I open data/numbers/3.txt in a new tab + And I run :tab-pin + And I run :tab-pin Then the following tabs should be open: - data/numbers/1.txt - data/numbers/2.txt - data/numbers/3.txt (active) Scenario: :tab-pin to index 2 - When I run :tab-pin 2 + When I open data/numbers/1.txt + And I open data/numbers/2.txt in a new tab + And I open data/numbers/3.txt in a new tab + And I run :tab-pin 2 Then the following tabs should be open: - data/numbers/1.txt - data/numbers/3.txt (active) - data/numbers/2.txt Scenario: :tab-pin unpin to index 1 - When I run :tab-pin 1 + When I open data/numbers/1.txt + And I open data/numbers/2.txt in a new tab + And I open data/numbers/3.txt in a new tab + And I run :tab-pin + And I run :tab-pin 1 Then the following tabs should be open: - data/numbers/3.txt (active) - data/numbers/1.txt From 69c82f85632ab13c4c3e1675583874944d3f7286 Mon Sep 17 00:00:00 2001 From: thuck Date: Fri, 18 Nov 2016 07:42:48 +0100 Subject: [PATCH 027/825] Including tests for pinned tab prompt Duplicate function for "I wait for a prompt" --- tests/end2end/features/tabs.feature | 23 +++++++++++++++++++++++ tests/end2end/features/test_tabs_bdd.py | 4 ++++ 2 files changed, 27 insertions(+) diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature index 546eae425..c4c1d56e4 100644 --- a/tests/end2end/features/tabs.feature +++ b/tests/end2end/features/tabs.feature @@ -1041,3 +1041,26 @@ Feature: Tab management - data/numbers/3.txt (active) - data/numbers/1.txt - data/numbers/2.txt + + Scenario: :tab-pin prompt yes + When I open data/numbers/1.txt + And I run :tab-pin + And I open data/numbers/2.txt in a new tab + And I run :tab-pin + And I run :tab-close + And I wait for a prompt + And I run :prompt-accept yes + Then the following tabs should be open: + - data/numbers/1.txt (active) + + Scenario: :tab-pin prompt no + When I open data/numbers/1.txt + And I run :tab-pin + And I open data/numbers/2.txt in a new tab + And I run :tab-pin + And I run :tab-close + And I wait for a prompt + And I run :prompt-accept no + Then the following tabs should be open: + - data/numbers/1.txt + - data/numbers/2.txt (active) diff --git a/tests/end2end/features/test_tabs_bdd.py b/tests/end2end/features/test_tabs_bdd.py index bcae6d60d..a61dd4fcd 100644 --- a/tests/end2end/features/test_tabs_bdd.py +++ b/tests/end2end/features/test_tabs_bdd.py @@ -19,3 +19,7 @@ import pytest_bdd as bdd bdd.scenarios('tabs.feature') + +@bdd.when("I wait for a prompt") +def wait_for_prompt(quteproc): + quteproc.wait_for(message='Asking question *') From 175744761b7bdf66f797912167dc62da88954286 Mon Sep 17 00:00:00 2001 From: thuck Date: Tue, 22 Nov 2016 06:57:00 +0100 Subject: [PATCH 028/825] flake8 fixes --- qutebrowser/browser/commands.py | 2 +- tests/end2end/features/test_tabs_bdd.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 567e45a9f..8f71fe75a 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -240,7 +240,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('count', count=True) def tab_close(self, prev=False, next_=False, opposite=False, - force=False, count=None): + force=False, count=None): """Close the current/[count]th tab. Args: diff --git a/tests/end2end/features/test_tabs_bdd.py b/tests/end2end/features/test_tabs_bdd.py index a61dd4fcd..d53241288 100644 --- a/tests/end2end/features/test_tabs_bdd.py +++ b/tests/end2end/features/test_tabs_bdd.py @@ -20,6 +20,7 @@ import pytest_bdd as bdd bdd.scenarios('tabs.feature') + @bdd.when("I wait for a prompt") def wait_for_prompt(quteproc): quteproc.wait_for(message='Asking question *') From 982e4f46e0d0dbd2887b203b3ecf2a402f42a0f5 Mon Sep 17 00:00:00 2001 From: thuck Date: Tue, 22 Nov 2016 07:24:02 +0100 Subject: [PATCH 029/825] Test for accidental url opened in a pinned tab --- tests/end2end/features/tabs.feature | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature index c4c1d56e4..a2e903001 100644 --- a/tests/end2end/features/tabs.feature +++ b/tests/end2end/features/tabs.feature @@ -1064,3 +1064,11 @@ Feature: Tab management Then the following tabs should be open: - data/numbers/1.txt - data/numbers/2.txt (active) + + Scenario: :tab-pin open url + When I open data/numbers/1.txt + And I run :tab-pin + And I run :open data/numbers/2.txt + Then the message "Tab is pinned!" should be shown + And the following tabs should be open: + - data/numbers/1.txt (active) From 92e1181680eeca0f4d6b73b541b1cd64e0a93ec6 Mon Sep 17 00:00:00 2001 From: thuck Date: Wed, 16 Nov 2016 08:18:08 +0100 Subject: [PATCH 030/825] Included --force option for tab-close This makes possible to close pinned tabs without any confirmation. --- qutebrowser/browser/commands.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 695ec1eff..0eb84c37b 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -239,7 +239,8 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('count', count=True) - def tab_close(self, prev=False, next_=False, opposite=False, count=None): + def tab_close(self, prev=False, next_=False, opposite=False, + force=False, count=None): """Close the current/[count]th tab. Args: @@ -247,6 +248,7 @@ class CommandDispatcher: next_: Force selecting the tab after the current tab. opposite: Force selecting the tab in the opposite direction of what's configured in 'tabs->select-on-remove'. + force: Avoid confirmation for pinned tabs. count: The tab index to close, or None """ tab = self._cntwidget(count) @@ -255,7 +257,7 @@ class CommandDispatcher: close = functools.partial(self._tab_close, tab, prev, next_, opposite) - if tab.data.pinned: + if tab.data.pinned and not force: message.confirm_async(title='Pinned Tab', text="Are you sure you want to close a pinned tab?", yes_action=close, default=False) From 9547938f7906b536f911f5e6cbbd7d98f17a4ffc Mon Sep 17 00:00:00 2001 From: thuck Date: Wed, 16 Nov 2016 08:19:21 +0100 Subject: [PATCH 031/825] Fix initial tests --- tests/end2end/features/conftest.py | 2 +- tests/end2end/features/tabs.feature | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/end2end/features/conftest.py b/tests/end2end/features/conftest.py index 7dd42c2d0..517d77144 100644 --- a/tests/end2end/features/conftest.py +++ b/tests/end2end/features/conftest.py @@ -160,7 +160,7 @@ def clean_open_tabs(quteproc): quteproc.set_setting('tabs', 'last-close', 'blank') quteproc.send_cmd(':window-only') quteproc.send_cmd(':tab-only') - quteproc.send_cmd(':tab-close') + quteproc.send_cmd(':tab-close --force') @bdd.given('pdfjs is available') diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature index 2e8a00790..546eae425 100644 --- a/tests/end2end/features/tabs.feature +++ b/tests/end2end/features/tabs.feature @@ -1011,21 +1011,32 @@ Feature: Tab management - data/numbers/2.txt Scenario: :tab-pin unpin - When I run :tab-pin + When I open data/numbers/1.txt + And I open data/numbers/2.txt in a new tab + And I open data/numbers/3.txt in a new tab + And I run :tab-pin + And I run :tab-pin Then the following tabs should be open: - data/numbers/1.txt - data/numbers/2.txt - data/numbers/3.txt (active) Scenario: :tab-pin to index 2 - When I run :tab-pin 2 + When I open data/numbers/1.txt + And I open data/numbers/2.txt in a new tab + And I open data/numbers/3.txt in a new tab + And I run :tab-pin 2 Then the following tabs should be open: - data/numbers/1.txt - data/numbers/3.txt (active) - data/numbers/2.txt Scenario: :tab-pin unpin to index 1 - When I run :tab-pin 1 + When I open data/numbers/1.txt + And I open data/numbers/2.txt in a new tab + And I open data/numbers/3.txt in a new tab + And I run :tab-pin + And I run :tab-pin 1 Then the following tabs should be open: - data/numbers/3.txt (active) - data/numbers/1.txt From be980a7268d5fb54173600e7addc8bed27ae24be Mon Sep 17 00:00:00 2001 From: thuck Date: Fri, 18 Nov 2016 07:42:48 +0100 Subject: [PATCH 032/825] Including tests for pinned tab prompt Duplicate function for "I wait for a prompt" --- tests/end2end/features/tabs.feature | 23 +++++++++++++++++++++++ tests/end2end/features/test_tabs_bdd.py | 4 ++++ 2 files changed, 27 insertions(+) diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature index 546eae425..c4c1d56e4 100644 --- a/tests/end2end/features/tabs.feature +++ b/tests/end2end/features/tabs.feature @@ -1041,3 +1041,26 @@ Feature: Tab management - data/numbers/3.txt (active) - data/numbers/1.txt - data/numbers/2.txt + + Scenario: :tab-pin prompt yes + When I open data/numbers/1.txt + And I run :tab-pin + And I open data/numbers/2.txt in a new tab + And I run :tab-pin + And I run :tab-close + And I wait for a prompt + And I run :prompt-accept yes + Then the following tabs should be open: + - data/numbers/1.txt (active) + + Scenario: :tab-pin prompt no + When I open data/numbers/1.txt + And I run :tab-pin + And I open data/numbers/2.txt in a new tab + And I run :tab-pin + And I run :tab-close + And I wait for a prompt + And I run :prompt-accept no + Then the following tabs should be open: + - data/numbers/1.txt + - data/numbers/2.txt (active) diff --git a/tests/end2end/features/test_tabs_bdd.py b/tests/end2end/features/test_tabs_bdd.py index bcae6d60d..a61dd4fcd 100644 --- a/tests/end2end/features/test_tabs_bdd.py +++ b/tests/end2end/features/test_tabs_bdd.py @@ -19,3 +19,7 @@ import pytest_bdd as bdd bdd.scenarios('tabs.feature') + +@bdd.when("I wait for a prompt") +def wait_for_prompt(quteproc): + quteproc.wait_for(message='Asking question *') From 9dff4299e89c4edba2d7f2909c7c05658df57737 Mon Sep 17 00:00:00 2001 From: thuck Date: Tue, 22 Nov 2016 06:57:00 +0100 Subject: [PATCH 033/825] flake8 fixes --- qutebrowser/browser/commands.py | 2 +- tests/end2end/features/test_tabs_bdd.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 0eb84c37b..ef3d0919e 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -240,7 +240,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('count', count=True) def tab_close(self, prev=False, next_=False, opposite=False, - force=False, count=None): + force=False, count=None): """Close the current/[count]th tab. Args: diff --git a/tests/end2end/features/test_tabs_bdd.py b/tests/end2end/features/test_tabs_bdd.py index a61dd4fcd..d53241288 100644 --- a/tests/end2end/features/test_tabs_bdd.py +++ b/tests/end2end/features/test_tabs_bdd.py @@ -20,6 +20,7 @@ import pytest_bdd as bdd bdd.scenarios('tabs.feature') + @bdd.when("I wait for a prompt") def wait_for_prompt(quteproc): quteproc.wait_for(message='Asking question *') From 05d36317501ec440cafce6c32ce8160b2e885b4e Mon Sep 17 00:00:00 2001 From: thuck Date: Tue, 22 Nov 2016 07:24:02 +0100 Subject: [PATCH 034/825] Test for accidental url opened in a pinned tab --- tests/end2end/features/tabs.feature | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature index c4c1d56e4..a2e903001 100644 --- a/tests/end2end/features/tabs.feature +++ b/tests/end2end/features/tabs.feature @@ -1064,3 +1064,11 @@ Feature: Tab management Then the following tabs should be open: - data/numbers/1.txt - data/numbers/2.txt (active) + + Scenario: :tab-pin open url + When I open data/numbers/1.txt + And I run :tab-pin + And I run :open data/numbers/2.txt + Then the message "Tab is pinned!" should be shown + And the following tabs should be open: + - data/numbers/1.txt (active) From e9c79e9be3549e7547ea9130fa0ad5ae7818169a Mon Sep 17 00:00:00 2001 From: thuck Date: Wed, 23 Nov 2016 08:18:10 +0100 Subject: [PATCH 035/825] Fix for comments on configdata --- qutebrowser/config/configdata.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 9b60b7402..53378daea 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -657,7 +657,7 @@ def data(readonly=False): ('pinned-width', SettingValue(typ.Int(minval=10), '43'), - "The width of the pinned tab if it's horizontal, in px."), + "The width for pinned tabs with a horizontal tabbar, in px."), ('indicator-width', SettingValue(typ.Int(minval=0), '3'), @@ -690,17 +690,8 @@ def data(readonly=False): fields=['perc', 'perc_raw', 'title', 'title_sep', 'index', 'id', 'scroll_pos', 'host'], none_ok=True), '{index}'), - "The format to use for the pinned tab title." - " The following placeholders are defined:\n\n" - "* `{perc}`: The percentage as a string like `[10%]`.\n" - "* `{perc_raw}`: The raw percentage, e.g. `10`\n" - "* `{title}`: The title of the current web page\n" - "* `{title_sep}`: The string ` - ` if a title is set, empty " - "otherwise.\n" - "* `{index}`: The index of this tab.\n" - "* `{id}`: The internal tab ID of this tab.\n" - "* `{scroll_pos}`: The page scroll position.\n" - "* `{host}`: The host of the current web page."), + "The format to use for the tab title for pinned tabs." + "The same placeholders like for title-format are defined."), ('title-alignment', SettingValue(typ.TextAlignment(), 'left'), From 8d4b55bb802d286c70f50d40dbb443b55952a02a Mon Sep 17 00:00:00 2001 From: thuck Date: Wed, 23 Nov 2016 22:18:55 +0100 Subject: [PATCH 036/825] Fix comments and change self.pinned to self.pinned_count --- qutebrowser/browser/commands.py | 2 +- qutebrowser/mainwindow/tabwidget.py | 38 ++++++++++++++--------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index ef3d0919e..e258ce135 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -282,7 +282,7 @@ class CommandDispatcher: tab.data.pinned = not tab.data.pinned if tab.data.pinned: - index = tabbar.pinned + 1 if index is None else int(index) + index = tabbar.pinned_count + 1 if index is None else int(index) else: index = self._count() if index is None else int(index) diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index 187bfb956..b65f2c032 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -103,9 +103,9 @@ class TabWidget(QTabWidget): bar.update(bar.tabRect(idx)) if pinned: - bar.pinned += 1 + bar.pinned_count += 1 else: - bar.pinned -= 1 + bar.pinned_count -= 1 bar.refresh() @@ -306,7 +306,7 @@ class TabBar(QTabBar): self._auto_hide_timer.timeout.connect(self._tabhide) self.setAutoFillBackground(True) self.set_colors() - self.pinned = 0 + self.pinned_count = 0 config_obj.changed.connect(self.set_colors) QTimer.singleShot(0, self._tabhide) config_obj.changed.connect(self.on_tab_colors_changed) @@ -488,28 +488,28 @@ class TabBar(QTabBar): try: pinned = self.tab_data(index, 'pinned') except KeyError: - pass - else: - if pinned: - size = QSize(tab_width_pinned_conf, height) - qtutils.ensure_valid(size) - return size + pinned = False + + if pinned: + size = QSize(tab_width_pinned_conf, height) + qtutils.ensure_valid(size) + return size # If we *do* have enough space, tabs should occupy the whole window - # width. Also taken in consideration the reduced space necessary - #for the pinned tabs. - #WORKAROUND: During shutdown the self.count goes down, - #but the self.pinned not this generates some odd bahavior. - #To avoid this we compare self.count against self.pinned. - if self.pinned > 0 and self.count() > self.pinned: - pinned_width = tab_width_pinned_conf * self.pinned + # width. If there is pinned tabs their size will be substracted from + # the total window width. + # During shutdown the self.count goes down, + # but the self.pinned_count not - this generates some odd behavior. + # To avoid this we compare self.count against self.pinned_count. + if self.pinned_count > 0 and self.count() > self.pinned_count: + pinned_width = tab_width_pinned_conf * self.pinned_count no_pinned_width = self.width() - pinned_width - width = no_pinned_width / (self.count() - self.pinned) + width = no_pinned_width / (self.count() - self.pinned_count) else: width = self.width() / self.count() - ## If width is not divisible by count, add a pixel to some tabs so - ## that there is no ugly leftover space. + # If width is not divisible by count, add a pixel to some tabs so + # that there is no ugly leftover space. if index < self.width() % self.count(): width += 1 size = QSize(width, height) From a25409755886f5e478312cd7898e02375a733534 Mon Sep 17 00:00:00 2001 From: thuck Date: Thu, 24 Nov 2016 00:05:17 +0100 Subject: [PATCH 037/825] Using log instead of prompt functions for test --- tests/end2end/features/tabs.feature | 4 ++-- tests/end2end/features/test_tabs_bdd.py | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature index a2e903001..5a7967b3a 100644 --- a/tests/end2end/features/tabs.feature +++ b/tests/end2end/features/tabs.feature @@ -1048,7 +1048,7 @@ Feature: Tab management And I open data/numbers/2.txt in a new tab And I run :tab-pin And I run :tab-close - And I wait for a prompt + And I wait for "*want to close a pinned tab*" in the log And I run :prompt-accept yes Then the following tabs should be open: - data/numbers/1.txt (active) @@ -1059,7 +1059,7 @@ Feature: Tab management And I open data/numbers/2.txt in a new tab And I run :tab-pin And I run :tab-close - And I wait for a prompt + And I wait for "*want to close a pinned tab*" in the log And I run :prompt-accept no Then the following tabs should be open: - data/numbers/1.txt diff --git a/tests/end2end/features/test_tabs_bdd.py b/tests/end2end/features/test_tabs_bdd.py index d53241288..bcae6d60d 100644 --- a/tests/end2end/features/test_tabs_bdd.py +++ b/tests/end2end/features/test_tabs_bdd.py @@ -19,8 +19,3 @@ import pytest_bdd as bdd bdd.scenarios('tabs.feature') - - -@bdd.when("I wait for a prompt") -def wait_for_prompt(quteproc): - quteproc.wait_for(message='Asking question *') From 38ca583084e89de0da35cb1b626d21b051ea2c70 Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Tue, 8 Nov 2016 12:51:51 +0100 Subject: [PATCH 038/825] new default keybinds --- qutebrowser/config/configdata.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index da0fea5e3..263bd4e26 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1637,14 +1637,14 @@ KEY_DATA = collections.OrderedDict([ ('download-clear', ['cd']), ('view-source', ['gf']), ('set-cmd-text -s :buffer', ['gt']), - ('tab-focus last', ['']), + ('tab-focus last', ['', '', '']), ('enter-mode passthrough', ['']), ('quit', ['']), ('scroll-page 0 1', ['']), ('scroll-page 0 -1', ['']), ('scroll-page 0 0.5', ['']), ('scroll-page 0 -0.5', ['']), - ('tab-focus 1', ['']), + ('tab-focus 1', ['', 'g0', 'g^']), ('tab-focus 2', ['']), ('tab-focus 3', ['']), ('tab-focus 4', ['']), @@ -1652,7 +1652,7 @@ KEY_DATA = collections.OrderedDict([ ('tab-focus 6', ['']), ('tab-focus 7', ['']), ('tab-focus 8', ['']), - ('tab-focus 9', ['']), + ('tab-focus -1', ['', 'g$']), ('home', ['']), ('stop', ['']), ('print', ['']), From a1e1e90ec916bcff0bf4fd5500fe80408f860f8b Mon Sep 17 00:00:00 2001 From: MikeinRealLife Date: Sat, 18 Mar 2017 20:05:21 -0700 Subject: [PATCH 039/825] Issue #2292 Added a list of chrome urls that work. A couple of with (?) next to them did nothing and didn't hang or kill the browser, so I removed them. They only work in OSX, not windows (opened separate issue for this). --- CONTRIBUTING.asciidoc | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CONTRIBUTING.asciidoc b/CONTRIBUTING.asciidoc index 12d52fee7..350637b3b 100644 --- a/CONTRIBUTING.asciidoc +++ b/CONTRIBUTING.asciidoc @@ -546,6 +546,25 @@ Rebuilding the website If you want to rebuild the website, run `./scripts/asciidoc2html.py --website `. +Chrome URLs +~~~~~~~~~~~ + +Qutebrowser supports several chrome urls. A short list of them can be found here: + +chrome://appcache-internals/ +chrome://blob-internals/ +chrome://gpu/ +chrome://histograms/ +chrome://indexeddb-internals/ +chrome://media-internals/ +chrome://network-errors/ +chrome://serviceworker-internals/ +chrome://webrtc-internals/ +chrome://crash/ +chrome://kill/ +chrome://gpucrash/ +chrome://gpuhang/ + Style conventions ----------------- From 2eb07fc9cc363f2195a20af8261f4cd9a8ba55e7 Mon Sep 17 00:00:00 2001 From: thuck Date: Sun, 19 Mar 2017 14:03:24 +0100 Subject: [PATCH 040/825] Fix line size --- qutebrowser/mainwindow/tabwidget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index b65f2c032..29daca354 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -496,8 +496,8 @@ class TabBar(QTabBar): return size # If we *do* have enough space, tabs should occupy the whole window - # width. If there is pinned tabs their size will be substracted from - # the total window width. + # width. If there is pinned tabs their size will be substracted + # from the total window width. # During shutdown the self.count goes down, # but the self.pinned_count not - this generates some odd behavior. # To avoid this we compare self.count against self.pinned_count. From 02bf0401ab4c8247fa86935e8661a5e784f46a3e Mon Sep 17 00:00:00 2001 From: thuck Date: Sun, 19 Mar 2017 14:05:21 +0100 Subject: [PATCH 041/825] Last pinned to pinned_count --- qutebrowser/browser/commands.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index e258ce135..b0c8dc9e9 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -226,9 +226,6 @@ class CommandDispatcher: selection_override = self._get_selection_override(prev, next_, opposite) - if tab.data.pinned: - tabbar.pinned -= 1 - if selection_override is None: self._tabbed_browser.close_tab(tab) else: @@ -237,6 +234,9 @@ class CommandDispatcher: self._tabbed_browser.close_tab(tab) tabbar.setSelectionBehaviorOnRemove(old_selection_behavior) + if tab.data.pinned: + tabbar.pinned_count -= 1 + @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('count', count=True) def tab_close(self, prev=False, next_=False, opposite=False, From 4a4c7b96d1ede93541a4314a6ea1f944a48ac81a Mon Sep 17 00:00:00 2001 From: Imran Sobir Date: Tue, 28 Mar 2017 18:34:47 +0500 Subject: [PATCH 042/825] Add nojs history page. --- qutebrowser/browser/qutescheme.py | 91 ++++++++++++++++++++------ qutebrowser/html/history_nojs.html | 58 ++++++++++++++++ tests/end2end/features/history.feature | 2 +- 3 files changed, 129 insertions(+), 22 deletions(-) create mode 100644 qutebrowser/html/history_nojs.html diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index 45ce71c27..80a808773 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -24,6 +24,7 @@ Module attributes: _HANDLERS: The handlers registered via decorators. """ +from datetime import date, datetime, timedelta import json import os import sys @@ -35,7 +36,7 @@ from PyQt5.QtCore import QUrlQuery import qutebrowser from qutebrowser.config import config from qutebrowser.utils import (version, utils, jinja, log, message, docutils, - objreg) + objreg, usertypes) from qutebrowser.misc import objects @@ -165,26 +166,29 @@ def qute_bookmarks(_url): return 'text/html', html -@add_handler('history') # noqa -def qute_history(url): - """Handler for qute:history. Display and serve history.""" +def history_data(start_time): # noqa + """Return history data + + Arguments: + start_time -- select history starting from this timestamp. + """ def history_iter(start_time, reverse=False): """Iterate through the history and get items we're interested. Arguments: reverse -- whether to reverse the history_dict before iterating. - start_time -- select history starting from this timestamp. """ history = objreg.get('web-history').history_dict.values() if reverse: history = reversed(history) - end_time = start_time - 24*60*60 # end is 24hrs earlier than start - # when history_dict is not reversed, we need to keep track of last item # so that we can yield its atime last_item = None + # end is 24hrs earlier than start + end_time = start_time - 24*60*60 + for item in history: # Skip redirects # Skip qute:// links @@ -226,6 +230,23 @@ def qute_history(url): # if we reached here, we had reached the end of history yield {"next": int(last_item.atime if last_item else -1)} + if sys.hexversion >= 0x03050000: + # On Python >= 3.5 we can reverse the ordereddict in-place and thus + # apply an additional performance improvement in history_iter. + # On my machine, this gets us down from 550ms to 72us with 500k old + # items. + history = history_iter(start_time, reverse=True) + else: + # On Python 3.4, we can't do that, so we'd need to copy the entire + # history to a list. There, filter first and then reverse it here. + history = reversed(list(history_iter(start_time, reverse=False))) + + return list(history) + + +@add_handler('history') +def qute_history(url): + """Handler for qute:history. Display and serve history.""" if url.path() == '/data': # Use start_time in query or current time. try: @@ -234,21 +255,49 @@ def qute_history(url): except ValueError as e: raise QuteSchemeError("Query parameter start_time is invalid", e) - if sys.hexversion >= 0x03050000: - # On Python >= 3.5 we can reverse the ordereddict in-place and thus - # apply an additional performance improvement in history_iter. - # On my machine, this gets us down from 550ms to 72us with 500k old - # items. - history = history_iter(start_time, reverse=True) - else: - # On Python 3.4, we can't do that, so we'd need to copy the entire - # history to a list. There, filter first and then reverse it here. - history = reversed(list(history_iter(start_time, reverse=False))) - - return 'text/html', json.dumps(list(history)) + return 'text/html', json.dumps(history_data(start_time)) else: - return 'text/html', jinja.render('history.html', title='History', - session_interval=config.get('ui', 'history-session-interval')) + if ( + config.get('content', 'allow-javascript') and + objects.backend == usertypes.Backend.QtWebEngine + ): + return 'text/html', jinja.render( + 'history.html', + title='History', + session_interval=config.get('ui', 'history-session-interval') + ) + else: + # Get current date from query parameter, if not given choose today. + curr_date = date.today() + try: + query_date = QUrlQuery(url).queryItemValue("date") + if query_date: + curr_date = datetime.strptime(query_date, + "%Y-%m-%d").date() + except ValueError: + log.misc.debug("Invalid date passed to qute:history: " + + query_date) + + one_day = timedelta(days=1) + next_date = curr_date + one_day + prev_date = curr_date - one_day + + # start_time is the last second of curr_date + start_time = time.mktime(next_date.timetuple()) - 1 + history = [ + (i["url"], i["title"], datetime.fromtimestamp(i["time"]/1000)) + for i in history_data(start_time) if "next" not in i + ] + + return 'text/html', jinja.render( + 'history_nojs.html', + title='History', + history=history, + curr_date=curr_date, + next_date=next_date, + prev_date=prev_date, + today=date.today(), + ) @add_handler('javascript') diff --git a/qutebrowser/html/history_nojs.html b/qutebrowser/html/history_nojs.html new file mode 100644 index 000000000..3fed67797 --- /dev/null +++ b/qutebrowser/html/history_nojs.html @@ -0,0 +1,58 @@ +{% extends "styled.html" %} + +{% block style %} +{{super()}} +body { + max-width: 1440px; +} + +td.title { + word-break: break-all; +} + +td.time { + color: #555; + text-align: right; + white-space: nowrap; +} + +table { + margin-bottom: 30px; +} + +.date { + color: #555; + font-size: 12pt; + padding-bottom: 15px; + font-weight: bold; + text-align: left; +} + +.pagination-link { + color: #555; + font-weight: bold; + margn-bottom: 15px; + text-decoration: none; +} +{% endblock %} +{% block content %} + +

Browsing history

+ + + + + {% for url, title, time in history %} + + + + + {% endfor %} + +
{{curr_date.strftime("%a, %d %B %Y")}}
{{title}}{{time.strftime("%X")}}
+ + +{% if today >= next_date %} + +{% endif %} +{% endblock %} diff --git a/tests/end2end/features/history.feature b/tests/end2end/features/history.feature index 2df4ada21..7caed43b2 100644 --- a/tests/end2end/features/history.feature +++ b/tests/end2end/features/history.feature @@ -77,7 +77,7 @@ Feature: Page history Scenario: Listing history When I open data/numbers/3.txt And I open data/numbers/4.txt - And I open qute://history/data + And I open qute://history Then the page should contain the plaintext "3.txt" Then the page should contain the plaintext "4.txt" From 3aaebe83fb05d6f117aabd0b94aa4683c115d256 Mon Sep 17 00:00:00 2001 From: Imran Sobir Date: Wed, 29 Mar 2017 14:22:58 +0500 Subject: [PATCH 043/825] Remove noscript message from history.html. --- qutebrowser/html/history.html | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/qutebrowser/html/history.html b/qutebrowser/html/history.html index 58e467135..35ac419be 100644 --- a/qutebrowser/html/history.html +++ b/qutebrowser/html/history.html @@ -47,22 +47,9 @@ table { text-align: center; } -.error { - background-color: #ffbbbb; - border-radius: 5px; - font-weight: bold; - padding: 10px; - text-align: center; - width: 100%; - border: 1px solid #ff7777; -} - {% endblock %} {% block content %}

Browsing history

-
From 5efca155948bc467e4fb7b19dafd98d47e33745b Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Thu, 30 Mar 2017 17:45:51 +0100 Subject: [PATCH 044/825] Put option comments right above the option value Problem: I like to edit `~/.config/qutebrowser/qutebrowser.conf` manually with Vim. This works great, except that the current format is a bit of a pain to deal with: [section-name] # section description # # [ Description of all the options] actual options So if I want to know the description or what the default value is, I need to scroll up and back down. Solution: change the order of the comments to: # section description [section-name] # Option description option = value # Option description two optiontwo = value # Hello, world! [section-two] ... Which is much more convenient (and also what almost any other program does). (This patch changes much less code than it looks in the diff; I just de-looped and moved `_str_option_desc` below `_str_items` as that makes more sense since it gets called by `_str_items`). --- qutebrowser/config/config.py | 78 ++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 40 deletions(-) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 05d71605f..83e0e9c8f 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -471,10 +471,9 @@ class ConfigManager(QObject): """Get the whole config as a string.""" lines = configdata.FIRST_COMMENT.strip('\n').splitlines() for sectname, sect in self.sections.items(): - lines.append('\n[{}]'.format(sectname)) - lines += self._str_section_desc(sectname) - lines += self._str_option_desc(sectname, sect) - lines += self._str_items(sect) + lines += ['\n'] + self._str_section_desc(sectname) + lines.append('[{}]'.format(sectname)) + lines += self._str_items(sectname, sect) return '\n'.join(lines) + '\n' def _str_section_desc(self, sectname): @@ -489,42 +488,7 @@ class ConfigManager(QObject): lines += wrapper.wrap(secline) return lines - def _str_option_desc(self, sectname, sect): - """Get the option description strings for sect/sectname.""" - wrapper = textwrapper.TextWrapper(initial_indent='#' + ' ' * 5, - subsequent_indent='#' + ' ' * 5) - lines = [] - if not getattr(sect, 'descriptions', None): - return lines - - for optname, option in sect.items(): - - lines.append('#') - typestr = ' ({})'.format(option.typ.get_name()) - lines.append("# {}{}:".format(optname, typestr)) - - try: - desc = self.sections[sectname].descriptions[optname] - except KeyError: - log.config.exception("No description for {}.{}!".format( - sectname, optname)) - continue - for descline in desc.splitlines(): - lines += wrapper.wrap(descline) - valid_values = option.typ.get_valid_values() - if valid_values is not None: - if valid_values.descriptions: - for val in valid_values: - desc = valid_values.descriptions[val] - lines += wrapper.wrap(" {}: {}".format(val, desc)) - else: - lines += wrapper.wrap("Valid values: {}".format(', '.join( - valid_values))) - lines += wrapper.wrap("Default: {}".format( - option.values['default'])) - return lines - - def _str_items(self, sect): + def _str_items(self, sectname, sect): """Get the option items as string for sect.""" lines = [] for optname, option in sect.items(): @@ -535,9 +499,43 @@ class ConfigManager(QObject): # configparser can't handle = in keys :( optname = optname.replace('=', '') keyval = '{} = {}'.format(optname, value) + lines += self._str_option_desc(sectname, sect, optname, option) lines.append(keyval) return lines + def _str_option_desc(self, sectname, sect, optname, option): + """Get the option description strings for a single option.""" + wrapper = textwrapper.TextWrapper(initial_indent='#' + ' ' * 5, + subsequent_indent='#' + ' ' * 5) + lines = [] + if not getattr(sect, 'descriptions', None): + return lines + + lines.append('') + typestr = ' ({})'.format(option.typ.get_name()) + lines.append("# {}{}:".format(optname, typestr)) + + try: + desc = self.sections[sectname].descriptions[optname] + except KeyError: + log.config.exception("No description for {}.{}!".format( + sectname, optname)) + return [] + for descline in desc.splitlines(): + lines += wrapper.wrap(descline) + valid_values = option.typ.get_valid_values() + if valid_values is not None: + if valid_values.descriptions: + for val in valid_values: + desc = valid_values.descriptions[val] + lines += wrapper.wrap(" {}: {}".format(val, desc)) + else: + lines += wrapper.wrap("Valid values: {}".format(', '.join( + valid_values))) + lines += wrapper.wrap("Default: {}".format( + option.values['default'])) + return lines + def _get_real_sectname(self, cp, sectname): """Get an old or new section name based on a configparser. From 7f13c9a3c31aa719144ca3afcad7af305dd2f6ed Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Thu, 30 Mar 2017 18:37:15 +0100 Subject: [PATCH 045/825] Relax commandline parsing a bit Problem 1: Entering a command of `:::save` gives an error. Problem 2: Entering a command of `:save\n` gives an error. Both scenarios may seem a bit silly at first, but I encountered both by copy/pasting a command: 1. Enter `:` in qutebrowser. 2. Copy a full line from a terminal starting with `:`. 3. You will now have both of the above problems. Solution: Trim all whitespace and `:` of a command. This is also what Vim does, by the way. --- qutebrowser/commands/runners.py | 3 ++- tests/end2end/features/misc.feature | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index f6a8e07e2..0a2d6085f 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -133,7 +133,8 @@ class CommandRunner(QObject): Yields: ParseResult tuples. """ - if not text.strip(): + text = text.strip().lstrip(':').strip() + if not text: raise cmdexc.NoSuchCommandError("No command given") if aliases: diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index cff7d7854..0aeb65f92 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -538,6 +538,16 @@ Feature: Various utility commands. When I run :message-i "Hello World" (invalid command) Then the error "message-i: no such command" should be shown + Scenario: Multiple leading : in command + When I run :::::set-cmd-text ::::message-i "Hello World" + And I run :command-accept + Then the message "Hello World" should be shown + + Scenario: Whitespace in command + When I run : : set-cmd-text : : message-i "Hello World" + And I run :command-accept + Then the message "Hello World" should be shown + # We can't run :message-i as startup command, so we use # :set-cmd-text @@ -632,7 +642,7 @@ Feature: Various utility commands. And I run :command-history-prev And I run :command-accept Then the message "blah" should be shown - + Scenario: Browsing through commands When I run :set-cmd-text :message-info blarg And I run :command-accept @@ -644,7 +654,7 @@ Feature: Various utility commands. And I run :command-history-next And I run :command-accept Then the message "blarg" should be shown - + Scenario: Calling previous command when history is empty Given I have a fresh instance When I run :set-cmd-text : From 522e105aaf4c326d27e087222c0ba76680d1bed3 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Fri, 31 Mar 2017 19:49:32 +0200 Subject: [PATCH 046/825] Update flask from 0.12 to 0.12.1 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index e1123a1d7..0187026ce 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -6,7 +6,7 @@ click==6.7 coverage==4.3.4 decorator==4.0.11 EasyProcess==0.2.3 -Flask==0.12 +Flask==0.12.1 glob2==0.5 httpbin==0.5.0 hypothesis==3.7.0 From 1a337f6a77cf9e284abcf14813b6c70241e0045c Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Fri, 31 Mar 2017 18:51:04 +0100 Subject: [PATCH 047/825] Document how to do spell checking in the FAQ --- FAQ.asciidoc | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/FAQ.asciidoc b/FAQ.asciidoc index 71d5f2834..c934d7e64 100644 --- a/FAQ.asciidoc +++ b/FAQ.asciidoc @@ -124,6 +124,34 @@ When using quickmark, you can give them all names, like `:open foodrecipes`, you will see a list of all the food recipe sites, without having to remember the exact website title or address. +How do I use spell checking?:: + Qutebrowser's support for spell checking is somewhat limited at the moment + (see https://github.com/qutebrowser/qutebrowser/issues/700[#700]), but it + can be done. ++ +For QtWebKit: + +. Install https://github.com/QupZilla/qtwebkit-plugins[qtwebkit-plugins]. + . Note: with QtWebKit reloaded you may experience some issues. See + https://github.com/QupZilla/qtwebkit-plugins/issues/10[#10]. +. The dictionary to use is taken from the `DICTIONARY` environment variable. + The default is `en_US`. For example to use Dutch spell check set `DICTIONARY` + to `nl_NL`; you can't use multiple dictionaries or change them at runtime at + the moment. + (also see the README file for `qtwebkit-plugins`). +. Remember to install the hunspell dictionaries if you don't have them already + (most distros should have packages for this). + ++ +For QtWebEngine: + +. Not yet supported unfortunately :-( + + Adding it shouldn't be too hard though, since QtWebEngine 5.8 added an API for + this (see + https://github.com/qutebrowser/qutebrowser/issues/700#issuecomment-290780706[this + comment for a basic example]), so what are you waiting for and why aren't you + hacking qutebrowser yet? + == Troubleshooting Configuration not saved after modifying config.:: From 79a22f1f4751741cabd5f5a19ee4bcdb6b6dfce8 Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Sat, 1 Apr 2017 21:14:35 +0100 Subject: [PATCH 048/825] Allow pressing ^C when there's an unknown setting All of it is just converting `objreg.get('xxx')` to `objreg.get('xxx', None)` and adding a `if xxx is not None` check. Fixes #1170 --- qutebrowser/app.py | 29 +++++++++++++++++------------ qutebrowser/misc/crashsignal.py | 8 ++++---- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 15a60ec4e..202eb9721 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -340,8 +340,9 @@ def _open_quickstart(args): def _save_version(): """Save the current version to the state config.""" - state_config = objreg.get('state-config') - state_config['general']['version'] = qutebrowser.__version__ + state_config = objreg.get('state-config', None) + if state_config is not None: + state_config['general']['version'] = qutebrowser.__version__ def on_focus_changed(_old, new): @@ -647,14 +648,14 @@ class Quitter: self._shutting_down = True log.destroy.debug("Shutting down with status {}, session {}...".format( status, session)) - - session_manager = objreg.get('session-manager') - if session is not None: - session_manager.save(session, last_window=last_window, - load_next_time=True) - elif config.get('general', 'save-session'): - session_manager.save(sessions.default, last_window=last_window, - load_next_time=True) + session_manager = objreg.get('session-manager', None) + if session_manager is not None: + if session is not None: + session_manager.save(session, last_window=last_window, + load_next_time=True) + elif config.get('general', 'save-session'): + session_manager.save(sessions.default, last_window=last_window, + load_next_time=True) if prompt.prompt_queue.shutdown(): # If shutdown was called while we were asking a question, we're in @@ -680,7 +681,9 @@ class Quitter: # Remove eventfilter try: log.destroy.debug("Removing eventfilter...") - qApp.removeEventFilter(objreg.get('event-filter')) + event_filter = objreg.get('event-filter', None) + if event_filter is not None: + qApp.removeEventFilter(event_filter) except AttributeError: pass # Close all windows @@ -722,7 +725,9 @@ class Quitter: # Now we can hopefully quit without segfaults log.destroy.debug("Deferring QApplication::exit...") objreg.get('signal-handler').deactivate() - objreg.get('session-manager').delete_autosave() + session_manager = objreg.get('session-manager', None) + if session_manager is not None: + session_manager.delete_autosave() # We use a singleshot timer to exit here to minimize the likelihood of # segfaults. QTimer.singleShot(0, functools.partial(qApp.exit, status)) diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py index 35c39bf2e..aa7438732 100644 --- a/qutebrowser/misc/crashsignal.py +++ b/qutebrowser/misc/crashsignal.py @@ -171,14 +171,14 @@ class CrashHandler(QObject): """ try: pages = self._recover_pages(forgiving=True) - except Exception: - log.destroy.exception("Error while recovering pages") + except Exception as e: + log.destroy.exception("Error while recovering pages: {}".format(e)) pages = [] try: cmd_history = objreg.get('command-history')[-5:] - except Exception: - log.destroy.exception("Error while getting history: {}") + except Exception as e: + log.destroy.exception("Error while getting history: {}".format(e)) cmd_history = [] try: From 2238a888dc70293adb4e253c81b9f1c600f93f18 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 2 Apr 2017 15:20:44 +0200 Subject: [PATCH 049/825] Fix changelog --- CHANGELOG.asciidoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 1e8afb3a2..e43780735 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -32,7 +32,8 @@ Changed version info. - Improved `qute:history` page with lazy loading - Messages are now hidden when clicked -- Paths like `C:Downloads` are now treated as absolute paths on Windows. +- Paths like `C:` are now treated as absolute paths on Windows for downloads, + and invalid paths are handled properly. - PAC on QtWebKit now supports SOCK5 as type. Fixed From f2ddf608a89334c40e11f89d689cfe69aa425870 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 2 Apr 2017 18:33:55 +0200 Subject: [PATCH 050/825] Update authors --- README.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/README.asciidoc b/README.asciidoc index 99062bd99..066c68078 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -285,6 +285,7 @@ Contributors, sorted by the number of commits in descending order: * Arseniy Seroka * Andy Balaam * Andreas Fischer +* Amos Bird * Akselmo // QUTE_AUTHORS_END From f595c2a7fb36280c5d0591f7ca711cf1d8c6db5d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 2 Apr 2017 18:34:39 +0200 Subject: [PATCH 051/825] Update changelog --- CHANGELOG.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index e43780735..40b90308d 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -45,6 +45,7 @@ Fixed - Crash when cloning page without history - Continuing a search after clearing it - Crash when downloading a download resulting in a HTTP error +- Crash when pressing ctrl-c while a config error is shown - Various rare crashes v0.10.1 From 338307ac24d3b30a11ef443fdbe88d43839cddc1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 2 Apr 2017 18:35:10 +0200 Subject: [PATCH 052/825] Add #noqa for Quitter._shutdown --- qutebrowser/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 202eb9721..3fe15ff46 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -672,7 +672,7 @@ class Quitter: # event loop, so we can shut down immediately. self._shutdown(status, restart=restart) - def _shutdown(self, status, restart): + def _shutdown(self, status, restart): # noqa """Second stage of shutdown.""" log.destroy.debug("Stage 2 of shutting down...") if qApp is None: From 48094fb33d4f12704c66ea8dee1c278d40bc8263 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 2 Apr 2017 18:47:16 +0200 Subject: [PATCH 053/825] Update authors --- README.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.asciidoc b/README.asciidoc index 066c68078..65d9ed5ba 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -155,8 +155,8 @@ Contributors, sorted by the number of commits in descending order: * Alexander Cogneau * Felix Van der Jeugt * Daniel Karbach -* Imran Sobir * Martin Tournoij +* Imran Sobir * Kevin Velghe * Raphael Pierzina * Joel Torstensson From ad6ed837820cf85217b17879b14a07359f82ec4f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 2 Apr 2017 19:17:13 +0200 Subject: [PATCH 054/825] Update changelog --- CHANGELOG.asciidoc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 40b90308d..b74535176 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -34,7 +34,9 @@ Changed - Messages are now hidden when clicked - Paths like `C:` are now treated as absolute paths on Windows for downloads, and invalid paths are handled properly. -- PAC on QtWebKit now supports SOCK5 as type. +- PAC on QtWebKit now supports SOCKS5 as type. +- Comments in the config file are now before the individual + options instead of being before sections. Fixed ~~~~~ From e3fc23fa308fc36d7198e7ac8cbad121d8956fa5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 2 Apr 2017 19:24:06 +0200 Subject: [PATCH 055/825] Update authors --- README.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.asciidoc b/README.asciidoc index 65d9ed5ba..ad4c5232d 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -154,8 +154,8 @@ Contributors, sorted by the number of commits in descending order: * Bruno Oliveira * Alexander Cogneau * Felix Van der Jeugt -* Daniel Karbach * Martin Tournoij +* Daniel Karbach * Imran Sobir * Kevin Velghe * Raphael Pierzina From 30655e29fc292ec99cea41a03edea751fd5022b9 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 2 Apr 2017 22:58:22 +0200 Subject: [PATCH 056/825] Regenerate authors --- README.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/README.asciidoc b/README.asciidoc index ad4c5232d..3228d8c7b 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -239,6 +239,7 @@ Contributors, sorted by the number of commits in descending order: * adam * Samir Benmendil * Regina Hug +* Penaz * Mathias Fussenegger * Marcelo Santos * Joel Bradshaw From 4004d5adf09e6d22dae5d781a02c5fc2bbd26724 Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Sat, 1 Apr 2017 21:46:36 +0100 Subject: [PATCH 057/825] Don't crash when trying to write an unwritable keyconf. Also change the logic in _load_default a wee bit so that it won't try to write the keys.conf on startup. Fixes #1235 --- qutebrowser/config/parsers/keyconf.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/qutebrowser/config/parsers/keyconf.py b/qutebrowser/config/parsers/keyconf.py index 6ca5f72b7..d8aaa960e 100644 --- a/qutebrowser/config/parsers/keyconf.py +++ b/qutebrowser/config/parsers/keyconf.py @@ -142,9 +142,14 @@ class KeyConfigParser(QObject): def save(self): """Save the key config file.""" log.destroy.debug("Saving key config to {}".format(self._configfile)) - with qtutils.savefile_open(self._configfile, encoding='utf-8') as f: - data = str(self) - f.write(data) + + try: + with qtutils.savefile_open(self._configfile, + encoding='utf-8') as f: + data = str(self) + f.write(data) + except OSError as e: + message.error("Could not save key config: {}".format(e)) @cmdutils.register(instance='key-config', maxsplit=1, no_cmd_split=True, no_replace_variables=True) @@ -252,6 +257,7 @@ class KeyConfigParser(QObject): """ # {'sectname': {'keychain1': 'command', 'keychain2': 'command'}, ...} bindings_to_add = collections.OrderedDict() + mark_dirty = False for sectname, sect in configdata.KEY_DATA.items(): sectname = self._normalize_sectname(sectname) @@ -261,6 +267,7 @@ class KeyConfigParser(QObject): if not only_new or self._is_new(sectname, command, e): assert e not in bindings_to_add[sectname] bindings_to_add[sectname][e] = command + mark_dirty = True for sectname, sect in bindings_to_add.items(): if not sect: @@ -271,7 +278,7 @@ class KeyConfigParser(QObject): self._add_binding(sectname, keychain, command) self.changed.emit(sectname) - if bindings_to_add: + if mark_dirty: self._mark_config_dirty() def _is_new(self, sectname, command, keychain): From 9dc5e978ac92c514c92ea2e345046f8db84de7b2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Apr 2017 06:55:18 +0200 Subject: [PATCH 058/825] Update docs --- CHANGELOG.asciidoc | 1 + README.asciidoc | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index b74535176..ba5b7cfed 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -48,6 +48,7 @@ Fixed - Continuing a search after clearing it - Crash when downloading a download resulting in a HTTP error - Crash when pressing ctrl-c while a config error is shown +- Crash when the key config isn't writable - Various rare crashes v0.10.1 diff --git a/README.asciidoc b/README.asciidoc index 3228d8c7b..90c6abde9 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -153,8 +153,8 @@ Contributors, sorted by the number of commits in descending order: * Marshall Lochbaum * Bruno Oliveira * Alexander Cogneau -* Felix Van der Jeugt * Martin Tournoij +* Felix Van der Jeugt * Daniel Karbach * Imran Sobir * Kevin Velghe From a11356bb992bbe3070594c33af082f967c1c90c5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Apr 2017 08:32:39 +0200 Subject: [PATCH 059/825] Don't require working icon to start --- qutebrowser/app.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 3fe15ff46..6bea00f31 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -170,12 +170,15 @@ def _init_icon(): for size in [16, 24, 32, 48, 64, 96, 128, 256, 512]: filename = ':/icons/qutebrowser-{}x{}.png'.format(size, size) pixmap = QPixmap(filename) - qtutils.ensure_not_null(pixmap) - fallback_icon.addPixmap(pixmap) - qtutils.ensure_not_null(fallback_icon) + if pixmap.isNull(): + log.init.warn("Failed to load {}".format(filename)) + else: + fallback_icon.addPixmap(pixmap) icon = QIcon.fromTheme('qutebrowser', fallback_icon) - qtutils.ensure_not_null(icon) - qApp.setWindowIcon(icon) + if icon.isNull(): + log.init.warn("Failed to load icon") + else: + qApp.setWindowIcon(icon) def _process_args(args): From 3b1b325711ea5a2e5a3d8d59bee2f27b3b06b4fb Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Apr 2017 09:04:28 +0200 Subject: [PATCH 060/825] Fix logging --- qutebrowser/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 6bea00f31..81079104a 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -171,12 +171,12 @@ def _init_icon(): filename = ':/icons/qutebrowser-{}x{}.png'.format(size, size) pixmap = QPixmap(filename) if pixmap.isNull(): - log.init.warn("Failed to load {}".format(filename)) + log.init.warning("Failed to load {}".format(filename)) else: fallback_icon.addPixmap(pixmap) icon = QIcon.fromTheme('qutebrowser', fallback_icon) if icon.isNull(): - log.init.warn("Failed to load icon") + log.init.warning("Failed to load icon") else: qApp.setWindowIcon(icon) From 2c3fcda18e14c36cd3d39288cd622ad993fa1408 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Apr 2017 09:32:13 +0200 Subject: [PATCH 061/825] Remove qtutils.ensure_not_null It's not used anymore. --- qutebrowser/utils/qtutils.py | 6 ----- tests/unit/utils/test_qtutils.py | 42 ++++++++++---------------------- 2 files changed, 13 insertions(+), 35 deletions(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index e36fd0ffb..5c98a24da 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -174,12 +174,6 @@ def ensure_valid(obj): raise QtValueError(obj) -def ensure_not_null(obj): - """Ensure a Qt object with an .isNull() method is not null.""" - if obj.isNull(): - raise QtValueError(obj, null=True) - - def check_qdatastream(stream): """Check the status of a QDataStream and raise OSError if it's not ok.""" status_to_str = { diff --git a/tests/unit/utils/test_qtutils.py b/tests/unit/utils/test_qtutils.py index b2de7b58d..74da2ac4f 100644 --- a/tests/unit/utils/test_qtutils.py +++ b/tests/unit/utils/test_qtutils.py @@ -209,48 +209,32 @@ class QtObject: 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'), +@pytest.mark.parametrize('obj, raising, exc_reason, exc_str', [ + # good examples + (QtObject(valid=True, null=True), False, None, None), + (QtObject(valid=True, null=False), False, None, None), + # bad examples + (QtObject(valid=False, null=True), True, None, ' is not valid'), + (QtObject(valid=False, null=False), True, None, ' is not valid'), + (QtObject(valid=False, null=True, error='Test'), True, 'Test', + ' is not valid: 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. +def test_ensure_valid(obj, raising, exc_reason, exc_str): + """Test ensure_valid. 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) + qtutils.ensure_valid(obj) assert excinfo.value.reason == exc_reason assert str(excinfo.value) == exc_str else: - func(obj) + qtutils.ensure_valid(obj) @pytest.mark.parametrize('status, raising, message', [ From cb4c64eec99694af89f5256c176f186032c2395f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Apr 2017 10:18:56 +0200 Subject: [PATCH 062/825] Remove null argument for QtValueError --- qutebrowser/utils/qtutils.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 5c98a24da..adf0b2737 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -406,15 +406,12 @@ class QtValueError(ValueError): """Exception which gets raised by ensure_valid.""" - def __init__(self, obj, null=False): + def __init__(self, obj): try: self.reason = obj.errorString() except AttributeError: self.reason = None - if null: - err = "{} is null".format(obj) - else: - err = "{} is not valid".format(obj) + err = "{} is not valid".format(obj) if self.reason: err += ": {}".format(self.reason) super().__init__(err) From 8f821137483b7e16c1ff53a03e8774deea2cfc1a Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 3 Apr 2017 16:32:48 +0200 Subject: [PATCH 063/825] Update jinja2 from 2.9.5 to 2.9.6 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a62285a08..5b4053a33 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ colorama==0.3.7 cssutils==1.0.2 -Jinja2==2.9.5 +Jinja2==2.9.6 MarkupSafe==1.0 Pygments==2.2.0 pyPEG2==2.15.2 From 1b0ea19ca47e65d353ccb7f19fd5158f013b0cf4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Apr 2017 17:48:12 +0200 Subject: [PATCH 064/825] Disable QtNetwork cache on Qt 5.8 See #2427 --- CHANGELOG.asciidoc | 2 ++ qutebrowser/browser/webkit/cache.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index ba5b7cfed..5ac5f9460 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -37,6 +37,8 @@ Changed - PAC on QtWebKit now supports SOCKS5 as type. - Comments in the config file are now before the individual options instead of being before sections. +- The HTTP cache is disabled with QtWebKit on Qt 5.8 now as it leads to frequent + crashes due to a Qt bug. Fixed ~~~~~ diff --git a/qutebrowser/browser/webkit/cache.py b/qutebrowser/browser/webkit/cache.py index 860a532b0..78f459e3f 100644 --- a/qutebrowser/browser/webkit/cache.py +++ b/qutebrowser/browser/webkit/cache.py @@ -25,7 +25,7 @@ from PyQt5.QtCore import pyqtSlot from PyQt5.QtNetwork import QNetworkDiskCache, QNetworkCacheMetaData from qutebrowser.config import config -from qutebrowser.utils import utils, objreg +from qutebrowser.utils import utils, objreg, qtutils class DiskCache(QNetworkDiskCache): @@ -53,6 +53,9 @@ class DiskCache(QNetworkDiskCache): size = config.get('storage', 'cache-size') if size is None: size = 1024 * 1024 * 50 # default from QNetworkDiskCachePrivate + # WORKAROUND for https://github.com/qutebrowser/qutebrowser/issues/2427 + if qtutils.version_check('5.8', exact=True): + size = 0 self.setMaximumCacheSize(size) def _maybe_activate(self): From b6642e66fa1594a0a6e598110a1f53a299999a25 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Apr 2017 19:41:37 +0200 Subject: [PATCH 065/825] Fix cache tests on Qt 5.8 --- qutebrowser/browser/webkit/cache.py | 2 +- tests/unit/browser/webkit/test_cache.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/webkit/cache.py b/qutebrowser/browser/webkit/cache.py index 78f459e3f..349f9a61e 100644 --- a/qutebrowser/browser/webkit/cache.py +++ b/qutebrowser/browser/webkit/cache.py @@ -54,7 +54,7 @@ class DiskCache(QNetworkDiskCache): if size is None: size = 1024 * 1024 * 50 # default from QNetworkDiskCachePrivate # WORKAROUND for https://github.com/qutebrowser/qutebrowser/issues/2427 - if qtutils.version_check('5.8', exact=True): + if qtutils.version_check('5.8', exact=True): # pragma: no cover size = 0 self.setMaximumCacheSize(size) diff --git a/tests/unit/browser/webkit/test_cache.py b/tests/unit/browser/webkit/test_cache.py index c716b9b82..7e2164b9c 100644 --- a/tests/unit/browser/webkit/test_cache.py +++ b/tests/unit/browser/webkit/test_cache.py @@ -17,10 +17,16 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . +import pytest from PyQt5.QtCore import QUrl, QDateTime from PyQt5.QtNetwork import QNetworkDiskCache, QNetworkCacheMetaData from qutebrowser.browser.webkit import cache +from qutebrowser.utils import qtutils + + +pytestmark = pytest.mark.skipif(qtutils.version_check('5.8', exact=True), + reason="QNetworkDiskCache is broken on Qt 5.8") def preload_cache(cache, url='http://www.example.com/', content=b'foobar'): From 2eb365b1468c5a18df267f739dfbc10faa64216f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Apr 2017 20:22:54 +0200 Subject: [PATCH 066/825] Also disable cache on Qt 5.7 --- qutebrowser/browser/webkit/cache.py | 4 ++-- tests/unit/browser/webkit/test_cache.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/qutebrowser/browser/webkit/cache.py b/qutebrowser/browser/webkit/cache.py index 349f9a61e..4e09844cc 100644 --- a/qutebrowser/browser/webkit/cache.py +++ b/qutebrowser/browser/webkit/cache.py @@ -53,8 +53,8 @@ class DiskCache(QNetworkDiskCache): size = config.get('storage', 'cache-size') if size is None: size = 1024 * 1024 * 50 # default from QNetworkDiskCachePrivate - # WORKAROUND for https://github.com/qutebrowser/qutebrowser/issues/2427 - if qtutils.version_check('5.8', exact=True): # pragma: no cover + # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-59909 + if qtutils.version_check('5.7'): # pragma: no cover size = 0 self.setMaximumCacheSize(size) diff --git a/tests/unit/browser/webkit/test_cache.py b/tests/unit/browser/webkit/test_cache.py index 7e2164b9c..8d3f2a99b 100644 --- a/tests/unit/browser/webkit/test_cache.py +++ b/tests/unit/browser/webkit/test_cache.py @@ -25,8 +25,9 @@ from qutebrowser.browser.webkit import cache from qutebrowser.utils import qtutils -pytestmark = pytest.mark.skipif(qtutils.version_check('5.8', exact=True), - reason="QNetworkDiskCache is broken on Qt 5.8") +pytestmark = pytest.mark.skipif(qtutils.version_check('5.7'), + reason="QNetworkDiskCache is broken on Qt >= " + "5.7") def preload_cache(cache, url='http://www.example.com/', content=b'foobar'): From 0de3b5460e5e3f46384309bcff45a096b9fef740 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 4 Apr 2017 08:24:50 +0200 Subject: [PATCH 067/825] Only disable the cache on Qt 5.7.1 I ended up bisecting it, and https://codereview.qt-project.org/#/c/153977/ causes this, which is not in 5.7.0. --- qutebrowser/browser/webkit/cache.py | 2 +- tests/unit/browser/webkit/test_cache.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qutebrowser/browser/webkit/cache.py b/qutebrowser/browser/webkit/cache.py index 4e09844cc..8bbb8f812 100644 --- a/qutebrowser/browser/webkit/cache.py +++ b/qutebrowser/browser/webkit/cache.py @@ -54,7 +54,7 @@ class DiskCache(QNetworkDiskCache): if size is None: size = 1024 * 1024 * 50 # default from QNetworkDiskCachePrivate # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-59909 - if qtutils.version_check('5.7'): # pragma: no cover + if qtutils.version_check('5.7.1'): # pragma: no cover size = 0 self.setMaximumCacheSize(size) diff --git a/tests/unit/browser/webkit/test_cache.py b/tests/unit/browser/webkit/test_cache.py index 8d3f2a99b..ea2473949 100644 --- a/tests/unit/browser/webkit/test_cache.py +++ b/tests/unit/browser/webkit/test_cache.py @@ -25,9 +25,9 @@ from qutebrowser.browser.webkit import cache from qutebrowser.utils import qtutils -pytestmark = pytest.mark.skipif(qtutils.version_check('5.7'), +pytestmark = pytest.mark.skipif(qtutils.version_check('5.7.1'), reason="QNetworkDiskCache is broken on Qt >= " - "5.7") + "5.7.1") def preload_cache(cache, url='http://www.example.com/', content=b'foobar'): From c5427a01272230b1b35afaa9764f77368fb7bfb8 Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Tue, 4 Apr 2017 09:50:12 +0100 Subject: [PATCH 068/825] Fix display of errors while reading the key config file Also catch `cmdexc.CommandError` on startup to show these errors in the alert dialog on startup. Fixes #1340 --- qutebrowser/config/config.py | 3 ++- qutebrowser/config/parsers/keyconf.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 83e0e9c8f..1e0cec92b 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -186,7 +186,8 @@ def _init_key_config(parent): key_config = keyconf.KeyConfigParser(standarddir.config(), 'keys.conf', args.relaxed_config, parent=parent) - except (keyconf.KeyConfigError, UnicodeDecodeError) as e: + except (keyconf.KeyConfigError, cmdexc.CommandError, + UnicodeDecodeError) as e: log.init.exception(e) errstr = "Error while reading key config:\n" if e.lineno is not None: diff --git a/qutebrowser/config/parsers/keyconf.py b/qutebrowser/config/parsers/keyconf.py index d8aaa960e..2f0d8df69 100644 --- a/qutebrowser/config/parsers/keyconf.py +++ b/qutebrowser/config/parsers/keyconf.py @@ -322,7 +322,7 @@ class KeyConfigParser(QObject): else: line = line.strip() self._read_command(line) - except KeyConfigError as e: + except (KeyConfigError, cmdexc.CommandError) as e: if relaxed: continue else: From be254be13a61171d4109224450db9e67d1076080 Mon Sep 17 00:00:00 2001 From: Imran Sobir Date: Tue, 4 Apr 2017 19:20:55 +0500 Subject: [PATCH 069/825] Use new history page on webkit-ng. --- qutebrowser/browser/qutescheme.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index 80a808773..9fddb2c38 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -36,7 +36,7 @@ from PyQt5.QtCore import QUrlQuery import qutebrowser from qutebrowser.config import config from qutebrowser.utils import (version, utils, jinja, log, message, docutils, - objreg, usertypes) + objreg, usertypes, qtutils) from qutebrowser.misc import objects @@ -257,9 +257,15 @@ def qute_history(url): return 'text/html', json.dumps(history_data(start_time)) else: + try: + from PyQt5.QtWebKit import qWebKitVersion + is_webkit_ng = qtutils.is_qtwebkit_ng(qWebKitVersion()) + except ImportError: # pragma: no cover + is_webkit_ng = False + if ( config.get('content', 'allow-javascript') and - objects.backend == usertypes.Backend.QtWebEngine + (objects.backend == usertypes.Backend.QtWebEngine or is_webkit_ng) ): return 'text/html', jinja.render( 'history.html', From 6c8ca3076693d41f2dcd843f0e9548430e5b3581 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 4 Apr 2017 18:26:04 +0200 Subject: [PATCH 070/825] Update docs --- CHANGELOG.asciidoc | 1 + README.asciidoc | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 5ac5f9460..5f51fe822 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -51,6 +51,7 @@ Fixed - Crash when downloading a download resulting in a HTTP error - Crash when pressing ctrl-c while a config error is shown - Crash when the key config isn't writable +- Crash when unbinding an unbound key in the key config - Various rare crashes v0.10.1 diff --git a/README.asciidoc b/README.asciidoc index 90c6abde9..1496c1787 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -152,8 +152,8 @@ Contributors, sorted by the number of commits in descending order: * Lamar Pavel * Marshall Lochbaum * Bruno Oliveira -* Alexander Cogneau * Martin Tournoij +* Alexander Cogneau * Felix Van der Jeugt * Daniel Karbach * Imran Sobir From 200e439a3051326d0c6a1191f89628aded094f40 Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Mon, 3 Apr 2017 02:29:38 +0100 Subject: [PATCH 071/825] Fix crash of :debug-log-filter when --filter wasn't given There was no `LogFilter`. The fix is to always initialize a `LogFilter()` with `None`. as the "filter". Fixes #2303. --- CHANGELOG.asciidoc | 1 + qutebrowser/misc/utilcmds.py | 9 ++++++--- qutebrowser/utils/log.py | 5 +++-- tests/end2end/features/utilcmds.feature | 4 ++++ 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 5f51fe822..74bf9e661 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -52,6 +52,7 @@ Fixed - Crash when pressing ctrl-c while a config error is shown - Crash when the key config isn't writable - Crash when unbinding an unbound key in the key config +- Crash when using `:debug-log-filter` when `--filter` wasn't given on startup. - Various rare crashes v0.10.1 diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index e617c1af2..1998e8341 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -295,13 +295,16 @@ def debug_log_filter(filters: str): Args: filters: A comma separated list of logger names. """ - if set(filters.split(',')).issubset(log.LOGGER_NAMES): - log.console_filter.names = filters.split(',') - else: + if not set(filters.split(',')).issubset(log.LOGGER_NAMES): raise cmdexc.CommandError("filters: Invalid value {} - expected one " "of: {}".format(filters, ', '.join(log.LOGGER_NAMES))) + if log.console_filter is None: + raise cmdexc.CommandError("No log.console_filter. Not attached " + "to a console?") + log.console_filter.names = filters.split(',') + @cmdutils.register() @cmdutils.argument('current_win_id', win_id=True) diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 7c96d4072..9c660f035 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -182,9 +182,10 @@ def init_log(args): root = logging.getLogger() global console_filter if console is not None: + console_filter = LogFilter(None) if args.logfilter is not None: - console_filter = LogFilter(args.logfilter.split(',')) - console.addFilter(console_filter) + console_filter.names = args.logfilter.split(',') + console.addFilter(console_filter) root.addHandler(console) if ram is not None: root.addHandler(ram) diff --git a/tests/end2end/features/utilcmds.feature b/tests/end2end/features/utilcmds.feature index f9c43be98..aee57e053 100644 --- a/tests/end2end/features/utilcmds.feature +++ b/tests/end2end/features/utilcmds.feature @@ -164,3 +164,7 @@ Feature: Miscellaneous utility commands exposed to the user. Scenario: Using debug-log-filter with invalid filter When I run :debug-log-filter blah Then the error "filters: Invalid value blah - expected one of: statusbar, *" should be shown + + Scenario: Using debug-log-filter + When I run :debug-log-filter webview + Then no crash should happen From 857565c384b3cbba647c8581e8e11b348db29cd9 Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Tue, 4 Apr 2017 19:58:51 +0100 Subject: [PATCH 072/825] Mention that qutebrowser@ also gets the mails from qutebrowser-announce@ --- README.asciidoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.asciidoc b/README.asciidoc index 1496c1787..b4f9718ce 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -71,7 +71,8 @@ https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser[mailinglist] at mailto:qutebrowser@lists.qutebrowser.org[]. There's also a https://lists.schokokeks.org/mailman/listinfo.cgi/qutebrowser-announce[announce-only mailinglist] -at mailto:qutebrowser-announce@lists.qutebrowser.org[]. +at mailto:qutebrowser-announce@lists.qutebrowser.org[] (the announcements also +get sent to the general qutebrowser@ list). Contributions / Bugs -------------------- From e7755f5d9f8a5e995b83a239c05016cf1d58abba Mon Sep 17 00:00:00 2001 From: Martin Tournoij Date: Tue, 4 Apr 2017 17:13:08 +0100 Subject: [PATCH 073/825] Add :debug-log-filter none This allows us to clear any filters. Useful for users, and needed for the tests. --- CHANGELOG.asciidoc | 2 ++ qutebrowser/misc/utilcmds.py | 14 ++++++++++---- tests/end2end/features/utilcmds.feature | 7 +++++-- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 74bf9e661..81ad97343 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -24,6 +24,8 @@ Added - New `ui -> keyhint-delay` setting to configure the delay until the keyhint overlay pops up. - New `-s` option for `:open` to force a HTTPS scheme. +- `:debug-log-filter` now accepts `none` as an argument to clear any log + filters. Changed ~~~~~~~ diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index 1998e8341..fc777c2e9 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -293,16 +293,22 @@ def debug_log_filter(filters: str): """Change the log filter for console logging. Args: - filters: A comma separated list of logger names. + filters: A comma separated list of logger names. Can also be "none" to + clear any existing filters. """ + if log.console_filter is None: + raise cmdexc.CommandError("No log.console_filter. Not attached " + "to a console?") + + if filters.strip().lower() == 'none': + log.console_filter.names = None + return + if not set(filters.split(',')).issubset(log.LOGGER_NAMES): raise cmdexc.CommandError("filters: Invalid value {} - expected one " "of: {}".format(filters, ', '.join(log.LOGGER_NAMES))) - if log.console_filter is None: - raise cmdexc.CommandError("No log.console_filter. Not attached " - "to a console?") log.console_filter.names = filters.split(',') diff --git a/tests/end2end/features/utilcmds.feature b/tests/end2end/features/utilcmds.feature index aee57e053..5696de75d 100644 --- a/tests/end2end/features/utilcmds.feature +++ b/tests/end2end/features/utilcmds.feature @@ -166,5 +166,8 @@ Feature: Miscellaneous utility commands exposed to the user. Then the error "filters: Invalid value blah - expected one of: statusbar, *" should be shown Scenario: Using debug-log-filter - When I run :debug-log-filter webview - Then no crash should happen + When I run :debug-log-filter commands,ipc,webview + And I run :enter-mode insert + And I run :debug-log-filter none + And I run :leave-mode + Then "Entering mode KeyMode.insert *" should not be logged From 6f952c83af995adbfe5021df71520bc869e05009 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 6 Apr 2017 07:16:18 +0200 Subject: [PATCH 074/825] Update docs --- doc/help/commands.asciidoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 3e10b9d81..1a76e7fc8 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -1599,7 +1599,8 @@ Syntax: +:debug-log-filter 'filters'+ Change the log filter for console logging. ==== positional arguments -* +'filters'+: A comma separated list of logger names. +* +'filters'+: A comma separated list of logger names. Can also be "none" to clear any existing filters. + [[debug-log-level]] === debug-log-level From 6d4948f9d098171403615e08c4e85ce8c85c4cea Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 6 Apr 2017 20:35:32 +0200 Subject: [PATCH 075/825] Update authors --- README.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.asciidoc b/README.asciidoc index b4f9718ce..96b795463 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -155,9 +155,9 @@ Contributors, sorted by the number of commits in descending order: * Bruno Oliveira * Martin Tournoij * Alexander Cogneau +* Imran Sobir * Felix Van der Jeugt * Daniel Karbach -* Imran Sobir * Kevin Velghe * Raphael Pierzina * Joel Torstensson From 3cc9f9f0731f0b19a912bb03355727a177a59b2e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 6 Apr 2017 20:36:54 +0200 Subject: [PATCH 076/825] Don't use from-import --- qutebrowser/browser/qutescheme.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index 9fddb2c38..f7f074c54 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -24,12 +24,12 @@ Module attributes: _HANDLERS: The handlers registered via decorators. """ -from datetime import date, datetime, timedelta import json import os import sys import time import urllib.parse +import datetime from PyQt5.QtCore import QUrlQuery @@ -278,20 +278,21 @@ def qute_history(url): try: query_date = QUrlQuery(url).queryItemValue("date") if query_date: - curr_date = datetime.strptime(query_date, + curr_date = datetime.datetime.strptime(query_date, "%Y-%m-%d").date() except ValueError: log.misc.debug("Invalid date passed to qute:history: " + query_date) - one_day = timedelta(days=1) + one_day = datetime.timedelta(days=1) next_date = curr_date + one_day prev_date = curr_date - one_day # start_time is the last second of curr_date start_time = time.mktime(next_date.timetuple()) - 1 history = [ - (i["url"], i["title"], datetime.fromtimestamp(i["time"]/1000)) + (i["url"], i["title"], + datetime.datetime.fromtimestamp(i["time"]/1000)) for i in history_data(start_time) if "next" not in i ] From 4ec5700cbf9ced71c24d57de3ddcae102c9116c0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 6 Apr 2017 20:38:15 +0200 Subject: [PATCH 077/825] Redirect qute:foo to qute://foo Before, we just returned the same data for both, but then we'll run into same-origin restrictions as qute:history and qute:history/data are not the same host. --- misc/userscripts/password_fill | 2 +- qutebrowser/browser/qtnetworkdownloads.py | 2 +- qutebrowser/browser/qutescheme.py | 57 ++++++++++++------- .../browser/webengine/webenginequtescheme.py | 15 +++-- qutebrowser/browser/webengine/webenginetab.py | 2 +- .../browser/webkit/network/networkreply.py | 24 ++++++-- .../webkit/network/webkitqutescheme.py | 17 +++--- qutebrowser/browser/webkit/webview.py | 2 +- qutebrowser/config/config.py | 2 +- qutebrowser/config/configdata.py | 2 +- qutebrowser/misc/utilcmds.py | 2 +- scripts/dev/run_vulture.py | 2 +- tests/end2end/features/history.feature | 8 +++ tests/end2end/features/misc.feature | 12 ++-- tests/end2end/features/set.feature | 12 ++-- tests/end2end/features/urlmarks.feature | 4 +- tests/end2end/features/utilcmds.feature | 2 +- tests/end2end/test_invocations.py | 4 +- .../webkit/network/test_networkreply.py | 7 +++ tests/unit/utils/test_urlutils.py | 3 + 20 files changed, 118 insertions(+), 63 deletions(-) diff --git a/misc/userscripts/password_fill b/misc/userscripts/password_fill index 5d709512c..327a55690 100755 --- a/misc/userscripts/password_fill +++ b/misc/userscripts/password_fill @@ -9,7 +9,7 @@ directly ask me via IRC (nickname thorsten\`) in #qutebrowser on freenode. $blink!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!$reset WARNING: the passwords are stored in qutebrowser's - debug log reachable via the url qute:log + debug log reachable via the url qute://log $blink!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!$reset Usage: run as a userscript form qutebrowser, e.g.: diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py index 920673d4b..56149a8cc 100644 --- a/qutebrowser/browser/qtnetworkdownloads.py +++ b/qutebrowser/browser/qtnetworkdownloads.py @@ -273,7 +273,7 @@ class DownloadItem(downloads.AbstractDownloadItem): if self.fileobj is None or self._reply is None: # No filename has been set yet (so we don't empty the buffer) or we # got a readyRead after the reply was finished (which happens on - # qute:log for example). + # qute://log for example). return if not self._reply.isOpen(): raise OSError("Reply is closed!") diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index f7f074c54..5f25c24d1 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -"""Backend-independent qute:* code. +"""Backend-independent qute://* code. Module attributes: pyeval_output: The output of the last :pyeval command. @@ -31,7 +31,7 @@ import time import urllib.parse import datetime -from PyQt5.QtCore import QUrlQuery +from PyQt5.QtCore import QUrlQuery, QUrl import qutebrowser from qutebrowser.config import config @@ -78,12 +78,25 @@ class QuteSchemeError(Exception): super().__init__(errorstring) -class add_handler: # pylint: disable=invalid-name +class Redirect(Exception): - """Decorator to register a qute:* URL handler. + """Exception to signal a redirect should happen. Attributes: - _name: The 'foo' part of qute:foo + url: The URL to redirect to, as a QUrl. + """ + + def __init__(self, url): + super().__init__(url.toDisplayString()) + self.url = url + + +class add_handler: # pylint: disable=invalid-name + + """Decorator to register a qute://* URL handler. + + Attributes: + _name: The 'foo' part of qute://foo backend: Limit which backends the handler can run with. """ @@ -106,7 +119,7 @@ class add_handler: # pylint: disable=invalid-name def wrong_backend_handler(self, url): """Show an error page about using the invalid backend.""" html = jinja.render('error.html', - title="Error while opening qute:url", + title="Error while opening qute://url", url=url.toDisplayString(), error='{} is not available with this ' 'backend'.format(url.toDisplayString()), @@ -128,13 +141,17 @@ def data_for_url(url): # A url like "qute:foo" is split as "scheme:path", not "scheme:host". log.misc.debug("url: {}, path: {}, host {}".format( url.toDisplayString(), path, host)) + if path and not host: + new_url = QUrl() + new_url.setScheme('qute') + new_url.setHost(path) + raise Redirect(new_url) + try: - handler = _HANDLERS[path] + handler = _HANDLERS[host] except KeyError: - try: - handler = _HANDLERS[host] - except KeyError: - raise NoHandlerFound(url) + raise NoHandlerFound(url) + try: mimetype, data = handler(url) except OSError as e: @@ -153,7 +170,7 @@ def data_for_url(url): @add_handler('bookmarks') def qute_bookmarks(_url): - """Handler for qute:bookmarks. Display all quickmarks / bookmarks.""" + """Handler for qute://bookmarks. Display all quickmarks / bookmarks.""" bookmarks = sorted(objreg.get('bookmark-manager').marks.items(), key=lambda x: x[1]) # Sort by title quickmarks = sorted(objreg.get('quickmark-manager').marks.items(), @@ -246,7 +263,7 @@ def history_data(start_time): # noqa @add_handler('history') def qute_history(url): - """Handler for qute:history. Display and serve history.""" + """Handler for qute://history. Display and serve history.""" if url.path() == '/data': # Use start_time in query or current time. try: @@ -309,7 +326,7 @@ def qute_history(url): @add_handler('javascript') def qute_javascript(url): - """Handler for qute:javascript. + """Handler for qute://javascript. Return content of file given as query parameter. """ @@ -323,7 +340,7 @@ def qute_javascript(url): @add_handler('pyeval') def qute_pyeval(_url): - """Handler for qute:pyeval.""" + """Handler for qute://pyeval.""" html = jinja.render('pre.html', title='pyeval', content=pyeval_output) return 'text/html', html @@ -331,7 +348,7 @@ def qute_pyeval(_url): @add_handler('version') @add_handler('verizon') def qute_version(_url): - """Handler for qute:version.""" + """Handler for qute://version.""" html = jinja.render('version.html', title='Version info', version=version.version(), copyright=qutebrowser.__copyright__) @@ -340,7 +357,7 @@ def qute_version(_url): @add_handler('plainlog') def qute_plainlog(url): - """Handler for qute:plainlog. + """Handler for qute://plainlog. An optional query parameter specifies the minimum log level to print. For example, qute://log?level=warning prints warnings and errors. @@ -360,7 +377,7 @@ def qute_plainlog(url): @add_handler('log') def qute_log(url): - """Handler for qute:log. + """Handler for qute://log. An optional query parameter specifies the minimum log level to print. For example, qute://log?level=warning prints warnings and errors. @@ -381,13 +398,13 @@ def qute_log(url): @add_handler('gpl') def qute_gpl(_url): - """Handler for qute:gpl. Return HTML content as string.""" + """Handler for qute://gpl. Return HTML content as string.""" return 'text/html', utils.read_file('html/COPYING.html') @add_handler('help') def qute_help(url): - """Handler for qute:help.""" + """Handler for qute://help.""" try: utils.read_file('html/doc/index.html') except OSError: diff --git a/qutebrowser/browser/webengine/webenginequtescheme.py b/qutebrowser/browser/webengine/webenginequtescheme.py index 6bc31f9be..cebf31356 100644 --- a/qutebrowser/browser/webengine/webenginequtescheme.py +++ b/qutebrowser/browser/webengine/webenginequtescheme.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -"""QtWebEngine specific qute:* handlers and glue code.""" +"""QtWebEngine specific qute://* handlers and glue code.""" from PyQt5.QtCore import QBuffer, QIODevice # pylint: disable=no-name-in-module,import-error,useless-suppression @@ -26,15 +26,15 @@ from PyQt5.QtWebEngineCore import (QWebEngineUrlSchemeHandler, # pylint: enable=no-name-in-module,import-error,useless-suppression from qutebrowser.browser import qutescheme -from qutebrowser.utils import log +from qutebrowser.utils import log, qtutils class QuteSchemeHandler(QWebEngineUrlSchemeHandler): - """Handle qute:* requests on QtWebEngine.""" + """Handle qute://* requests on QtWebEngine.""" def install(self, profile): - """Install the handler for qute: URLs on the given profile.""" + """Install the handler for qute:// URLs on the given profile.""" profile.installUrlSchemeHandler(b'qute', self) def requestStarted(self, job): @@ -58,12 +58,15 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler): job.fail(QWebEngineUrlRequestJob.UrlNotFound) except qutescheme.QuteSchemeOSError: # FIXME:qtwebengine how do we show a better error here? - log.misc.exception("OSError while handling qute:* URL") + log.misc.exception("OSError while handling qute://* URL") job.fail(QWebEngineUrlRequestJob.UrlNotFound) except qutescheme.QuteSchemeError: # FIXME:qtwebengine how do we show a better error here? - log.misc.exception("Error while handling qute:* URL") + log.misc.exception("Error while handling qute://* URL") job.fail(QWebEngineUrlRequestJob.RequestFailed) + except qutescheme.Redirect as e: + qtutils.ensure_valid(e.url) + job.redirect(e.url) else: log.misc.debug("Returning {} data".format(mimetype)) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 76a526670..7eef0b03a 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -55,7 +55,7 @@ def init(): app = QApplication.instance() profile = QWebEngineProfile.defaultProfile() - log.init.debug("Initializing qute:* handler...") + log.init.debug("Initializing qute://* handler...") _qute_scheme_handler = webenginequtescheme.QuteSchemeHandler(parent=app) _qute_scheme_handler.install(profile) diff --git a/qutebrowser/browser/webkit/network/networkreply.py b/qutebrowser/browser/webkit/network/networkreply.py index 2cc5727be..9638daf4a 100644 --- a/qutebrowser/browser/webkit/network/networkreply.py +++ b/qutebrowser/browser/webkit/network/networkreply.py @@ -19,11 +19,15 @@ # # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . +# +# For some reason, a segfault will be triggered if the unnecessary lambdas in +# this file aren't there. +# pylint: disable=unnecessary-lambda """Special network replies..""" -from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest -from PyQt5.QtCore import pyqtSlot, QIODevice, QByteArray, QTimer +from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest, QNetworkAccessManager +from PyQt5.QtCore import pyqtSlot, QIODevice, QByteArray, QTimer, QUrl class FixedDataNetworkReply(QNetworkReply): @@ -114,9 +118,6 @@ class ErrorNetworkReply(QNetworkReply): # the device to avoid getting a warning. self.setOpenMode(QIODevice.ReadOnly) self.setError(error, errorstring) - # For some reason, a segfault will be triggered if these lambdas aren't - # there. - # pylint: disable=unnecessary-lambda QTimer.singleShot(0, lambda: self.error.emit(error)) QTimer.singleShot(0, lambda: self.finished.emit()) @@ -137,3 +138,16 @@ class ErrorNetworkReply(QNetworkReply): def isRunning(self): return False + + +class RedirectNetworkReply(QNetworkReply): + + """A reply which redirects to the given URL.""" + + def __init__(self, new_url, parent=None): + super().__init__(parent) + self.setAttribute(QNetworkRequest.RedirectionTargetAttribute, new_url) + QTimer.singleShot(0, lambda: self.finished.emit()) + + def readData(self, _maxlen): + return bytes() diff --git a/qutebrowser/browser/webkit/network/webkitqutescheme.py b/qutebrowser/browser/webkit/network/webkitqutescheme.py index 61ef760bc..34db29ee9 100644 --- a/qutebrowser/browser/webkit/network/webkitqutescheme.py +++ b/qutebrowser/browser/webkit/network/webkitqutescheme.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -"""QtWebKit specific qute:* handlers and glue code.""" +"""QtWebKit specific qute://* handlers and glue code.""" import mimetypes import functools @@ -28,13 +28,13 @@ from PyQt5.QtNetwork import QNetworkReply from qutebrowser.browser import pdfjs, qutescheme from qutebrowser.browser.webkit.network import schemehandler, networkreply -from qutebrowser.utils import jinja, log, message, objreg, usertypes +from qutebrowser.utils import jinja, log, message, objreg, usertypes, qtutils from qutebrowser.config import configexc, configdata class QuteSchemeHandler(schemehandler.SchemeHandler): - """Scheme handler for qute: URLs.""" + """Scheme handler for qute:// URLs.""" def createRequest(self, _op, request, _outgoing_data): """Create a new request. @@ -62,6 +62,9 @@ class QuteSchemeHandler(schemehandler.SchemeHandler): except qutescheme.QuteSchemeError as e: return networkreply.ErrorNetworkReply(request, e.errorstring, e.error, self.parent()) + except qutescheme.Redirect as e: + qtutils.ensure_valid(e.url) + return networkreply.RedirectNetworkReply(e.url, self.parent()) return networkreply.FixedDataNetworkReply(request, data, mimetype, self.parent()) @@ -69,15 +72,15 @@ class QuteSchemeHandler(schemehandler.SchemeHandler): class JSBridge(QObject): - """Javascript-bridge for special qute:... pages.""" + """Javascript-bridge for special qute://... pages.""" @pyqtSlot(str, str, str) def set(self, sectname, optname, value): - """Slot to set a setting from qute:settings.""" + """Slot to set a setting from qute://settings.""" # https://github.com/qutebrowser/qutebrowser/issues/727 if ((sectname, optname) == ('content', 'allow-javascript') and value == 'false'): - message.error("Refusing to disable javascript via qute:settings " + message.error("Refusing to disable javascript via qute://settings " "as it needs javascript support.") return try: @@ -88,7 +91,7 @@ class JSBridge(QObject): @qutescheme.add_handler('settings', backend=usertypes.Backend.QtWebKit) def qute_settings(_url): - """Handler for qute:settings. View/change qute configuration.""" + """Handler for qute://settings. View/change qute configuration.""" config_getter = functools.partial(objreg.get('config').get, raw=True) html = jinja.render('settings.html', title='settings', config=configdata, confget=config_getter) diff --git a/qutebrowser/browser/webkit/webview.py b/qutebrowser/browser/webkit/webview.py index 12670be4f..0c3aa179e 100644 --- a/qutebrowser/browser/webkit/webview.py +++ b/qutebrowser/browser/webkit/webview.py @@ -140,7 +140,7 @@ class WebView(QWebView): @pyqtSlot() def add_js_bridge(self): - """Add the javascript bridge for qute:... pages.""" + """Add the javascript bridge for qute://... pages.""" frame = self.sender() if not isinstance(frame, QWebFrame): log.webview.error("Got non-QWebFrame {!r} in " diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 1e0cec92b..82c6aca00 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -805,7 +805,7 @@ class ConfigManager(QObject): if section_ is None and option is None: tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) - tabbed_browser.openurl(QUrl('qute:settings'), newtab=False) + tabbed_browser.openurl(QUrl('qute://settings'), newtab=False) return if option.endswith('?') and option != '?': diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 1fbc22937..2c5b27808 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1689,7 +1689,7 @@ KEY_DATA = collections.OrderedDict([ ('home', ['']), ('stop', ['']), ('print', ['']), - ('open qute:settings', ['Ss']), + ('open qute://settings', ['Ss']), ('follow-selected', RETURN_KEYS), ('follow-selected -t', ['', '']), ('repeat-command', ['.']), diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index fc777c2e9..de43c71f4 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -228,7 +228,7 @@ def debug_pyeval(s, quiet=False): else: tabbed_browser = objreg.get('tabbed-browser', scope='window', window='last-focused') - tabbed_browser.openurl(QUrl('qute:pyeval'), newtab=True) + tabbed_browser.openurl(QUrl('qute://pyeval'), newtab=True) @cmdutils.register(debug=True) diff --git a/scripts/dev/run_vulture.py b/scripts/dev/run_vulture.py index 55f4d9c86..f1eed1803 100755 --- a/scripts/dev/run_vulture.py +++ b/scripts/dev/run_vulture.py @@ -74,7 +74,7 @@ def whitelist_generator(): yield 'PyQt5.QtWebKit.QWebPage.ErrorPageExtensionReturn().fileNames' yield 'PyQt5.QtWidgets.QStyleOptionViewItem.backgroundColor' - ## qute:... handlers + ## qute://... handlers for name in qutescheme._HANDLERS: # pylint: disable=protected-access yield 'qutebrowser.browser.qutescheme.qute_' + name diff --git a/tests/end2end/features/history.feature b/tests/end2end/features/history.feature index 27454c522..613b660af 100644 --- a/tests/end2end/features/history.feature +++ b/tests/end2end/features/history.feature @@ -83,6 +83,14 @@ Feature: Page history Then the page should contain the plaintext "3.txt" Then the page should contain the plaintext "4.txt" + Scenario: Listing history with qute:history redirect + When I open data/numbers/3.txt + And I open data/numbers/4.txt + And I open qute:history without waiting + And I wait until qute://history is loaded + Then the page should contain the plaintext "3.txt" + Then the page should contain the plaintext "4.txt" + ## Bugs @qtwebengine_skip @qtwebkit_ng_skip diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index 0aeb65f92..e3bb13b6f 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -405,12 +405,12 @@ Feature: Various utility commands. # :pyeval Scenario: Running :pyeval When I run :debug-pyeval 1+1 - And I wait until qute:pyeval is loaded + And I wait until qute://pyeval is loaded Then the page should contain the plaintext "2" Scenario: Causing exception in :pyeval When I run :debug-pyeval 1/0 - And I wait until qute:pyeval is loaded + And I wait until qute://pyeval is loaded Then the page should contain the plaintext "ZeroDivisionError" Scenario: Running :pyeval with --quiet @@ -512,12 +512,12 @@ Feature: Various utility commands. When I run :messages cataclysmic Then the error "Invalid log level cataclysmic!" should be shown - Scenario: Using qute:log directly - When I open qute:log + Scenario: Using qute://log directly + When I open qute://log Then no crash should happen - Scenario: Using qute:plainlog directly - When I open qute:plainlog + Scenario: Using qute://plainlog directly + When I open qute://plainlog Then no crash should happen Scenario: Using :messages without messages diff --git a/tests/end2end/features/set.feature b/tests/end2end/features/set.feature index b2b165542..bc144eb6e 100644 --- a/tests/end2end/features/set.feature +++ b/tests/end2end/features/set.feature @@ -78,15 +78,15 @@ Feature: Setting settings. When I run :set -t colors statusbar.bg green Then colors -> statusbar.bg should be green - # qute:settings isn't actually implemented on QtWebEngine, but this works + # qute://settings isn't actually implemented on QtWebEngine, but this works # (and displays a page saying it's not available) - Scenario: Opening qute:settings + Scenario: Opening qute://settings When I run :set - And I wait until qute:settings is loaded + And I wait until qute://settings is loaded Then the following tabs should be open: - - qute:settings (active) + - qute://settings (active) - @qtwebengine_todo: qute:settings is not implemented yet + @qtwebengine_todo: qute://settings is not implemented yet Scenario: Focusing input fields in qute://settings and entering valid value When I set general -> ignore-case to false And I open qute://settings @@ -101,7 +101,7 @@ Feature: Setting settings. And I press the key "" Then general -> ignore-case should be true - @qtwebengine_todo: qute:settings is not implemented yet + @qtwebengine_todo: qute://settings is not implemented yet Scenario: Focusing input fields in qute://settings and entering invalid value When I open qute://settings # scroll to the right - the table does not fit in the default screen diff --git a/tests/end2end/features/urlmarks.feature b/tests/end2end/features/urlmarks.feature index 873d83563..f233215b8 100644 --- a/tests/end2end/features/urlmarks.feature +++ b/tests/end2end/features/urlmarks.feature @@ -225,12 +225,12 @@ Feature: quickmarks and bookmarks Scenario: Listing quickmarks When I run :quickmark-add http://localhost:(port)/data/numbers/20.txt twenty And I run :quickmark-add http://localhost:(port)/data/numbers/21.txt twentyone - And I open qute:bookmarks + And I open qute://bookmarks Then the page should contain the plaintext "twenty" And the page should contain the plaintext "twentyone" Scenario: Listing bookmarks When I open data/title.html in a new tab And I run :bookmark-add - And I open qute:bookmarks + And I open qute://bookmarks Then the page should contain the plaintext "Test title" diff --git a/tests/end2end/features/utilcmds.feature b/tests/end2end/features/utilcmds.feature index 5696de75d..7fd5952a8 100644 --- a/tests/end2end/features/utilcmds.feature +++ b/tests/end2end/features/utilcmds.feature @@ -145,7 +145,7 @@ Feature: Miscellaneous utility commands exposed to the user. And I run :message-info oldstuff And I run :repeat 20 message-info otherstuff And I run :message-info newstuff - And I open qute:log + And I open qute://log Then the page should contain the plaintext "newstuff" And the page should not contain the plaintext "oldstuff" diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py index d6697ec29..ed0222014 100644 --- a/tests/end2end/test_invocations.py +++ b/tests/end2end/test_invocations.py @@ -138,10 +138,10 @@ def test_misconfigured_user_dirs(request, httpbin, temp_basedir_env, def test_no_loglines(request, quteproc_new): - """Test qute:log with --loglines=0.""" + """Test qute://log with --loglines=0.""" quteproc_new.start(args=['--temp-basedir', '--loglines=0'] + _base_args(request.config)) - quteproc_new.open_path('qute:log') + quteproc_new.open_path('qute://log') assert quteproc_new.get_content() == 'Log output was disabled.' diff --git a/tests/unit/browser/webkit/network/test_networkreply.py b/tests/unit/browser/webkit/network/test_networkreply.py index 7a9c89393..7505dbc8c 100644 --- a/tests/unit/browser/webkit/network/test_networkreply.py +++ b/tests/unit/browser/webkit/network/test_networkreply.py @@ -91,3 +91,10 @@ def test_error_network_reply(qtbot, req): assert reply.readData(1) == b'' assert reply.error() == QNetworkReply.UnknownNetworkError assert reply.errorString() == "This is an error" + + +def test_redirect_network_reply(): + url = QUrl('https://www.example.com/') + reply = networkreply.RedirectNetworkReply(url) + assert reply.readData(1) == b'' + assert reply.attribute(QNetworkRequest.RedirectionTargetAttribute) == url diff --git a/tests/unit/utils/test_urlutils.py b/tests/unit/utils/test_urlutils.py index f944f95b2..4729bd661 100644 --- a/tests/unit/utils/test_urlutils.py +++ b/tests/unit/utils/test_urlutils.py @@ -265,6 +265,7 @@ class TestFuzzyUrl: ('file:///tmp/foo', True), ('about:blank', True), ('qute:version', True), + ('qute://version', True), ('http://www.qutebrowser.org/', False), ('www.qutebrowser.org', False), ]) @@ -317,9 +318,11 @@ def test_get_search_url_invalid(urlutils_config_stub, url): (True, True, False, 'file:///tmp/foo'), (True, True, False, 'about:blank'), (True, True, False, 'qute:version'), + (True, True, False, 'qute://version'), (True, True, False, 'localhost'), # _has_explicit_scheme False, special_url True (True, True, False, 'qute::foo'), + (True, True, False, 'qute:://foo'), # Invalid URLs (False, False, False, ''), (False, True, False, 'onlyscheme:'), From 8878f867a7c8565801bc0187796e6638bfe02c85 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 6 Apr 2017 21:19:49 +0200 Subject: [PATCH 078/825] Update tox from 2.6.0 to 2.7.0 --- misc/requirements/requirements-tox.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt index df38c7fca..59f0806bf 100644 --- a/misc/requirements/requirements-tox.txt +++ b/misc/requirements/requirements-tox.txt @@ -2,5 +2,5 @@ pluggy==0.4.0 py==1.4.33 -tox==2.6.0 +tox==2.7.0 virtualenv==15.1.0 From 871504d91bc91351862f8cecddc3549eb25a024e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 6 Apr 2017 21:37:23 +0200 Subject: [PATCH 079/825] Fix undefined names --- qutebrowser/browser/qutescheme.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index 5f25c24d1..b35fb45f1 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -291,7 +291,7 @@ def qute_history(url): ) else: # Get current date from query parameter, if not given choose today. - curr_date = date.today() + curr_date = datetime.date.today() try: query_date = QUrlQuery(url).queryItemValue("date") if query_date: @@ -320,7 +320,7 @@ def qute_history(url): curr_date=curr_date, next_date=next_date, prev_date=prev_date, - today=date.today(), + today=datetime.date.today(), ) From fd9b86a34017eed5edde19a231f99c8b0b330a65 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 6 Apr 2017 21:40:26 +0200 Subject: [PATCH 080/825] Remove unused imports --- qutebrowser/browser/webkit/network/networkreply.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/webkit/network/networkreply.py b/qutebrowser/browser/webkit/network/networkreply.py index 9638daf4a..ec37e1e84 100644 --- a/qutebrowser/browser/webkit/network/networkreply.py +++ b/qutebrowser/browser/webkit/network/networkreply.py @@ -26,8 +26,8 @@ """Special network replies..""" -from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest, QNetworkAccessManager -from PyQt5.QtCore import pyqtSlot, QIODevice, QByteArray, QTimer, QUrl +from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest +from PyQt5.QtCore import pyqtSlot, QIODevice, QByteArray, QTimer class FixedDataNetworkReply(QNetworkReply): From 3b87e7c2972f24f4391b3c5f4c3ff9212f3bbade Mon Sep 17 00:00:00 2001 From: Jacob Sword Date: Fri, 7 Apr 2017 21:12:42 -0400 Subject: [PATCH 081/825] Add --debug-exit argument and validity check --- qutebrowser/qutebrowser.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index 8321fb04b..aaf86850f 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -118,6 +118,9 @@ def get_argparser(): action='append') debug.add_argument('--qt-flag', help="Pass an argument to Qt as flag.", nargs=1, action='append') + debug.add_argument('--debug-flag', type=debug_flag_error, + help="Pass name of debugging feature to be turned on.", + nargs=1, action='append') parser.add_argument('command', nargs='*', help="Commands to execute on " "startup.", metavar=':command') # URLs will actually be in command @@ -144,6 +147,21 @@ def logfilter_error(logfilter: str): "filters: Invalid value {} - expected a list of: {}".format( logfilter, ', '.join(log.LOGGER_NAMES))) +def debug_flag_error(flag): + """Validate flags passed to --debug-flag. + + Available flags: + debug-exit + pdb-postmortem + """ + valid_flags = ['debug-exit','pdb-postmortem'] + + if flag in valid_flags: + return flag + else: + raise argparse.ArgumentTypeError("Invalid flag - valid flags include: " + + str(valid_flags)) + def main(): parser = get_argparser() From 043039d673e9435d80034a80dcfe389f26d2dd06 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sat, 8 Apr 2017 05:19:34 +0200 Subject: [PATCH 082/825] Update setuptools from 34.3.3 to 34.4.0 --- misc/requirements/requirements-pip.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-pip.txt b/misc/requirements/requirements-pip.txt index 54c6747af..90c042c8e 100644 --- a/misc/requirements/requirements-pip.txt +++ b/misc/requirements/requirements-pip.txt @@ -3,6 +3,6 @@ appdirs==1.4.3 packaging==16.8 pyparsing==2.2.0 -setuptools==34.3.3 +setuptools==34.4.0 six==1.10.0 wheel==0.29.0 From c23e4b1c5f7f8979002ae66c82cd76e05d656ef5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 8 Apr 2017 19:20:53 +0200 Subject: [PATCH 083/825] tests: Allow @qt<... marker for BDD tests --- tests/end2end/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/end2end/conftest.py b/tests/end2end/conftest.py index 5dcff3fb5..7d1c0e90d 100644 --- a/tests/end2end/conftest.py +++ b/tests/end2end/conftest.py @@ -68,7 +68,7 @@ def _get_version_tag(tag): """ version_re = re.compile(r""" (?Pqt|pyqt) - (?P==|>=|!=) + (?P==|>=|!=|<) (?P\d+\.\d+(\.\d+)?) """, re.VERBOSE) @@ -84,6 +84,7 @@ def _get_version_tag(tag): do_skip = { '==': not qtutils.version_check(version, exact=True), '>=': not qtutils.version_check(version), + '<': qtutils.version_check(version), '!=': qtutils.version_check(version, exact=True), } return pytest.mark.skipif(do_skip[op], reason='Needs ' + tag) From a081d4184d45a643be0774ce25e618822d320c7c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 8 Apr 2017 19:22:00 +0200 Subject: [PATCH 084/825] tests: Adjust percent-encoding tests for Qt 5.9 changes See #2514 --- tests/end2end/features/downloads.feature | 12 ++++++++++-- .../browser/webengine/test_webenginedownloads.py | 12 ++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/tests/end2end/features/downloads.feature b/tests/end2end/features/downloads.feature index 768521183..b1dd5155b 100644 --- a/tests/end2end/features/downloads.feature +++ b/tests/end2end/features/downloads.feature @@ -121,14 +121,22 @@ Feature: Downloading things from a website. And I wait until the download is finished Then the downloaded file download with spaces.bin should exist - @qtwebkit_skip - Scenario: Downloading a file with evil content-disposition header + @qtwebkit_skip @qt<5.9 + Scenario: Downloading a file with evil content-disposition header (Qt 5.8 or older) # Content-Disposition: download; filename=..%2Ffoo When I open response-headers?Content-Disposition=download;%20filename%3D..%252Ffoo without waiting And I wait until the download is finished Then the downloaded file ../foo should not exist And the downloaded file foo should exist + @qtwebkit_skip @qt>=5.9 + Scenario: Downloading a file with evil content-disposition header (Qt 5.9 or newer) + # Content-Disposition: download; filename=..%2Ffoo + When I open response-headers?Content-Disposition=download;%20filename%3D..%252Ffoo without waiting + And I wait until the download is finished + Then the downloaded file ../foo should not exist + And the downloaded file ..%2Ffoo should exist + @windows Scenario: Downloading a file to a reserved path When I set storage -> prompt-download-directory to true diff --git a/tests/unit/browser/webengine/test_webenginedownloads.py b/tests/unit/browser/webengine/test_webenginedownloads.py index ee5b8e22a..735852cd3 100644 --- a/tests/unit/browser/webengine/test_webenginedownloads.py +++ b/tests/unit/browser/webengine/test_webenginedownloads.py @@ -24,6 +24,12 @@ import pytest pytest.importorskip('PyQt5.QtWebEngineWidgets') from qutebrowser.browser.webengine import webenginedownloads +from qutebrowser.utils import qtutils + +qt58 = pytest.mark.skipif( + qtutils.version_check('5.9'), reason="Needs Qt 5.8 or earlier") +qt59 = pytest.mark.skipif( + not qtutils.version_check('5.9'), reason="Needs Qt 5.9 or newer") @pytest.mark.parametrize('path, expected', [ @@ -31,8 +37,10 @@ from qutebrowser.browser.webengine import webenginedownloads ('foo(1)', 'foo'), ('foo(a)', 'foo(a)'), ('foo1', 'foo1'), - ('foo%20bar', 'foo bar'), - ('foo%2Fbar', 'bar'), + qt58(('foo%20bar', 'foo bar')), + qt58(('foo%2Fbar', 'bar')), + qt59(('foo%20bar', 'foo%20bar')), + qt59(('foo%2Fbar', 'foo%2Fbar')), ]) def test_get_suggested_filename(path, expected): assert webenginedownloads._get_suggested_filename(path) == expected From e23318fe914ff31c7f0810736905a9318d2a5e50 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 8 Apr 2017 19:29:46 +0200 Subject: [PATCH 085/825] Mark failing test as flaky on QtWebEngine It consistently fails on Qt 5.9 now while waiting the page to be scrolled to 0/20, but I can't figure out why that is happening. See #2514, #2410 --- tests/end2end/features/marks.feature | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/end2end/features/marks.feature b/tests/end2end/features/marks.feature index 28de753c9..31ddd034d 100644 --- a/tests/end2end/features/marks.feature +++ b/tests/end2end/features/marks.feature @@ -27,6 +27,7 @@ Feature: Setting positional marks And I wait until the scroll position changed to 10/20 Then the page should be scrolled to 10 20 + @qtwebengine_flaky Scenario: Setting the same local mark on another page When I run :scroll-px 5 10 And I wait until the scroll position changed to 5/10 From 6051f93c025cf8c49cb32fec63d5594687d344a2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 8 Apr 2017 19:52:46 +0200 Subject: [PATCH 086/825] Avoid checking for scroll position in macro tests This makes them simpler and more stable. --- tests/end2end/features/keyinput.feature | 51 ++++++++++--------------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/tests/end2end/features/keyinput.feature b/tests/end2end/features/keyinput.feature index 452d66757..93fbb1b93 100644 --- a/tests/end2end/features/keyinput.feature +++ b/tests/end2end/features/keyinput.feature @@ -202,34 +202,30 @@ Feature: Keyboard input # Macros Scenario: Recording a simple macro - Given I open data/scroll/simple.html - And I run :tab-only - When I run :scroll down with count 6 - And I wait until the scroll position changed - And I run :record-macro + When I run :record-macro And I press the key "a" - And I run :scroll up - And I run :scroll up - And I wait until the scroll position changed + And I run :message-info "foo 1" + And I run :message-info "bar 1" And I run :record-macro And I run :run-macro with count 2 And I press the key "a" - And I wait until the scroll position changed to 0/0 - Then the page should not be scrolled + Then the message "foo 1" should be shown + And the message "bar 1" should be shown + And the message "foo 1" should be shown + And the message "bar 1" should be shown + And the message "foo 1" should be shown + And the message "bar 1" should be shown Scenario: Recording a named macro - Given I open data/scroll/simple.html - And I run :tab-only - When I run :scroll down with count 6 - And I wait until the scroll position changed + When I run :record-macro foo + And I run :message-info "foo 2" + And I run :message-info "bar 2" And I run :record-macro foo - And I run :scroll up - And I run :scroll up - And I wait until the scroll position changed - And I run :record-macro foo - And I run :run-macro foo with count 2 - And I wait until the scroll position changed to 0/0 - Then the page should not be scrolled + And I run :run-macro foo + Then the message "foo 2" should be shown + And the message "bar 2" should be shown + And the message "foo 2" should be shown + And the message "bar 2" should be shown Scenario: Running an invalid macro Given I open data/scroll/simple.html @@ -264,17 +260,12 @@ Feature: Keyboard input Then "Leaving mode KeyMode.record_macro (reason: leave current)" should be logged Scenario: Ignoring non-register keys - Given I open data/scroll/simple.html - And I run :tab-only - When I run :scroll down with count 2 - And I wait until the scroll position changed - And I run :record-macro + When I run :record-macro And I press the key "" And I press the key "c" - And I run :scroll up - And I wait until the scroll position changed + And I run :message-info "foo 3" And I run :record-macro And I run :run-macro And I press the key "c" - And I wait until the scroll position changed to 0/0 - Then the page should not be scrolled + Then the message "foo 3" should be shown + And the message "foo 3" should be shown From 28e6158a044804d691a4448175d0460983e9e16a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 8 Apr 2017 20:38:23 +0200 Subject: [PATCH 087/825] Stabilize some tests with Qt 5.9 QtWebEngine --- tests/end2end/features/misc.feature | 8 ++++++-- tests/end2end/features/scroll.feature | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index e3bb13b6f..73950fc41 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -513,11 +513,15 @@ Feature: Various utility commands. Then the error "Invalid log level cataclysmic!" should be shown Scenario: Using qute://log directly - When I open qute://log + When I open qute://log without waiting + # With Qt 5.9, we don't get a loaded message? + And I wait for "Changing title for idx * to 'log'" in the log Then no crash should happen Scenario: Using qute://plainlog directly - When I open qute://plainlog + When I open qute://plainlog without waiting + # With Qt 5.9, we don't get a loaded message? + And I wait for "Changing title for idx * to 'log'" in the log Then no crash should happen Scenario: Using :messages without messages diff --git a/tests/end2end/features/scroll.feature b/tests/end2end/features/scroll.feature index 44f60aa66..4e2643b6d 100644 --- a/tests/end2end/features/scroll.feature +++ b/tests/end2end/features/scroll.feature @@ -39,6 +39,7 @@ Feature: Scrolling And I wait until the scroll position changed to 0/0 Then the page should not be scrolled + @qtwebengine_flaky Scenario: Scrolling left and right with count When I run :scroll-px 10 0 with count 2 And I wait until the scroll position changed to 20/0 @@ -146,7 +147,6 @@ Feature: Scrolling Scenario: Scrolling down with a very big count When I run :scroll down with count 99999999999 - And I wait until the scroll position changed # Make sure it doesn't hang And I run :message-info "Still alive!" Then the message "Still alive!" should be shown From 45dff6c0c81effecb05e6d2a88096d357a0bc736 Mon Sep 17 00:00:00 2001 From: Jacob Sword Date: Sat, 8 Apr 2017 16:54:08 -0400 Subject: [PATCH 088/825] update usage of debug-flag arguments --- qutebrowser/app.py | 2 +- qutebrowser/misc/crashsignal.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 81079104a..086c67528 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -792,7 +792,7 @@ class Application(QApplication): def exit(self, status): """Extend QApplication::exit to log the event.""" log.destroy.debug("Now calling QApplication::exit.") - if self._args.debug_exit: + if 'debug_exit' in self._args.debug_flags: if hunter is None: print("Not logging late shutdown because hunter could not be " "imported!", file=sys.stderr) diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py index aa7438732..d8d8bb385 100644 --- a/qutebrowser/misc/crashsignal.py +++ b/qutebrowser/misc/crashsignal.py @@ -207,10 +207,10 @@ class CrashHandler(QObject): is_ignored_exception = (exctype is bdb.BdbQuit or not issubclass(exctype, Exception)) - if self._args.pdb_postmortem: + if 'pdb-postmortem' in self._args.debug_flags: pdb.post_mortem(tb) - if is_ignored_exception or self._args.pdb_postmortem: + if is_ignored_exception or 'pdb-postmortem' in self._args.debug_flags: # pdb exit, KeyboardInterrupt, ... status = 0 if is_ignored_exception else 2 try: From 778832a8131f80d1dc4e1aebdb7e39faf31968ad Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 8 Apr 2017 23:04:10 +0200 Subject: [PATCH 089/825] Set path when redirecting qute:* URLs Fixes #2513 --- qutebrowser/browser/qutescheme.py | 1 + tests/end2end/features/misc.feature | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index b35fb45f1..ccd976d0a 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -145,6 +145,7 @@ def data_for_url(url): new_url = QUrl() new_url.setScheme('qute') new_url.setHost(path) + new_url.setPath('/') raise Redirect(new_url) try: diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index 73950fc41..52c56b5c1 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -308,6 +308,14 @@ Feature: Various utility commands. - about:blank - qute://help/index.html (active) + # https://github.com/qutebrowser/qutebrowser/issues/2513 + Scenario: Opening link with qute:help + When I run :tab-only + And I open qute:help without waiting + And I wait for "Changing title for idx 0 to 'qutebrowser help'" in the log + And I hint with args "links normal" and follow a + Then qute://help/quickstart.html should be loaded + # :history Scenario: :history without arguments From 6ccb42023050287f7b16404556e2dc07386cebf5 Mon Sep 17 00:00:00 2001 From: Jacob Sword Date: Sat, 8 Apr 2017 18:42:26 -0400 Subject: [PATCH 090/825] Fix syntax error in debug-exit --- qutebrowser/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 086c67528..8ca322078 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -792,7 +792,7 @@ class Application(QApplication): def exit(self, status): """Extend QApplication::exit to log the event.""" log.destroy.debug("Now calling QApplication::exit.") - if 'debug_exit' in self._args.debug_flags: + if 'debug-exit' in self._args.debug_flags: if hunter is None: print("Not logging late shutdown because hunter could not be " "imported!", file=sys.stderr) From 7588cdb1856d7cada79f4dbbe99f6495838f1a8d Mon Sep 17 00:00:00 2001 From: Jacob Sword Date: Sat, 8 Apr 2017 19:04:25 -0400 Subject: [PATCH 091/825] fixed formatting issues --- qutebrowser/qutebrowser.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index aaf86850f..4019b9650 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -118,9 +118,9 @@ def get_argparser(): action='append') debug.add_argument('--qt-flag', help="Pass an argument to Qt as flag.", nargs=1, action='append') - debug.add_argument('--debug-flag', type=debug_flag_error, - help="Pass name of debugging feature to be turned on.", - nargs=1, action='append') + debug.add_argument('--debug-flag', type=debug_flag_error, + help="Pass name of debugging feature to be turned on.", + nargs=1, action='append') parser.add_argument('command', nargs='*', help="Commands to execute on " "startup.", metavar=':command') # URLs will actually be in command @@ -147,21 +147,22 @@ def logfilter_error(logfilter: str): "filters: Invalid value {} - expected a list of: {}".format( logfilter, ', '.join(log.LOGGER_NAMES))) + def debug_flag_error(flag): """Validate flags passed to --debug-flag. - Available flags: - debug-exit - pdb-postmortem - """ - valid_flags = ['debug-exit','pdb-postmortem'] + Available flags: + debug-exit + pdb-postmortem + """ + valid_flags = ['debug-exit', 'pdb-postmortem'] - if flag in valid_flags: - return flag + if flag in valid_flags: + return flag else: - raise argparse.ArgumentTypeError("Invalid flag - valid flags include: " + - str(valid_flags)) - + raise argparse.ArgumentTypeError("Invalid flag - valid flags include: " + + str(valid_flags)) + def main(): parser = get_argparser() From fc37223d1b3b0430c38f09c88e0a279212a762ee Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 9 Apr 2017 11:36:13 +0200 Subject: [PATCH 092/825] Regenerate docs properly for qute:help test --- tests/end2end/features/misc.feature | 3 ++- tests/end2end/features/test_misc_bdd.py | 10 ++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index 52c56b5c1..83de1c822 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -310,7 +310,8 @@ Feature: Various utility commands. # https://github.com/qutebrowser/qutebrowser/issues/2513 Scenario: Opening link with qute:help - When I run :tab-only + When the documentation is up to date + And I run :tab-only And I open qute:help without waiting And I wait for "Changing title for idx 0 to 'qutebrowser help'" in the log And I hint with args "links normal" and follow a diff --git a/tests/end2end/features/test_misc_bdd.py b/tests/end2end/features/test_misc_bdd.py index 177f7f383..9f7ce63e6 100644 --- a/tests/end2end/features/test_misc_bdd.py +++ b/tests/end2end/features/test_misc_bdd.py @@ -38,11 +38,13 @@ def update_documentation(): doc_path = os.path.join(base_path, 'html', 'doc') script_path = os.path.join(base_path, '..', 'scripts') - if not os.path.exists(doc_path): - # On CI, we can test this without actually building the docs - return + try: + os.mkdir(doc_path) + except FileExistsError: + pass - if all(docutils.docs_up_to_date(p) for p in os.listdir(doc_path)): + files = os.listdir(doc_path) + if files and all(docutils.docs_up_to_date(p) for p in files): return try: From c0ac1bd79a82d6abeaa8b0fa38035f89f9993768 Mon Sep 17 00:00:00 2001 From: Jacob Sword Date: Sun, 9 Apr 2017 10:34:51 -0500 Subject: [PATCH 093/825] Add 'dest' for '--debug-flag' --- qutebrowser/qutebrowser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index 4019b9650..e318cc2e4 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -120,7 +120,7 @@ def get_argparser(): nargs=1, action='append') debug.add_argument('--debug-flag', type=debug_flag_error, help="Pass name of debugging feature to be turned on.", - nargs=1, action='append') + nargs=1, action='append', dest='debug_flags') parser.add_argument('command', nargs='*', help="Commands to execute on " "startup.", metavar=':command') # URLs will actually be in command From dcf8f29a6765fea0f94f580dfa37067431a0e1a5 Mon Sep 17 00:00:00 2001 From: Jacob Sword Date: Sun, 9 Apr 2017 10:43:40 -0500 Subject: [PATCH 094/825] Remove old --pdb-postmortem and --debug-exit flags --- qutebrowser/qutebrowser.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index e318cc2e4..98bb681ef 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -103,10 +103,6 @@ def get_argparser(): help="Silently remove unknown config options.") debug.add_argument('--nowindow', action='store_true', help="Don't show " "the main window.") - debug.add_argument('--debug-exit', help="Turn on debugging of late exit.", - action='store_true') - debug.add_argument('--pdb-postmortem', action='store_true', - help="Drop into pdb on exceptions.") debug.add_argument('--temp-basedir', action='store_true', help="Use a " "temporary basedir.") debug.add_argument('--no-err-windows', action='store_true', help="Don't " @@ -152,8 +148,8 @@ def debug_flag_error(flag): """Validate flags passed to --debug-flag. Available flags: - debug-exit - pdb-postmortem + debug-exit: Turn on debugging of late exit. + pdb-postmortem: Drop into pdb on exceptions. """ valid_flags = ['debug-exit', 'pdb-postmortem'] From f31aead992e829cb15c4fbedbf816a23d2a916a7 Mon Sep 17 00:00:00 2001 From: Jacob Sword Date: Sun, 9 Apr 2017 23:34:33 -0400 Subject: [PATCH 095/825] Add default to --debug-flag --- qutebrowser/qutebrowser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index 98bb681ef..3ce11b07b 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -114,7 +114,7 @@ def get_argparser(): action='append') debug.add_argument('--qt-flag', help="Pass an argument to Qt as flag.", nargs=1, action='append') - debug.add_argument('--debug-flag', type=debug_flag_error, + debug.add_argument('--debug-flag', type=debug_flag_error, default=[], help="Pass name of debugging feature to be turned on.", nargs=1, action='append', dest='debug_flags') parser.add_argument('command', nargs='*', help="Commands to execute on " From b1e3add02e952387bb6e79a83504bdaa0784671a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 10 Apr 2017 08:47:43 +0200 Subject: [PATCH 096/825] Update screenshots --- doc/img/completion.png | Bin 14282 -> 48272 bytes doc/img/downloads.png | Bin 26987 -> 46490 bytes doc/img/hints.png | Bin 32318 -> 59935 bytes doc/img/main.png | Bin 27002 -> 46597 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/img/completion.png b/doc/img/completion.png index 9ba30877e74c6cb1e47475081bbbdee409dd95f0..84a49828ff3e89116c07933e937d5605c2dc1b31 100644 GIT binary patch literal 48272 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 LG^9-Eo6r9N-Ud6C literal 14282 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 W_V|+|>7C>BpS3kks6SUd>-!%;5xk`U literal 26987 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 uCvo zUTwrXgA@p8!YF_!sAPDpKYOLSXr#v7gYMk;8#* zFV{k)LPKm(U00sOlI@G-$xHSU65avy=4NJv z#Y8cjS>gtFRPBl?1lC3)y04+<(iGTXmE+hNE2Towbo}~4Q~k;ISs$p8S}^r-ECDb@ zM+OIl%1h})ts91hJdd{A$N%+b^8SA3T>rTwXASg5g<+$<-yPzrfcotP&1|%A8G>D^ zM5o$S%lFC^{HNRCIFjLciu67f0xsP$ImO9odtfXoE^cjUX(S81n*$evw)|+%2RW7CZg5GyAuuuAwdm{-hAH(gpDtTLb8T^cE}6t^3;-Ms2IZgFV3@jC`& zl=Z3pXWidxW-%dhj*lq0G)iYjikKOy!r%ci8_VCXsd5>^ItU)0v8~KndeO^aByj^* zsw!o@u^c$te?fEn9wW{+3W>PlIQ_GlbFt9wkn@c@tb3s1G!;rbDcTrl`aG8BGBQ5l ziksuep|boDa#O&Se1Tq(m^cuY?Xqoa7DS1b1>dEeaTUUa#XbTwIVqbSW}o|t=%t^`#`m_hc3|Dj~6IgG>+@y{q)6C!}V<7e#B z6cN(7c3Kn(WuA%?|3S}%`$f2+UnIPQILh$jC7!eH55cOWsyZbDpZ0)@1iqUAzL`LR zT$jp=>q9(T!Y{g64Z9o+#;N@RoUi7Pq_Rc4Xgqqt=v3TT7yPah;VbiKp9 zX3|)ewM(PefY!rt^GMnCfcYUMloap_=BCC*fnxnr_PX?)zJ!DqXhb02aBHClw13pZ z(Mu}9Xd^5L_GH3&-F;4tj`~f61p_(iD*!ah#y(J$m;Y;Wk{4cw-1XCwL2PD;agQNn z?t*yWr&;3`T*P~dztY7X9Cs-7+E$Z1&CG*FvtHC8)LTek4Pq=gQ) zqc|Z`>U^h;WrU$al=hQNQ?mI(U)A!UF4D{GidTR^eXwJUefT!fnANk3l7)~7|MNxm zJHNt|jC%1UKTpY@tG&W4UZvF>ndpf=a z1-<>uP|lVmxX4M%)AzkLC$JT=D}!SFden4BG#p@FydawE{Uuzy9-W#qPMcG~bWk7u zQeA(k3*9TTi5mRwey5v}KS0-mp8u#c(yA&vHJ09F@!gStEA8{%v3rG-Hnw<<{Zx@# zckMqME6~5vU-OTA+3+JS&`pM_=VC4AoFCr=+vM&u9owCg^UV}c;m791AK!g8pXSsM zYZUHuhyL1wrlmiaGMe>F5xfU&;c2cPI_};X(;(YwDsFsmLBE@HZi($acYD{FNvBvb z6H)1uP`!gqy%(m+yPn})s-1!xh2LuC)zi1)rQ$|AEL>wNeRCQ_47 diff --git a/doc/img/hints.png b/doc/img/hints.png index 812af3581ca59f33a92f877cec6fbf0f76cc7384..3407ddf47058588a26606e289d56d8147eb4b265 100644 GIT binary patch literal 59935 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
+qutebrowser is currently running a crowdfunding campaign for its new configuration system, allowing for per-domain settings and much more. +See the Kickstarter campaign for more information! +
+++ diff --git a/www/qute.css b/www/qute.css index 2de633b18..9a47b8122 100644 --- a/www/qute.css +++ b/www/qute.css @@ -53,6 +53,12 @@ p { color: #666666; } +#crowdfunding { + padding: 10px 10px; + background-color: #a6dfff; + margin-bottom: 10px; +} + #menu { padding: 0px 20px; background-color: #555555; From b82aada50b11e438a2a4bc9259352a38a1c3ce93 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 18 Apr 2017 16:38:36 +0200 Subject: [PATCH 129/825] Fix highlights in crowdfunding note --- README.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.asciidoc b/README.asciidoc index d9dbd8c0c..fdb82b4d2 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -26,9 +26,9 @@ It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl. // QUTE_WEB_HIDE **qutebrowser is currently running a crowdfunding campaign for its new -configuration system, allowing for per-domain settings and much more. +configuration system, allowing for per-domain settings and much more.** -See the link:https://www.kickstarter.com/projects/the-compiler/qutebrowser-v10-with-per-domain-settings?ref=6zw7qz[Kickstarter campaign] for more information!** +See the link:https://www.kickstarter.com/projects/the-compiler/qutebrowser-v10-with-per-domain-settings?ref=6zw7qz[Kickstarter campaign] for more information! // QUTE_WEB_HIDE_END Screenshots From 0c96a32366c568fe984ed57721b1bdefed4a20e9 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 19 Apr 2017 01:52:32 +0200 Subject: [PATCH 130/825] Update setuptools from 35.0.0 to 35.0.1 --- misc/requirements/requirements-pip.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-pip.txt b/misc/requirements/requirements-pip.txt index 9979e87f2..b015ca518 100644 --- a/misc/requirements/requirements-pip.txt +++ b/misc/requirements/requirements-pip.txt @@ -3,6 +3,6 @@ appdirs==1.4.3 packaging==16.8 pyparsing==2.2.0 -setuptools==35.0.0 +setuptools==35.0.1 six==1.10.0 wheel==0.29.0 From 0b118f4fd83195e24b3e658c6f919db8718dd820 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 20 Apr 2017 04:35:10 +0200 Subject: [PATCH 131/825] Blacklist pydocstyle >= 2.0.0 Closes #2539 --- misc/requirements/requirements-flake8.txt | 2 +- misc/requirements/requirements-flake8.txt-raw | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt index 9c0aad110..d30cb2dd9 100644 --- a/misc/requirements/requirements-flake8.txt +++ b/misc/requirements/requirements-flake8.txt @@ -16,5 +16,5 @@ flake8-tuple==0.2.12 mccabe==0.6.1 pep8-naming==0.4.1 pycodestyle==2.3.1 -pydocstyle==1.1.1 +pydocstyle==1.1.1 # rq.filter: < 2.0.0 pyflakes==1.5.0 diff --git a/misc/requirements/requirements-flake8.txt-raw b/misc/requirements/requirements-flake8.txt-raw index cf660a8ce..e235df395 100644 --- a/misc/requirements/requirements-flake8.txt-raw +++ b/misc/requirements/requirements-flake8.txt-raw @@ -11,7 +11,7 @@ flake8-string-format flake8-tidy-imports flake8-tuple pep8-naming -pydocstyle +pydocstyle<2.0.0 pyflakes # Pinned to 2.0.0 otherwise @@ -21,6 +21,7 @@ mccabe==0.6.1 # Waiting until flake8-putty updated #@ filter: flake8 < 3.0.0 +#@ filter: pydocstyle < 2.0.0 # https://github.com/JBKahn/flake8-debugger/issues/5 #@ filter: flake8-debugger != 2.0.0 From 1ebe0f2ce88927264f5f56dc66f2422df59d069d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 20 Apr 2017 04:40:03 +0200 Subject: [PATCH 132/825] Regenerate docs --- README.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/README.asciidoc b/README.asciidoc index fdb82b4d2..7969d794b 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -292,6 +292,7 @@ Contributors, sorted by the number of commits in descending order: * Dietrich Daroch * Derek Sivers * Daniel Lu +* Daniel Jakots * Arseniy Seroka * Andy Balaam * Andreas Fischer From 616a764b6dc048053bb46919c8ea055e19e67809 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Fri, 21 Apr 2017 20:00:01 +0200 Subject: [PATCH 133/825] Update hypothesis from 3.7.0 to 3.7.3 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 0187026ce..9805a1931 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -9,7 +9,7 @@ EasyProcess==0.2.3 Flask==0.12.1 glob2==0.5 httpbin==0.5.0 -hypothesis==3.7.0 +hypothesis==3.7.3 itsdangerous==0.24 # Jinja2==2.9.5 Mako==1.0.6 From 02cccb3673e7880fb1c507ca0f3e12e3ed954d6e Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sun, 23 Apr 2017 11:48:10 +0200 Subject: [PATCH 134/825] Update colorama from 0.3.7 to 0.3.8 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5b4053a33..57ed78734 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -colorama==0.3.7 +colorama==0.3.8 cssutils==1.0.2 Jinja2==2.9.6 MarkupSafe==1.0 From 4220cfc34e85c10fc810a779a37eb1f3871e2ff4 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sun, 23 Apr 2017 16:45:10 +0200 Subject: [PATCH 135/825] Update hypothesis from 3.7.3 to 3.8.0 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 9805a1931..d5b0a0335 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -9,7 +9,7 @@ EasyProcess==0.2.3 Flask==0.12.1 glob2==0.5 httpbin==0.5.0 -hypothesis==3.7.3 +hypothesis==3.8.0 itsdangerous==0.24 # Jinja2==2.9.5 Mako==1.0.6 From a1de313aa3c11aff44041110a3efbb3237eeb799 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 23 Apr 2017 23:10:29 +0200 Subject: [PATCH 136/825] Add qapp to test_proxy_from_url_pac --- tests/unit/utils/test_urlutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/utils/test_urlutils.py b/tests/unit/utils/test_urlutils.py index 4729bd661..6649873e9 100644 --- a/tests/unit/utils/test_urlutils.py +++ b/tests/unit/utils/test_urlutils.py @@ -765,7 +765,7 @@ class TestProxyFromUrl: assert urlutils.proxy_from_url(QUrl(url)) == expected @pytest.mark.parametrize('scheme', ['pac+http', 'pac+https']) - def test_proxy_from_url_pac(self, scheme): + def test_proxy_from_url_pac(self, scheme, qapp): fetcher = urlutils.proxy_from_url(QUrl('{}://foo'.format(scheme))) assert isinstance(fetcher, pac.PACFetcher) From beb661cdc713da873dd4a42d7b93b40bd9ab80b1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 23 Apr 2017 23:11:12 +0200 Subject: [PATCH 137/825] Add xos4 Terminus to default monospace fonts --- doc/help/settings.asciidoc | 2 +- qutebrowser/config/configdata.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 9092ea052..c6a80754c 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -2267,7 +2267,7 @@ Fonts used for the UI, with optional style/weight/size. === _monospace Default monospace fonts. -Default: +pass:[Terminus, Monospace, "DejaVu Sans Mono", Monaco, "Bitstream Vera Sans Mono", "Andale Mono", "Courier New", Courier, "Liberation Mono", monospace, Fixed, Consolas, Terminal]+ +Default: +pass:[xos4 Terminus, Terminus, Monospace, "DejaVu Sans Mono", Monaco, "Bitstream Vera Sans Mono", "Andale Mono", "Courier New", Courier, "Liberation Mono", monospace, Fixed, Consolas, Terminal]+ [[fonts-completion]] === completion diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index abf704801..2bfcabce7 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1359,7 +1359,7 @@ def data(readonly=False): ('fonts', sect.KeyValue( ('_monospace', - SettingValue(typ.Font(), 'Terminus, Monospace, ' + SettingValue(typ.Font(), 'xos4 Terminus, Terminus, Monospace, ' '"DejaVu Sans Mono", Monaco, ' '"Bitstream Vera Sans Mono", "Andale Mono", ' '"Courier New", Courier, "Liberation Mono", ' From 195d0ea2074cd559c6f4b29cccff9022dbbe7658 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 24 Apr 2017 06:58:41 +0200 Subject: [PATCH 138/825] Show Punycode URL for IDN pages in addition to decoded one This helps when Unicode homographs are used for phishing purposes. Fixes #2547 --- CHANGELOG.asciidoc | 2 + qutebrowser/mainwindow/statusbar/url.py | 13 +++-- qutebrowser/utils/urlutils.py | 24 ++++++++ tests/unit/mainwindow/statusbar/test_url.py | 65 +++++++++------------ tests/unit/utils/test_urlutils.py | 25 ++++++++ 5 files changed, 88 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 983f8a6d3..6e4c8c372 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -32,6 +32,8 @@ Added Changed ~~~~~~~ +- To prevent elaborate phishing attacks, the Punycode version is now shown in + addition to the decoded version for IDN domain names. - When using QtWebEngine, the underlying Chromium version is now shown in the version info. - Improved `qute:history` page with lazy loading diff --git a/qutebrowser/mainwindow/statusbar/url.py b/qutebrowser/mainwindow/statusbar/url.py index bca1f0458..817953d9d 100644 --- a/qutebrowser/mainwindow/statusbar/url.py +++ b/qutebrowser/mainwindow/statusbar/url.py @@ -24,7 +24,7 @@ from PyQt5.QtCore import pyqtSlot, pyqtProperty, Qt, QUrl from qutebrowser.browser import browsertab from qutebrowser.mainwindow.statusbar import textbase from qutebrowser.config import style -from qutebrowser.utils import usertypes +from qutebrowser.utils import usertypes, urlutils # Note this has entries for success/error/warn from widgets.webview:LoadStatus @@ -139,7 +139,7 @@ class UrlText(textbase.TextBase): if url is None: self._normal_url = None else: - self._normal_url = url.toDisplayString() + self._normal_url = urlutils.safe_display_url(url) self._normal_url_type = UrlType.normal self._update_url() @@ -156,9 +156,9 @@ class UrlText(textbase.TextBase): if link: qurl = QUrl(link) if qurl.isValid(): - self._hover_url = qurl.toDisplayString() + self._hover_url = urlutils.safe_display_url(qurl) else: - self._hover_url = link + self._hover_url = '(invalid URL!) {}'.format(link) else: self._hover_url = None self._update_url() @@ -167,6 +167,9 @@ class UrlText(textbase.TextBase): def on_tab_changed(self, tab): """Update URL if the tab changed.""" self._hover_url = None - self._normal_url = tab.url().toDisplayString() + if tab.url().isValid(): + self._normal_url = urlutils.safe_display_url(tab.url()) + else: + self._normal_url = '' self.on_load_status_changed(tab.load_status().name) self._update_url() diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 1beebbe92..376d46a42 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -592,6 +592,30 @@ def data_url(mimetype, data): return url +def safe_display_url(qurl): + """Get a IDN-homograph phising safe form of the given QUrl. + + If we're dealing with a Punycode-encoded URL, this prepends the hostname in + its encoded form, to make sure those URLs are distinguishable. + + See https://github.com/qutebrowser/qutebrowser/issues/2547 + and https://bugreports.qt.io/browse/QTBUG-60365 + """ + if not qurl.isValid(): + raise InvalidUrlError(qurl) + + host = qurl.host(QUrl.FullyEncoded) + if '..' in host: + # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-60364 + return '(unparseable URL!) {}'.format(qurl.toDisplayString()) + + for part in host.split('.'): + if part.startswith('xn--') and host != qurl.host(QUrl.FullyDecoded): + return '({}) {}'.format(host, qurl.toDisplayString()) + + return qurl.toDisplayString() + + class InvalidProxyTypeError(Exception): """Error raised when proxy_from_url gets an unknown proxy type.""" diff --git a/tests/unit/mainwindow/statusbar/test_url.py b/tests/unit/mainwindow/statusbar/test_url.py index 1fe201bff..97f09840d 100644 --- a/tests/unit/mainwindow/statusbar/test_url.py +++ b/tests/unit/mainwindow/statusbar/test_url.py @@ -22,7 +22,7 @@ import pytest -from qutebrowser.utils import usertypes +from qutebrowser.utils import usertypes, urlutils from qutebrowser.mainwindow.statusbar import url from PyQt5.QtCore import QUrl @@ -51,49 +51,41 @@ def url_widget(qtbot, monkeypatch, config_stub): return widget -@pytest.mark.parametrize('qurl', [ - QUrl('http://abc123.com/this/awesome/url.html'), - QUrl('https://supersecret.gov/nsa/files.txt'), - None -]) -def test_set_url(url_widget, qurl): - """Test text displayed by the widget.""" - url_widget.set_url(qurl) - if qurl is not None: - assert url_widget.text() == qurl.toDisplayString() - else: - assert url_widget.text() == "" - - -@pytest.mark.parametrize('url_text', [ - 'http://abc123.com/this/awesome/url.html', - 'https://supersecret.gov/nsa/files.txt', - None, -]) -def test_set_hover_url(url_widget, url_text): - """Test text when hovering over a link.""" - url_widget.set_hover_url(url_text) - if url_text is not None: - assert url_widget.text() == url_text - assert url_widget._urltype == url.UrlType.hover - else: - assert url_widget.text() == '' - assert url_widget._urltype == url.UrlType.normal - - @pytest.mark.parametrize('url_text, expected', [ + ('http://example.com/foo/bar.html', 'http://example.com/foo/bar.html'), ('http://test.gr/%CE%B1%CE%B2%CE%B3%CE%B4.txt', 'http://test.gr/αβγδ.txt'), ('http://test.ru/%D0%B0%D0%B1%D0%B2%D0%B3.txt', 'http://test.ru/абвг.txt'), ('http://test.com/s%20p%20a%20c%20e.txt', 'http://test.com/s p a c e.txt'), ('http://test.com/%22quotes%22.html', 'http://test.com/%22quotes%22.html'), ('http://username:secret%20password@test.com', 'http://username@test.com'), - ('http://example.com%5b/', 'http://example.com%5b/'), # invalid url + ('http://example.com%5b/', '(invalid URL!) http://example.com%5b/'), + # https://bugreports.qt.io/browse/QTBUG-60364 + ('http://www.xn--80ak6aa92e.com', + '(unparseable URL!) http://www.аррӏе.com'), + # IDN URL + ('http://www.ä.com', '(www.xn--4ca.com) http://www.ä.com'), + (None, ''), ]) -def test_set_hover_url_encoded(url_widget, url_text, expected): +@pytest.mark.parametrize('which', ['normal', 'hover']) +def test_set_url(url_widget, url_text, expected, which): """Test text when hovering over a percent encoded link.""" - url_widget.set_hover_url(url_text) + qurl = QUrl(url_text) + if which == 'normal': + if not qurl.isValid(): + with pytest.raises(urlutils.InvalidUrlError): + url_widget.set_url(qurl) + return + else: + url_widget.set_url(qurl) + else: + url_widget.set_hover_url(url_text) + assert url_widget.text() == expected - assert url_widget._urltype == url.UrlType.hover + + if which == 'hover' and expected: + assert url_widget._urltype == url.UrlType.hover + else: + assert url_widget._urltype == url.UrlType.normal @pytest.mark.parametrize('status, expected', [ @@ -114,6 +106,7 @@ def test_on_load_status_changed(url_widget, status, expected): @pytest.mark.parametrize('load_status, qurl', [ (url.UrlType.success, QUrl('http://abc123.com/this/awesome/url.html')), (url.UrlType.success, QUrl('http://reddit.com/r/linux')), + (url.UrlType.success, QUrl('http://ä.com/')), (url.UrlType.success_https, QUrl('www.google.com')), (url.UrlType.success_https, QUrl('https://supersecret.gov/nsa/files.txt')), (url.UrlType.warn, QUrl('www.shadysite.org/some/file/with/issues.htm')), @@ -123,7 +116,7 @@ def test_on_tab_changed(url_widget, fake_web_tab, load_status, qurl): tab_widget = fake_web_tab(load_status=load_status, url=qurl) url_widget.on_tab_changed(tab_widget) assert url_widget._urltype == load_status - assert url_widget.text() == qurl.toDisplayString() + assert url_widget.text() == urlutils.safe_display_url(qurl) @pytest.mark.parametrize('qurl, load_status, expected_status', [ diff --git a/tests/unit/utils/test_urlutils.py b/tests/unit/utils/test_urlutils.py index 6649873e9..8b0fb4e3b 100644 --- a/tests/unit/utils/test_urlutils.py +++ b/tests/unit/utils/test_urlutils.py @@ -742,6 +742,31 @@ def test_data_url(): assert url == QUrl('data:text/plain;base64,Zm9v') +@pytest.mark.parametrize('url, expected', [ + # No IDN + (QUrl('http://www.example.com'), 'http://www.example.com'), + # IDN in domain + (QUrl('http://www.ä.com'), '(www.xn--4ca.com) http://www.ä.com'), + # IDN with non-whitelisted TLD + (QUrl('http://www.ä.foo'), 'http://www.xn--4ca.foo'), + # Unicode only in path + (QUrl('http://www.example.com/ä'), 'http://www.example.com/ä'), + # Unicode only in TLD (looks like Qt shows Punycode with рф...) + (QUrl('http://www.example.xn--p1ai'), + '(www.example.xn--p1ai) http://www.example.рф'), + # https://bugreports.qt.io/browse/QTBUG-60364 + (QUrl('http://www.xn--80ak6aa92e.com'), + '(unparseable URL!) http://www.аррӏе.com'), +]) +def test_safe_display_url(url, expected): + assert urlutils.safe_display_url(url) == expected + + +def test_safe_display_url_invalid(): + with pytest.raises(urlutils.InvalidUrlError): + urlutils.safe_display_url(QUrl()) + + class TestProxyFromUrl: @pytest.mark.parametrize('url, expected', [ From b632fe3285728d3bce965e5ee40c0b045c741dba Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 24 Apr 2017 07:47:58 +0200 Subject: [PATCH 139/825] Fix invalid URL handling in statusbar --- qutebrowser/mainwindow/statusbar/url.py | 2 ++ tests/unit/mainwindow/statusbar/test_url.py | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/qutebrowser/mainwindow/statusbar/url.py b/qutebrowser/mainwindow/statusbar/url.py index 817953d9d..b9a9d86aa 100644 --- a/qutebrowser/mainwindow/statusbar/url.py +++ b/qutebrowser/mainwindow/statusbar/url.py @@ -138,6 +138,8 @@ class UrlText(textbase.TextBase): """ if url is None: self._normal_url = None + elif not url.isValid(): + self._normal_url = "Invalid URL!" else: self._normal_url = urlutils.safe_display_url(url) self._normal_url_type = UrlType.normal diff --git a/tests/unit/mainwindow/statusbar/test_url.py b/tests/unit/mainwindow/statusbar/test_url.py index 97f09840d..696b2be9f 100644 --- a/tests/unit/mainwindow/statusbar/test_url.py +++ b/tests/unit/mainwindow/statusbar/test_url.py @@ -71,12 +71,12 @@ def test_set_url(url_widget, url_text, expected, which): """Test text when hovering over a percent encoded link.""" qurl = QUrl(url_text) if which == 'normal': - if not qurl.isValid(): - with pytest.raises(urlutils.InvalidUrlError): - url_widget.set_url(qurl) + if url_text is None: return - else: - url_widget.set_url(qurl) + if not qurl.isValid(): + # Special case for the invalid URL above + expected = "Invalid URL!" + url_widget.set_url(qurl) else: url_widget.set_hover_url(url_text) From 52f31ed15c084b361eabe046eab35b521924d49f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 24 Apr 2017 07:49:12 +0200 Subject: [PATCH 140/825] Rename urlutils.safe_display_url to safe_display_string --- qutebrowser/mainwindow/statusbar/url.py | 6 +++--- qutebrowser/utils/urlutils.py | 2 +- tests/unit/mainwindow/statusbar/test_url.py | 2 +- tests/unit/utils/test_urlutils.py | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/qutebrowser/mainwindow/statusbar/url.py b/qutebrowser/mainwindow/statusbar/url.py index b9a9d86aa..c7bee2ae9 100644 --- a/qutebrowser/mainwindow/statusbar/url.py +++ b/qutebrowser/mainwindow/statusbar/url.py @@ -141,7 +141,7 @@ class UrlText(textbase.TextBase): elif not url.isValid(): self._normal_url = "Invalid URL!" else: - self._normal_url = urlutils.safe_display_url(url) + self._normal_url = urlutils.safe_display_string(url) self._normal_url_type = UrlType.normal self._update_url() @@ -158,7 +158,7 @@ class UrlText(textbase.TextBase): if link: qurl = QUrl(link) if qurl.isValid(): - self._hover_url = urlutils.safe_display_url(qurl) + self._hover_url = urlutils.safe_display_string(qurl) else: self._hover_url = '(invalid URL!) {}'.format(link) else: @@ -170,7 +170,7 @@ class UrlText(textbase.TextBase): """Update URL if the tab changed.""" self._hover_url = None if tab.url().isValid(): - self._normal_url = urlutils.safe_display_url(tab.url()) + self._normal_url = urlutils.safe_display_string(tab.url()) else: self._normal_url = '' self.on_load_status_changed(tab.load_status().name) diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 376d46a42..ac11c1fb5 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -592,7 +592,7 @@ def data_url(mimetype, data): return url -def safe_display_url(qurl): +def safe_display_string(qurl): """Get a IDN-homograph phising safe form of the given QUrl. If we're dealing with a Punycode-encoded URL, this prepends the hostname in diff --git a/tests/unit/mainwindow/statusbar/test_url.py b/tests/unit/mainwindow/statusbar/test_url.py index 696b2be9f..c104cb233 100644 --- a/tests/unit/mainwindow/statusbar/test_url.py +++ b/tests/unit/mainwindow/statusbar/test_url.py @@ -116,7 +116,7 @@ def test_on_tab_changed(url_widget, fake_web_tab, load_status, qurl): tab_widget = fake_web_tab(load_status=load_status, url=qurl) url_widget.on_tab_changed(tab_widget) assert url_widget._urltype == load_status - assert url_widget.text() == urlutils.safe_display_url(qurl) + assert url_widget.text() == urlutils.safe_display_string(qurl) @pytest.mark.parametrize('qurl, load_status, expected_status', [ diff --git a/tests/unit/utils/test_urlutils.py b/tests/unit/utils/test_urlutils.py index 8b0fb4e3b..9b3c5d1c3 100644 --- a/tests/unit/utils/test_urlutils.py +++ b/tests/unit/utils/test_urlutils.py @@ -758,13 +758,13 @@ def test_data_url(): (QUrl('http://www.xn--80ak6aa92e.com'), '(unparseable URL!) http://www.аррӏе.com'), ]) -def test_safe_display_url(url, expected): - assert urlutils.safe_display_url(url) == expected +def test_safe_display_string(url, expected): + assert urlutils.safe_display_string(url) == expected -def test_safe_display_url_invalid(): +def test_safe_display_string_invalid(): with pytest.raises(urlutils.InvalidUrlError): - urlutils.safe_display_url(QUrl()) + urlutils.safe_display_string(QUrl()) class TestProxyFromUrl: From 930b0f08181f1ba6b96669af29b11b2c1691d7c5 Mon Sep 17 00:00:00 2001 From: Marcel Schilling Date: Mon, 24 Apr 2017 07:56:44 +0200 Subject: [PATCH 141/825] typo fix (in comment) --- qutebrowser/utils/urlutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index ac11c1fb5..23bf97f9f 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -593,7 +593,7 @@ def data_url(mimetype, data): def safe_display_string(qurl): - """Get a IDN-homograph phising safe form of the given QUrl. + """Get a IDN-homograph phishing safe form of the given QUrl. If we're dealing with a Punycode-encoded URL, this prepends the hostname in its encoded form, to make sure those URLs are distinguishable. From 18082526f48afcef3fc574e1536ef282aa0cf5bd Mon Sep 17 00:00:00 2001 From: Imran Sobir Date: Mon, 24 Apr 2017 10:59:11 +0500 Subject: [PATCH 142/825] Show hostname in history page. --- qutebrowser/html/history.html | 7 +++++++ qutebrowser/javascript/history.js | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/qutebrowser/html/history.html b/qutebrowser/html/history.html index 35ac419be..955aefb19 100644 --- a/qutebrowser/html/history.html +++ b/qutebrowser/html/history.html @@ -47,6 +47,13 @@ table { text-align: center; } +.hostname { + color: #858585; + font-size: 0.9em; + margin-left: 10px; + text-decoration: none; +} + {% endblock %} {% block content %}

Browsing history

diff --git a/qutebrowser/javascript/history.js b/qutebrowser/javascript/history.js index a2448a14f..f46ceb49d 100644 --- a/qutebrowser/javascript/history.js +++ b/qutebrowser/javascript/history.js @@ -111,7 +111,11 @@ window.loadHistory = (function() { var link = document.createElement("a"); link.href = itemUrl; link.innerHTML = itemTitle; + var host = document.createElement("span"); + host.className = "hostname"; + host.innerHTML = link.hostname; title.appendChild(link); + title.appendChild(host); var time = document.createElement("td"); time.className = "time"; From 21204299604e1ecaddfd834aab44e12fd9bccf07 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 24 Apr 2017 08:01:44 +0200 Subject: [PATCH 143/825] Regenerate authors --- README.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.asciidoc b/README.asciidoc index 7969d794b..8f1af3bf0 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -251,6 +251,7 @@ Contributors, sorted by the number of commits in descending order: * Penaz * Mathias Fussenegger * Marcelo Santos +* Marcel Schilling * Joel Bradshaw * Jean-Louis Fuchs * Franz Fellner @@ -278,7 +279,6 @@ Contributors, sorted by the number of commits in descending order: * Noah Huesser * Moez Bouhlel * Matthias Lisin -* Marcel Schilling * Lazlow Carmichael * Kevin Wang * Ján Kobezda From 1539301d64634dec59d67ad5bd91689b1505d7ca Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 24 Apr 2017 08:41:29 +0200 Subject: [PATCH 144/825] Fix test coverage for statusbar.url --- tests/unit/mainwindow/statusbar/test_url.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/unit/mainwindow/statusbar/test_url.py b/tests/unit/mainwindow/statusbar/test_url.py index c104cb233..78f7ab646 100644 --- a/tests/unit/mainwindow/statusbar/test_url.py +++ b/tests/unit/mainwindow/statusbar/test_url.py @@ -69,13 +69,14 @@ def url_widget(qtbot, monkeypatch, config_stub): @pytest.mark.parametrize('which', ['normal', 'hover']) def test_set_url(url_widget, url_text, expected, which): """Test text when hovering over a percent encoded link.""" - qurl = QUrl(url_text) if which == 'normal': if url_text is None: - return - if not qurl.isValid(): - # Special case for the invalid URL above - expected = "Invalid URL!" + qurl = None + else: + qurl = QUrl(url_text) + if not qurl.isValid(): + # Special case for the invalid URL above + expected = "Invalid URL!" url_widget.set_url(qurl) else: url_widget.set_hover_url(url_text) @@ -111,12 +112,18 @@ def test_on_load_status_changed(url_widget, status, expected): (url.UrlType.success_https, QUrl('https://supersecret.gov/nsa/files.txt')), (url.UrlType.warn, QUrl('www.shadysite.org/some/file/with/issues.htm')), (url.UrlType.error, QUrl('invalid::/url')), + (url.UrlType.error, QUrl()), ]) def test_on_tab_changed(url_widget, fake_web_tab, load_status, qurl): tab_widget = fake_web_tab(load_status=load_status, url=qurl) url_widget.on_tab_changed(tab_widget) + assert url_widget._urltype == load_status - assert url_widget.text() == urlutils.safe_display_string(qurl) + if not qurl.isValid(): + expected = '' + else: + expected = urlutils.safe_display_string(qurl) + assert url_widget.text() == expected @pytest.mark.parametrize('qurl, load_status, expected_status', [ From 11c026bf4cbea51791cfaae2d12be4b280939e70 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 24 Apr 2017 12:22:19 +0200 Subject: [PATCH 145/825] Reenable QtWebKit cache with Qt 5.9. This was fixed here: https://codereview.qt-project.org/#/c/190818/ See #2427 --- CHANGELOG.asciidoc | 4 ++-- qutebrowser/browser/webkit/cache.py | 3 ++- tests/unit/browser/webkit/test_cache.py | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 6e4c8c372..a86dc2669 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -43,8 +43,8 @@ Changed - PAC on QtWebKit now supports SOCKS5 as type. - Comments in the config file are now before the individual options instead of being before sections. -- The HTTP cache is disabled with QtWebKit on Qt 5.8 now as it leads to frequent - crashes due to a Qt bug. +- The HTTP cache is disabled with QtWebKit on Qt 5.7.1 and 5.8 now as it leads + to frequent crashes due to a Qt bug. - stdin is now closed immediately for processes spawned from qutebrowser - When ui -> message-timeout is set to 0, messages are now never cleared. diff --git a/qutebrowser/browser/webkit/cache.py b/qutebrowser/browser/webkit/cache.py index 8bbb8f812..4f96b6528 100644 --- a/qutebrowser/browser/webkit/cache.py +++ b/qutebrowser/browser/webkit/cache.py @@ -54,7 +54,8 @@ class DiskCache(QNetworkDiskCache): if size is None: size = 1024 * 1024 * 50 # default from QNetworkDiskCachePrivate # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-59909 - if qtutils.version_check('5.7.1'): # pragma: no cover + if (qtutils.version_check('5.7.1') and # pragma: no cover + not qtutils.version_check('5.9')): size = 0 self.setMaximumCacheSize(size) diff --git a/tests/unit/browser/webkit/test_cache.py b/tests/unit/browser/webkit/test_cache.py index ea2473949..bc8efb675 100644 --- a/tests/unit/browser/webkit/test_cache.py +++ b/tests/unit/browser/webkit/test_cache.py @@ -25,9 +25,9 @@ from qutebrowser.browser.webkit import cache from qutebrowser.utils import qtutils -pytestmark = pytest.mark.skipif(qtutils.version_check('5.7.1'), - reason="QNetworkDiskCache is broken on Qt >= " - "5.7.1") +pytestmark = pytest.mark.skipif( + qtutils.version_check('5.7.1') and not qtutils.version_check('5.9'), + reason="QNetworkDiskCache is broken on Qt 5.7.1 and 5.8") def preload_cache(cache, url='http://www.example.com/', content=b'foobar'): From fe7d21dfbe0b9dabcb66eaa61c20a5d16c9e175c Mon Sep 17 00:00:00 2001 From: Imran Sobir Date: Mon, 24 Apr 2017 15:30:01 +0500 Subject: [PATCH 146/825] Show hostname in non-javascript history page. --- qutebrowser/browser/qutescheme.py | 3 ++- qutebrowser/html/history.html | 8 -------- qutebrowser/html/history_nojs.html | 7 +++++-- qutebrowser/html/styled.html | 7 +++++++ 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index d71b6c135..bca93e3c8 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -311,7 +311,8 @@ def qute_history(url): start_time = time.mktime(next_date.timetuple()) - 1 history = [ (i["url"], i["title"], - datetime.datetime.fromtimestamp(i["time"]/1000)) + datetime.datetime.fromtimestamp(i["time"]/1000), + QUrl(i["url"]).host()) for i in history_data(start_time) if "next" not in i ] diff --git a/qutebrowser/html/history.html b/qutebrowser/html/history.html index 955aefb19..2c6c0a2f1 100644 --- a/qutebrowser/html/history.html +++ b/qutebrowser/html/history.html @@ -46,14 +46,6 @@ table { height: 40px; text-align: center; } - -.hostname { - color: #858585; - font-size: 0.9em; - margin-left: 10px; - text-decoration: none; -} - {% endblock %} {% block content %}

Browsing history

diff --git a/qutebrowser/html/history_nojs.html b/qutebrowser/html/history_nojs.html index 3fed67797..bcc5663c1 100644 --- a/qutebrowser/html/history_nojs.html +++ b/qutebrowser/html/history_nojs.html @@ -42,9 +42,12 @@ table { - {% for url, title, time in history %} + {% for url, title, time, host in history %} - + {% endfor %} diff --git a/qutebrowser/html/styled.html b/qutebrowser/html/styled.html index e2a608538..f4d256422 100644 --- a/qutebrowser/html/styled.html +++ b/qutebrowser/html/styled.html @@ -38,4 +38,11 @@ td { padding: 2px 5px; text-align: left; } + +.hostname { + color: #858585; + font-size: 0.9em; + margin-left: 10px; + text-decoration: none; +} {% endblock %} From 111944fb655bc34776e3ae8ad1a556fe10b6d498 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 24 Apr 2017 23:15:34 +0200 Subject: [PATCH 147/825] Revert "Raise exception when a stylesheet is unparsable." This reverts commit 0400945ac4c3b31dd476e2f8727f75d895a09378. See #2571 --- CHANGELOG.asciidoc | 1 + qutebrowser/utils/log.py | 17 +---------------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index a86dc2669..dd27c434b 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -61,6 +61,7 @@ Fixed - Crash when the key config isn't writable - Crash when unbinding an unbound key in the key config - Crash when using `:debug-log-filter` when `--filter` wasn't given on startup. +- Crash with some invalid setting values - Various rare crashes v0.10.1 diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 9c660f035..19b4f506f 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -161,11 +161,6 @@ def stub(suffix=''): misc.warning(text) -class CriticalQtWarning(Exception): - - """Exception raised when there's a critical Qt warning.""" - - def init_log(args): """Init loggers based on the argparse namespace passed.""" level = args.loglevel.upper() @@ -424,17 +419,7 @@ def qt_message_handler(msg_type, context, msg): 'with: -9805', # flake8: disable=E131 ] - # Messages which will trigger an exception immediately - critical_msgs = [ - 'Could not parse stylesheet of object', - ] - - if any(msg.strip().startswith(pattern) for pattern in critical_msgs): - # For some reason, the stack gets lost when raising here... - logger = logging.getLogger('misc') - logger.error("Got critical Qt warning!", stack_info=True) - raise CriticalQtWarning(msg) - elif any(msg.strip().startswith(pattern) for pattern in suppressed_msgs): + if any(msg.strip().startswith(pattern) for pattern in suppressed_msgs): level = logging.DEBUG else: level = qt_to_logging[msg_type] From ab61fc57a98abcbfe6ac50622e9cbff25ad0325c Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 25 Apr 2017 00:34:15 +0200 Subject: [PATCH 148/825] Update codecov from 2.0.5 to 2.0.7 --- misc/requirements/requirements-codecov.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-codecov.txt b/misc/requirements/requirements-codecov.txt index 4f020b5cd..2dcb22731 100644 --- a/misc/requirements/requirements-codecov.txt +++ b/misc/requirements/requirements-codecov.txt @@ -1,5 +1,5 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -codecov==2.0.5 +codecov==2.0.7 coverage==4.3.4 requests==2.13.0 From 3125b69d194309464ef0abcaa4f915ff4cf4316f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 25 Apr 2017 06:43:31 +0200 Subject: [PATCH 149/825] Fix no-cover pragma --- qutebrowser/browser/webkit/cache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/webkit/cache.py b/qutebrowser/browser/webkit/cache.py index 4f96b6528..be7dba486 100644 --- a/qutebrowser/browser/webkit/cache.py +++ b/qutebrowser/browser/webkit/cache.py @@ -54,8 +54,8 @@ class DiskCache(QNetworkDiskCache): if size is None: size = 1024 * 1024 * 50 # default from QNetworkDiskCachePrivate # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-59909 - if (qtutils.version_check('5.7.1') and # pragma: no cover - not qtutils.version_check('5.9')): + if (qtutils.version_check('5.7.1') and + not qtutils.version_check('5.9')): # pragma: no cover size = 0 self.setMaximumCacheSize(size) From c3e62222960da5584c8512c74f8791b6acaa51c1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 25 Apr 2017 06:59:51 +0200 Subject: [PATCH 150/825] Close the current tab when the tabbar itself is clicked --- CHANGELOG.asciidoc | 2 ++ qutebrowser/mainwindow/tabwidget.py | 9 +++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index dd27c434b..b75d20c4a 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -47,6 +47,8 @@ Changed to frequent crashes due to a Qt bug. - stdin is now closed immediately for processes spawned from qutebrowser - When ui -> message-timeout is set to 0, messages are now never cleared. +- Middle/right-clicking the blank parts of the tab bar (when vertical) now + closes the current tab. Fixed ~~~~~ diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index ce96607fa..f6ed0a8d1 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -395,11 +395,12 @@ class TabBar(QTabBar): button = config.get('tabs', 'close-mouse-button') if (e.button() == Qt.RightButton and button == 'right' or e.button() == Qt.MiddleButton and button == 'middle'): + e.accept() idx = self.tabAt(e.pos()) - if idx != -1: - e.accept() - self.tabCloseRequested.emit(idx) - return + if idx == -1: + idx = self.currentIndex() + self.tabCloseRequested.emit(idx) + return super().mousePressEvent(e) def minimumTabSizeHint(self, index): From 1015badb8bb753ed562bbecc64942cf2dc26416e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 25 Apr 2017 09:18:31 +0200 Subject: [PATCH 151/825] Disable animation for completion view --- qutebrowser/completion/completionwidget.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index bd6b8c5be..923a67280 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -124,6 +124,7 @@ class CompletionView(QTreeView): self.setIndentation(0) self.setItemsExpandable(False) self.setExpandsOnDoubleClick(False) + self.setAnimated(False) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) # WORKAROUND # This is a workaround for weird race conditions with invalid From ca0e04fd0df85f56116fda9ca8db9c6366ac07d3 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 25 Apr 2017 18:41:00 +0200 Subject: [PATCH 152/825] Mention :open in issue template See #2574 --- .github/ISSUE_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 9ea69b642..b9bf8d399 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,2 +1,2 @@ +`:open qute:version` or `qutebrowser --version` --> From 66eb330a0a481cdd77820492c4745b6ead5f05eb Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 25 Apr 2017 21:40:28 +0200 Subject: [PATCH 153/825] Always base tabbar on Fusion style. Fixes crashes with qt5ct. Fixes #2477. Fixes #1554. --- CHANGELOG.asciidoc | 1 + qutebrowser/mainwindow/tabwidget.py | 17 ++++++----------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index b75d20c4a..0fa268d02 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -65,6 +65,7 @@ Fixed - Crash when using `:debug-log-filter` when `--filter` wasn't given on startup. - Crash with some invalid setting values - Various rare crashes +- Various styling issues with the tabbar and a crash with qt5ct v0.10.1 ------- diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index f6ed0a8d1..6acaf235a 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -24,7 +24,8 @@ import functools from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QSize, QRect, QTimer, QUrl from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle, - QStyle, QStylePainter, QStyleOptionTab) + QStyle, QStylePainter, QStyleOptionTab, + QStyleFactory) from PyQt5.QtGui import QIcon, QPalette, QColor from qutebrowser.utils import qtutils, objreg, utils, usertypes, log @@ -51,7 +52,7 @@ class TabWidget(QTabWidget): def __init__(self, win_id, parent=None): super().__init__(parent) bar = TabBar(win_id) - self.setStyle(TabBarStyle(self.style())) + self.setStyle(TabBarStyle()) self.setTabBar(bar) bar.tabCloseRequested.connect(self.tabCloseRequested) bar.tabMoved.connect(functools.partial( @@ -269,7 +270,7 @@ class TabBar(QTabBar): def __init__(self, win_id, parent=None): super().__init__(parent) self._win_id = win_id - self.setStyle(TabBarStyle(self.style())) + self.setStyle(TabBarStyle()) self.set_font() config_obj = objreg.get('config') config_obj.changed.connect(self.set_font) @@ -554,20 +555,14 @@ class TabBarStyle(QCommonStyle): http://stackoverflow.com/a/17294081 https://code.google.com/p/makehuman/source/browse/trunk/makehuman/lib/qtgui.py - - Attributes: - _style: The base/"parent" style. """ - def __init__(self, style): + def __init__(self): """Initialize all functions we're not overriding. This simply calls the corresponding function in self._style. - - Args: - style: The base/"parent" style. """ - self._style = style + self._style = QStyleFactory.create('Fusion') for method in ['drawComplexControl', 'drawItemPixmap', 'generatedIconPixmap', 'hitTestComplexControl', 'itemPixmapRect', 'itemTextRect', 'polish', 'styleHint', From 70d7a56b1189d2d85da1303a2fbacea57171e324 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 25 Apr 2017 22:13:59 +0200 Subject: [PATCH 154/825] Also set Fusion style for downloads and completion This makes those UI elements look the same on different platforms/OS styles, with the small drawback of overriding the context menu style. This most likely fixes #80 (though I couldn't reproduce that on Windows 10). --- qutebrowser/browser/downloadview.py | 3 ++- qutebrowser/completion/completionwidget.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/downloadview.py b/qutebrowser/browser/downloadview.py index 633fd8700..715fe2a54 100644 --- a/qutebrowser/browser/downloadview.py +++ b/qutebrowser/browser/downloadview.py @@ -23,7 +23,7 @@ import functools import sip from PyQt5.QtCore import pyqtSlot, QSize, Qt, QTimer -from PyQt5.QtWidgets import QListView, QSizePolicy, QMenu +from PyQt5.QtWidgets import QListView, QSizePolicy, QMenu, QStyleFactory from qutebrowser.browser import downloads from qutebrowser.config import style @@ -75,6 +75,7 @@ class DownloadView(QListView): def __init__(self, win_id, parent=None): super().__init__(parent) + self.setStyle(QStyleFactory.create('Fusion')) style.set_register_stylesheet(self) self.setResizeMode(QListView.Adjust) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index 923a67280..e9fad39fd 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -23,7 +23,7 @@ Defines a CompletionView which uses CompletionFiterModel and CompletionModel subclasses to provide completions. """ -from PyQt5.QtWidgets import QStyle, QTreeView, QSizePolicy +from PyQt5.QtWidgets import QStyle, QTreeView, QSizePolicy, QStyleFactory from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, QSize from qutebrowser.config import config, style @@ -117,6 +117,7 @@ class CompletionView(QTreeView): self._delegate = completiondelegate.CompletionItemDelegate(self) self.setItemDelegate(self._delegate) + self.setStyle(QStyleFactory.create('Fusion')) style.set_register_stylesheet(self) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.setHeaderHidden(True) From 6a35797a2c3df93fcbcbab988b2ce7f95251fd85 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 25 Apr 2017 22:57:39 +0200 Subject: [PATCH 155/825] Mention C++/JS in CONTRIBUTING --- CONTRIBUTING.asciidoc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CONTRIBUTING.asciidoc b/CONTRIBUTING.asciidoc index 12d52fee7..25a7a8a13 100644 --- a/CONTRIBUTING.asciidoc +++ b/CONTRIBUTING.asciidoc @@ -42,6 +42,12 @@ be easy to solve] * https://github.com/qutebrowser/qutebrowser/labels/not%20code[Issues which require little/no coding] +If you prefer C++ or Javascript to Python, see the relevant issues which involve +work in those languages: + +* https://github.com/qutebrowser/qutebrowser/issues?utf8=%E2%9C%93&q=is%3Aopen%20is%3Aissue%20label%3Ac%2B%2B[C++] (mostly work on Qt, the library behind qutebrowser) +* https://github.com/qutebrowser/qutebrowser/issues?q=is%3Aopen+is%3Aissue+label%3Ajavascript[JavaScript] + There are also some things to do if you don't want to write code: * Help the community, e.g., on the mailinglist and the IRC channel. From 27057622ba77b762d8d063e8a313ac97b133f040 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 26 Apr 2017 08:56:41 +0200 Subject: [PATCH 156/825] Update authors --- README.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.asciidoc b/README.asciidoc index 8f1af3bf0..7aa1afa66 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -161,8 +161,8 @@ Contributors, sorted by the number of commits in descending order: * Marshall Lochbaum * Bruno Oliveira * Martin Tournoij -* Alexander Cogneau * Imran Sobir +* Alexander Cogneau * Felix Van der Jeugt * Daniel Karbach * Kevin Velghe From b1d88b47c13bf3cf6dbb74ad1b0bb77bff40cb73 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 26 Apr 2017 10:32:42 +0200 Subject: [PATCH 157/825] Update vulture from 0.13 to 0.14 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index d5b0a0335..fed0d3dca 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -32,5 +32,5 @@ pytest-travis-fold==1.2.0 pytest-warnings==0.2.0 pytest-xvfb==1.0.0 PyVirtualDisplay==0.2.1 -vulture==0.13 +vulture==0.14 Werkzeug==0.12.1 From 9ef17d434ddc85c6cda4d08f00fef15b7904add1 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 26 Apr 2017 10:32:43 +0200 Subject: [PATCH 158/825] Update vulture from 0.13 to 0.14 --- misc/requirements/requirements-vulture.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-vulture.txt b/misc/requirements/requirements-vulture.txt index 26ee55a6e..56e20c603 100644 --- a/misc/requirements/requirements-vulture.txt +++ b/misc/requirements/requirements-vulture.txt @@ -1,3 +1,3 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -vulture==0.13 +vulture==0.14 From e252862f51db7df5ff34e2dc0221a7f56e022f9f Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 26 Apr 2017 15:15:11 +0200 Subject: [PATCH 159/825] Update hypothesis from 3.8.0 to 3.8.1 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index d5b0a0335..edcab990f 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -9,7 +9,7 @@ EasyProcess==0.2.3 Flask==0.12.1 glob2==0.5 httpbin==0.5.0 -hypothesis==3.8.0 +hypothesis==3.8.1 itsdangerous==0.24 # Jinja2==2.9.5 Mako==1.0.6 From 38099c45bd4b6283fcdf04411263c4378e81cf95 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 26 Apr 2017 21:48:21 +0200 Subject: [PATCH 160/825] Update hypothesis from 3.8.1 to 3.8.2 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index c659218d2..511300e96 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -9,7 +9,7 @@ EasyProcess==0.2.3 Flask==0.12.1 glob2==0.5 httpbin==0.5.0 -hypothesis==3.8.1 +hypothesis==3.8.2 itsdangerous==0.24 # Jinja2==2.9.5 Mako==1.0.6 From 046401c48904caae02c0248ef48410dbbe462228 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 27 Apr 2017 08:20:55 +0200 Subject: [PATCH 161/825] Clean up search.feature --- tests/end2end/features/search.feature | 57 ++++++++--------------- tests/end2end/features/test_search_bdd.py | 14 +++++- 2 files changed, 32 insertions(+), 39 deletions(-) diff --git a/tests/end2end/features/search.feature b/tests/end2end/features/search.feature index 4d8b8e17f..aeea2e4e8 100644 --- a/tests/end2end/features/search.feature +++ b/tests/end2end/features/search.feature @@ -11,20 +11,17 @@ Feature: Searching on a page Scenario: Searching text When I run :search foo - And I run :yank selection - Then the clipboard should contain "foo" + Then "foo" should be found Scenario: Searching twice When I run :search foo And I run :search bar - And I run :yank selection - Then the clipboard should contain "Bar" + Then "Bar" should be found Scenario: Searching with --reverse When I set general -> ignore-case to true And I run :search -r foo - And I run :yank selection - Then the clipboard should contain "Foo" + Then "Foo" should be found Scenario: Searching without matches When I run :search doesnotmatch @@ -34,14 +31,12 @@ Feature: Searching on a page Scenario: Searching with / and spaces at the end (issue 874) When I run :set-cmd-text -s /space And I run :command-accept - And I run :yank selection - Then the clipboard should contain "space " + Then "space " should be found Scenario: Searching with / and slash in search term (issue 507) When I run :set-cmd-text -s //slash And I run :command-accept - And I run :yank selection - Then the clipboard should contain "/slash" + Then "/slash" should be found # This doesn't work because this is QtWebKit behavior. @xfail_norun @@ -54,26 +49,22 @@ Feature: Searching on a page Scenario: Searching text with ignore-case = true When I set general -> ignore-case to true And I run :search bar - And I run :yank selection - Then the clipboard should contain "Bar" + Then "Bar" should be found Scenario: Searching text with ignore-case = false When I set general -> ignore-case to false And I run :search bar - And I run :yank selection - Then the clipboard should contain "bar" + Then "bar" should be found Scenario: Searching text with ignore-case = smart (lower-case) When I set general -> ignore-case to smart And I run :search bar - And I run :yank selection - Then the clipboard should contain "Bar" + Then "Bar" should be found Scenario: Searching text with ignore-case = smart (upper-case) When I set general -> ignore-case to smart And I run :search Foo - And I run :yank selection - Then the clipboard should contain "Foo" # even though foo was first + Then "Foo" should be found # even though foo was first ## :search-next @@ -81,22 +72,19 @@ Feature: Searching on a page When I set general -> ignore-case to true And I run :search foo And I run :search-next - And I run :yank selection - Then the clipboard should contain "Foo" + Then "Foo" should be found Scenario: Jumping to next match with count When I set general -> ignore-case to true And I run :search baz And I run :search-next with count 2 - And I run :yank selection - Then the clipboard should contain "BAZ" + Then "BAZ" should be found Scenario: Jumping to next match with --reverse When I set general -> ignore-case to true And I run :search --reverse foo And I run :search-next - And I run :yank selection - Then the clipboard should contain "foo" + Then "foo" should be found Scenario: Jumping to next match without search # Make sure there was no search in the same window before @@ -109,8 +97,7 @@ Feature: Searching on a page And I run :search foo And I run :tab-prev And I run :search-next - And I run :yank selection - Then the clipboard should contain "foo" + Then "foo" should be found # https://github.com/qutebrowser/qutebrowser/issues/2438 Scenario: Jumping to next match after clearing @@ -118,8 +105,7 @@ Feature: Searching on a page And I run :search foo And I run :search And I run :search-next - And I run :yank selection - Then the clipboard should contain "foo" + Then "foo" should be found ## :search-prev @@ -128,8 +114,7 @@ Feature: Searching on a page And I run :search foo And I run :search-next And I run :search-prev - And I run :yank selection - Then the clipboard should contain "foo" + Then "foo" should be found Scenario: Jumping to previous match with count When I set general -> ignore-case to true @@ -137,16 +122,14 @@ Feature: Searching on a page And I run :search-next And I run :search-next And I run :search-prev with count 2 - And I run :yank selection - Then the clipboard should contain "baz" + Then "baz" should be found Scenario: Jumping to previous match with --reverse When I set general -> ignore-case to true And I run :search --reverse foo And I run :search-next And I run :search-prev - And I run :yank selection - Then the clipboard should contain "Foo" + Then "Foo" should be found Scenario: Jumping to previous match without search # Make sure there was no search in the same window before @@ -160,15 +143,13 @@ Feature: Searching on a page When I run :search foo And I run :search-next And I run :search-next - And I run :yank selection - Then the clipboard should contain "foo" + Then "foo" should be found Scenario: Wrapping around page with --reverse When I run :search --reverse foo And I run :search-next And I run :search-next - And I run :yank selection - Then the clipboard should contain "Foo" + Then "Foo" should be found # TODO: wrapping message with scrolling # TODO: wrapping message without scrolling diff --git a/tests/end2end/features/test_search_bdd.py b/tests/end2end/features/test_search_bdd.py index 12e5d9480..d9866fe03 100644 --- a/tests/end2end/features/test_search_bdd.py +++ b/tests/end2end/features/test_search_bdd.py @@ -17,6 +17,8 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . +import json + import pytest import pytest_bdd as bdd @@ -24,6 +26,16 @@ import pytest_bdd as bdd from end2end.features.test_yankpaste_bdd import init_fake_clipboard -bdd.scenarios('search.feature') +@bdd.then(bdd.parsers.parse('"{text}" should be found')) +def check_found_text(request, quteproc, text): + quteproc.send_cmd(':yank selection') + quteproc.wait_for(message='Setting fake clipboard: {}'.format( + json.dumps(text))) + +# After cancelling the search, the text (sometimes?) shows up as selected. +# However, we can't get it via ':yank selection' it seems? pytestmark = pytest.mark.qtwebengine_skip("Searched text is not selected...") + + +bdd.scenarios('search.feature') From 6549fd84ce461d3098c13818219df4e4bfd6b444 Mon Sep 17 00:00:00 2001 From: Niklas Haas Date: Wed, 26 Apr 2017 02:49:57 +0200 Subject: [PATCH 162/825] Add tabs->favicon-scale setting This allows users to change the size of the favicon independently from the size of the font/tab, in order to adjust the balance between favicons and text. The drawing code is also adjusted to place the icon relative to the text center, rather than the text top. Works as expected even for values of 0.0 (which is equivalent to hiding the favicon completely). Closes #2549. --- doc/help/settings.asciidoc | 7 +++++++ qutebrowser/config/configdata.py | 5 +++++ qutebrowser/mainwindow/tabwidget.py | 14 +++++++++++--- tests/unit/mainwindow/test_tabwidget.py | 1 + 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index c6a80754c..68ca0a118 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -126,6 +126,7 @@ |<>|On which mouse button to close tabs. |<>|The position of the tab bar. |<>|Whether to show favicons in the tab bar. +|<>|Scale for favicons in the tab bar. The tab size is unchanged, so big favicons also require extra `tabs->padding`. |<>|The width of the tab bar if it's vertical, in px or as percentage of the window. |<>|Width of the progress indicator (0 to disable). |<>|Whether to open windows instead of tabs. @@ -1206,6 +1207,12 @@ Valid values: Default: +pass:[true]+ +[[tabs-favicon-scale]] +=== favicon-scale +Scale for favicons in the tab bar. The tab size is unchanged, so big favicons also require extra `tabs->padding`. + +Default: +pass:[1.0]+ + [[tabs-width]] === width The width of the tab bar if it's vertical, in px or as percentage of the window. diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 2bfcabce7..2de0c3eb2 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -680,6 +680,11 @@ def data(readonly=False): SettingValue(typ.Bool(), 'true'), "Whether to show favicons in the tab bar."), + ('favicon-scale', + SettingValue(typ.Float(minval=0.0), '1.0'), + "Scale for favicons in the tab bar. The tab size is unchanged, " + "so big favicons also require extra `tabs->padding`."), + ('width', SettingValue(typ.PercOrInt(minperc=0, maxperc=100, minint=1), '20%'), diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index ce96607fa..f5a0b3c55 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -22,7 +22,8 @@ import collections import functools -from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QSize, QRect, QTimer, QUrl +from PyQt5.QtCore import (pyqtSignal, pyqtSlot, Qt, QSize, QRect, QPoint, + QTimer, QUrl) from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle, QStyle, QStylePainter, QStyleOptionTab) from PyQt5.QtGui import QIcon, QPalette, QColor @@ -273,6 +274,7 @@ class TabBar(QTabBar): self.set_font() config_obj = objreg.get('config') config_obj.changed.connect(self.set_font) + config_obj.changed.connect(self.set_icon_size) self.vertical = False self._page_fullscreen = False self._auto_hide_timer = QTimer() @@ -374,7 +376,13 @@ class TabBar(QTabBar): def set_font(self): """Set the tab bar font.""" self.setFont(config.get('fonts', 'tabbar')) + self.set_icon_size() + + @config.change_filter('tabs', 'favicon-scale') + def set_icon_size(self): + """Set the tab bar favicon size.""" size = self.fontMetrics().height() - 2 + size *= config.get('tabs', 'favicon-scale') self.setIconSize(QSize(size, size)) @config.change_filter('colors', 'tabs.bg.bar') @@ -775,7 +783,7 @@ class TabBarStyle(QCommonStyle): tab_icon_size = QSize( min(actual_size.width(), icon_size.width()), min(actual_size.height(), icon_size.height())) - icon_rect = QRect(text_rect.left(), text_rect.top() + 1, - tab_icon_size.width(), tab_icon_size.height()) + icon_top = text_rect.center().y() + 1 - tab_icon_size.height() / 2 + icon_rect = QRect(QPoint(text_rect.left(), icon_top), tab_icon_size) icon_rect = self._style.visualRect(opt.direction, opt.rect, icon_rect) return icon_rect diff --git a/tests/unit/mainwindow/test_tabwidget.py b/tests/unit/mainwindow/test_tabwidget.py index 2d3e9bda2..7a3fd68af 100644 --- a/tests/unit/mainwindow/test_tabwidget.py +++ b/tests/unit/mainwindow/test_tabwidget.py @@ -44,6 +44,7 @@ class TestTabWidget: 'select-on-remove': 1, 'show': 'always', 'show-favicons': True, + 'favicon-scale': 1.0, 'padding': configtypes.PaddingValues(0, 0, 5, 5), 'indicator-width': 3, 'indicator-padding': configtypes.PaddingValues(2, 2, 0, 4), From c12347189fca26261af7dcdce121e51c170b521d Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 27 Apr 2017 09:42:00 +0200 Subject: [PATCH 163/825] Update colorama from 0.3.8 to 0.3.9 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 57ed78734..b2cc93c1f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -colorama==0.3.8 +colorama==0.3.9 cssutils==1.0.2 Jinja2==2.9.6 MarkupSafe==1.0 From 0e14117fdad0442520c4e1fe64231b447306cbc7 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 27 Apr 2017 18:27:03 +0200 Subject: [PATCH 164/825] Update codecov from 2.0.7 to 2.0.8 --- misc/requirements/requirements-codecov.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-codecov.txt b/misc/requirements/requirements-codecov.txt index 2dcb22731..8337d667d 100644 --- a/misc/requirements/requirements-codecov.txt +++ b/misc/requirements/requirements-codecov.txt @@ -1,5 +1,5 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -codecov==2.0.7 +codecov==2.0.8 coverage==4.3.4 requests==2.13.0 From 4cd977cab67ee95b0f1b399e20c4fe863e92039b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 27 Apr 2017 20:14:51 +0200 Subject: [PATCH 165/825] Fix zero handling in qflags_key --- qutebrowser/utils/debug.py | 4 ++++ tests/unit/utils/test_debug.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/qutebrowser/utils/debug.py b/qutebrowser/utils/debug.py index 89ae62faf..a516c43f3 100644 --- a/qutebrowser/utils/debug.py +++ b/qutebrowser/utils/debug.py @@ -156,6 +156,10 @@ def qflags_key(base, value, add_base=False, klass=None): klass = value.__class__ if klass == int: raise TypeError("Can't guess enum class of an int!") + + if not value: + return qenum_key(base, value, add_base, klass) + bits = [] names = [] mask = 0x01 diff --git a/tests/unit/utils/test_debug.py b/tests/unit/utils/test_debug.py index 04a2fa436..7bcad59ec 100644 --- a/tests/unit/utils/test_debug.py +++ b/tests/unit/utils/test_debug.py @@ -138,6 +138,7 @@ class TestQEnumKey: (QFrame, QFrame.Sunken, None, 'Sunken'), (QFrame, 0x0030, QFrame.Shadow, 'Sunken'), (QFrame, 0x1337, QFrame.Shadow, '0x1337'), + (Qt, Qt.AnchorLeft, None, 'AnchorLeft'), ]) def test_qenum_key(self, base, value, klass, expected): key = debug.qenum_key(base, value, klass=klass) @@ -168,6 +169,8 @@ class TestQFlagsKey: (Qt, Qt.AlignCenter, None, 'AlignHCenter|AlignVCenter'), fixme((Qt, 0x0021, Qt.Alignment, 'AlignLeft|AlignTop')), (Qt, 0x1100, Qt.Alignment, '0x0100|0x1000'), + (Qt, Qt.DockWidgetAreas(0), Qt.DockWidgetArea, 'NoDockWidgetArea'), + (Qt, Qt.DockWidgetAreas(0), None, '0x0000'), ]) def test_qflags_key(self, base, value, klass, expected): flags = debug.qflags_key(base, value, klass=klass) From d62ebdb926f9dc1fe9d23a29992f7a323ab2b328 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 27 Apr 2017 21:01:10 +0200 Subject: [PATCH 166/825] Make most of search BDD tests work with QtWebEngine --- qutebrowser/browser/webengine/webenginetab.py | 29 +++++++++----- qutebrowser/browser/webkit/webkittab.py | 25 +++++++++--- tests/end2end/features/search.feature | 38 +++++++++++++++++++ tests/end2end/features/test_search_bdd.py | 13 ++++--- 4 files changed, 84 insertions(+), 21 deletions(-) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 821b73a1a..cedd59b1e 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -40,7 +40,7 @@ from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory, webenginedownloads) from qutebrowser.misc import miscwidgets from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils, - objreg, jinja) + objreg, jinja, debug) _qute_scheme_handler = None @@ -128,12 +128,21 @@ class WebEngineSearch(browsertab.AbstractSearch): super().__init__(parent) self._flags = QWebEnginePage.FindFlags(0) - def _find(self, text, flags, cb=None): - """Call findText on the widget with optional callback.""" - if cb is None: - self._widget.findText(text, flags) - else: - self._widget.findText(text, flags, cb) + def _find(self, text, flags, callback, caller): + """Call findText on the widget.""" + def wrapped_callback(found): + """Wrap the callback to do debug logging.""" + found_text = 'found' if found else "didn't find" + if flags: + flag_text = 'with flags {}'.format(debug.qflags_key( + QWebEnginePage, flags, klass=QWebEnginePage.FindFlag)) + else: + flag_text = '' + log.webview.debug(' '.join([caller, found_text, text, flag_text]) + .strip()) + if callback is not None: + callback(found) + self._widget.findText(text, flags, wrapped_callback) def search(self, text, *, ignore_case=False, reverse=False, result_cb=None): @@ -148,7 +157,7 @@ class WebEngineSearch(browsertab.AbstractSearch): self.text = text self._flags = flags - self._find(text, flags, result_cb) + self._find(text, flags, result_cb, 'search') def clear(self): self._widget.findText('') @@ -160,10 +169,10 @@ class WebEngineSearch(browsertab.AbstractSearch): flags &= ~QWebEnginePage.FindBackward else: flags |= QWebEnginePage.FindBackward - self._find(self.text, flags, result_cb) + self._find(self.text, flags, result_cb, 'prev_result') def next_result(self, *, result_cb=None): - self._find(self.text, self._flags, result_cb) + self._find(self.text, self._flags, result_cb, 'next_result') class WebEngineCaret(browsertab.AbstractCaret): diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index ef427c3be..0a844ddb0 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -36,7 +36,7 @@ from qutebrowser.browser import browsertab from qutebrowser.browser.network import proxy from qutebrowser.browser.webkit import webview, tabhistory, webkitelem from qutebrowser.browser.webkit.network import webkitqutescheme -from qutebrowser.utils import qtutils, objreg, usertypes, utils, log +from qutebrowser.utils import qtutils, objreg, usertypes, utils, log, debug def init(): @@ -104,7 +104,7 @@ class WebKitSearch(browsertab.AbstractSearch): super().__init__(parent) self._flags = QWebPage.FindFlags(0) - def _call_cb(self, callback, found): + def _call_cb(self, callback, found, text, flags, caller): """Call the given callback if it's non-None. Delays the call via a QTimer so the website is re-rendered in between. @@ -112,7 +112,22 @@ class WebKitSearch(browsertab.AbstractSearch): Args: callback: What to call found: If the text was found + text: The text searched for + flags: The flags searched with + caller: Name of the caller. """ + found_text = 'found' if found else "didn't find" + # Removing FindWrapsAroundDocument to get the same logging as with + # QtWebEngine + debug_flags = debug.qflags_key( + QWebPage, flags & ~QWebPage.FindWrapsAroundDocument, + klass=QWebPage.FindFlag) + if debug_flags != '0x0000': + flag_text = 'with flags {}'.format(debug_flags) + else: + flag_text = '' + log.webview.debug(' '.join([caller, found_text, text, flag_text]) + .strip()) if callback is not None: QTimer.singleShot(0, functools.partial(callback, found)) @@ -137,11 +152,11 @@ class WebKitSearch(browsertab.AbstractSearch): self._widget.findText(text, flags | QWebPage.HighlightAllOccurrences) self.text = text self._flags = flags - self._call_cb(result_cb, found) + self._call_cb(result_cb, found, text, flags, 'search') def next_result(self, *, result_cb=None): found = self._widget.findText(self.text, self._flags) - self._call_cb(result_cb, found) + self._call_cb(result_cb, found, self.text, self._flags, 'next_result') def prev_result(self, *, result_cb=None): # The int() here makes sure we get a copy of the flags. @@ -151,7 +166,7 @@ class WebKitSearch(browsertab.AbstractSearch): else: flags |= QWebPage.FindBackward found = self._widget.findText(self.text, flags) - self._call_cb(result_cb, found) + self._call_cb(result_cb, found, self.text, flags, 'prev_result') class WebKitCaret(browsertab.AbstractCaret): diff --git a/tests/end2end/features/search.feature b/tests/end2end/features/search.feature index aeea2e4e8..285b3cbf8 100644 --- a/tests/end2end/features/search.feature +++ b/tests/end2end/features/search.feature @@ -11,37 +11,45 @@ Feature: Searching on a page Scenario: Searching text When I run :search foo + And I wait for "search found foo" in the log Then "foo" should be found Scenario: Searching twice When I run :search foo + And I wait for "search found foo" in the log And I run :search bar + And I wait for "search found bar" in the log Then "Bar" should be found Scenario: Searching with --reverse When I set general -> ignore-case to true And I run :search -r foo + And I wait for "search found foo with flags FindBackward" in the log Then "Foo" should be found Scenario: Searching without matches When I run :search doesnotmatch + And I wait for "search didn't find doesnotmatch" in the log Then the warning "Text 'doesnotmatch' not found on page!" should be shown @xfail_norun Scenario: Searching with / and spaces at the end (issue 874) When I run :set-cmd-text -s /space And I run :command-accept + And I wait for "search found space " in the log Then "space " should be found Scenario: Searching with / and slash in search term (issue 507) When I run :set-cmd-text -s //slash And I run :command-accept + And I wait for "search found /slash" in the log Then "/slash" should be found # This doesn't work because this is QtWebKit behavior. @xfail_norun Scenario: Searching text with umlauts When I run :search blub + And I wait for "search didn't find blub" in the log Then the warning "Text 'blub' not found on page!" should be shown ## ignore-case @@ -49,21 +57,25 @@ Feature: Searching on a page Scenario: Searching text with ignore-case = true When I set general -> ignore-case to true And I run :search bar + And I wait for "search found bar" in the log Then "Bar" should be found Scenario: Searching text with ignore-case = false When I set general -> ignore-case to false And I run :search bar + And I wait for "search found bar with flags FindCaseSensitively" in the log Then "bar" should be found Scenario: Searching text with ignore-case = smart (lower-case) When I set general -> ignore-case to smart And I run :search bar + And I wait for "search found bar" in the log Then "Bar" should be found Scenario: Searching text with ignore-case = smart (upper-case) When I set general -> ignore-case to smart And I run :search Foo + And I wait for "search found Foo with flags FindCaseSensitively" in the log Then "Foo" should be found # even though foo was first ## :search-next @@ -71,19 +83,25 @@ Feature: Searching on a page Scenario: Jumping to next match When I set general -> ignore-case to true And I run :search foo + And I wait for "search found foo" in the log And I run :search-next + And I wait for "next_result found foo" in the log Then "Foo" should be found Scenario: Jumping to next match with count When I set general -> ignore-case to true And I run :search baz + And I wait for "search found baz" in the log And I run :search-next with count 2 + And I wait for "next_result found baz" in the log Then "BAZ" should be found Scenario: Jumping to next match with --reverse When I set general -> ignore-case to true And I run :search --reverse foo + And I wait for "search found foo with flags FindBackward" in the log And I run :search-next + And I wait for "next_result found foo with flags FindBackward" in the log Then "foo" should be found Scenario: Jumping to next match without search @@ -95,16 +113,20 @@ Feature: Searching on a page Scenario: Repeating search in a second tab (issue #940) When I open data/search.html in a new tab And I run :search foo + And I wait for "search found foo" in the log And I run :tab-prev And I run :search-next + And I wait for "search found foo" in the log Then "foo" should be found # https://github.com/qutebrowser/qutebrowser/issues/2438 Scenario: Jumping to next match after clearing When I set general -> ignore-case to true And I run :search foo + And I wait for "search found foo" in the log And I run :search And I run :search-next + And I wait for "next_result found foo" in the log Then "foo" should be found ## :search-prev @@ -112,23 +134,33 @@ Feature: Searching on a page Scenario: Jumping to previous match When I set general -> ignore-case to true And I run :search foo + And I wait for "search found foo" in the log And I run :search-next + And I wait for "next_result found foo" in the log And I run :search-prev + And I wait for "prev_result found foo with flags FindBackward" in the log Then "foo" should be found Scenario: Jumping to previous match with count When I set general -> ignore-case to true And I run :search baz + And I wait for "search found baz" in the log And I run :search-next + And I wait for "next_result found baz" in the log And I run :search-next + And I wait for "next_result found baz" in the log And I run :search-prev with count 2 + And I wait for "prev_result found baz with flags FindBackward" in the log Then "baz" should be found Scenario: Jumping to previous match with --reverse When I set general -> ignore-case to true And I run :search --reverse foo + And I wait for "search found foo with flags FindBackward" in the log And I run :search-next + And I wait for "next_result found foo with flags FindBackward" in the log And I run :search-prev + And I wait for "prev_result found foo" in the log Then "Foo" should be found Scenario: Jumping to previous match without search @@ -141,14 +173,20 @@ Feature: Searching on a page Scenario: Wrapping around page When I run :search foo + And I wait for "search found foo" in the log And I run :search-next + And I wait for "next_result found foo" in the log And I run :search-next + And I wait for "next_result found foo" in the log Then "foo" should be found Scenario: Wrapping around page with --reverse When I run :search --reverse foo + And I wait for "search found foo with flags FindBackward" in the log And I run :search-next + And I wait for "next_result found foo with flags FindBackward" in the log And I run :search-next + And I wait for "next_result found foo with flags FindBackward" in the log Then "Foo" should be found # TODO: wrapping message with scrolling diff --git a/tests/end2end/features/test_search_bdd.py b/tests/end2end/features/test_search_bdd.py index d9866fe03..caf592ce6 100644 --- a/tests/end2end/features/test_search_bdd.py +++ b/tests/end2end/features/test_search_bdd.py @@ -19,7 +19,6 @@ import json -import pytest import pytest_bdd as bdd # pylint: disable=unused-import @@ -28,14 +27,16 @@ from end2end.features.test_yankpaste_bdd import init_fake_clipboard @bdd.then(bdd.parsers.parse('"{text}" should be found')) def check_found_text(request, quteproc, text): + if request.config.webengine: + # WORKAROUND + # This probably should work with Qt 5.9: + # https://codereview.qt-project.org/#/c/192920/ + # https://codereview.qt-project.org/#/c/192921/ + # https://bugreports.qt.io/browse/QTBUG-53134 + return quteproc.send_cmd(':yank selection') quteproc.wait_for(message='Setting fake clipboard: {}'.format( json.dumps(text))) -# After cancelling the search, the text (sometimes?) shows up as selected. -# However, we can't get it via ':yank selection' it seems? -pytestmark = pytest.mark.qtwebengine_skip("Searched text is not selected...") - - bdd.scenarios('search.feature') From f9055dc1e43390cba2c5d915335ecd80ba5cbd84 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 27 Apr 2017 21:14:28 +0200 Subject: [PATCH 167/825] Update changelog --- CHANGELOG.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 0fa268d02..5ec1d26be 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -28,6 +28,7 @@ Added filters. - New `--debug-flag` argument which replaces `--debug-exit` and `--pdb-postmortem`. +- New `tabs -> favicon-scale` option to scale up/down favicons. Changed ~~~~~~~ From 26b4d13c6faf6217bb116e15c01eec37c10dd976 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 27 Apr 2017 22:07:03 +0200 Subject: [PATCH 168/825] Update setuptools from 35.0.1 to 35.0.2 --- misc/requirements/requirements-pip.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-pip.txt b/misc/requirements/requirements-pip.txt index b015ca518..1eb264be8 100644 --- a/misc/requirements/requirements-pip.txt +++ b/misc/requirements/requirements-pip.txt @@ -3,6 +3,6 @@ appdirs==1.4.3 packaging==16.8 pyparsing==2.2.0 -setuptools==35.0.1 +setuptools==35.0.2 six==1.10.0 wheel==0.29.0 From 4f92fe689578279b148876ad67f25bc2cc78849e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 28 Apr 2017 10:33:57 +0200 Subject: [PATCH 169/825] Add an assert for tag_name we get from JS This should help tracking down #2569 once we get another report about it. --- qutebrowser/browser/webengine/webengineelem.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/webengine/webengineelem.py b/qutebrowser/browser/webengine/webengineelem.py index 1147b8475..8d364c620 100644 --- a/qutebrowser/browser/webengine/webengineelem.py +++ b/qutebrowser/browser/webengine/webengineelem.py @@ -88,7 +88,9 @@ class WebEngineElement(webelem.AbstractWebElement): The returned name will always be lower-case. """ - return self._js_dict['tag_name'].lower() + tag = self._js_dict['tag_name'] + assert isinstance(tag, str), tag + return tag.lower() def outer_xml(self): """Get the full HTML representation of this element.""" From 06e317ac53efcecfb772f1c192970740a3f3c839 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 28 Apr 2017 13:58:29 +0200 Subject: [PATCH 170/825] Do type checks on values we get from JS --- .../browser/webengine/webengineelem.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/qutebrowser/browser/webengine/webengineelem.py b/qutebrowser/browser/webengine/webengineelem.py index 8d364c620..9c15884f1 100644 --- a/qutebrowser/browser/webengine/webengineelem.py +++ b/qutebrowser/browser/webengine/webengineelem.py @@ -39,6 +39,37 @@ class WebEngineElement(webelem.AbstractWebElement): def __init__(self, js_dict, tab): super().__init__(tab) + # Do some sanity checks on the data we get from JS + js_dict_types = { + 'id': int, + 'text': str, + 'value': str, + 'tag_name': str, + 'outer_xml': str, + 'class_name': str, + 'rects': list, + 'attributes': dict, + } + assert set(js_dict.keys()).issubset(js_dict_types.keys()) + for name, typ in js_dict_types.items(): + if name in js_dict and not isinstance(js_dict[name], typ): + raise TypeError("Got {} for {} from JS but expected {}: {}".format( + type(js_dict[name]), name, typ, js_dict)) + for name, value in js_dict['attributes'].items(): + if not isinstance(name, str): + raise TypeError("Got {} ({}) for attribute name from JS: " + "{}".format(name, type(name), js_dict)) + if not isinstance(value, str): + raise TypeError("Got {} ({}) for attribute {} from JS: " + "{}".format(value, type(value), name, js_dict)) + for rect in js_dict['rects']: + assert set(rect.keys()) == {'top', 'right', 'bottom', 'left', + 'height', 'width'}, rect.keys() + for value in rect.values(): + if not isinstance(value, (int, float)): + raise TypeError("Got {} ({}) for rect from JS: " + "{}".format(value, type(value), js_dict)) + self._id = js_dict['id'] self._js_dict = js_dict From 513f83d44600479b24090e2ab50ef6b0ec459354 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 28 Apr 2017 14:28:28 +0200 Subject: [PATCH 171/825] Try harder to get tag name from element This could happen for any of the attributes, but for tagName this actually happens in the wild... Since elem.tagName is equal to elem.nodeName we just try to use this. Fixes #2569 --- qutebrowser/javascript/webelem.js | 10 +++++++++- tests/end2end/features/misc.feature | 9 +++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/qutebrowser/javascript/webelem.js b/qutebrowser/javascript/webelem.js index 206cdf129..749dfcc72 100644 --- a/qutebrowser/javascript/webelem.js +++ b/qutebrowser/javascript/webelem.js @@ -52,12 +52,20 @@ window._qutebrowser.webelem = (function() { "id": id, "text": elem.text, "value": elem.value, - "tag_name": elem.tagName, "outer_xml": elem.outerHTML, "class_name": elem.className, "rects": [], // Gets filled up later }; + // https://github.com/qutebrowser/qutebrowser/issues/2569 + if (typeof elem.tagName === "string") { + out.tag_name = elem.tagName; + } else if (typeof elem.nodeName === "string") { + out.tag_name = elem.nodeName; + } else { + out.tag_name = ""; + } + var attributes = {}; for (var i = 0; i < elem.attributes.length; ++i) { var attr = elem.attributes[i]; diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index 83de1c822..171fdf476 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -726,3 +726,12 @@ Feature: Various utility commands. Then no crash should happen And the following tabs should be open: - data/numbers/3.txt (active) + + ## Bugs + + # https://github.com/qutebrowser/qutebrowser/issues/2569 + Scenario: Clicking on form element with tagName child + When I open data/issue2569.html + And I run :click-element id theform + And I wait for "Clicked editable element!" in the log + Then no crash should happen From 0c653c4703848085f79b64b2b4c8b7360c78ab24 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 28 Apr 2017 14:48:08 +0200 Subject: [PATCH 172/825] Handle elem.className in webelem.js --- qutebrowser/javascript/webelem.js | 8 +++++++- tests/end2end/features/misc.feature | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/qutebrowser/javascript/webelem.js b/qutebrowser/javascript/webelem.js index 749dfcc72..3b0e324dd 100644 --- a/qutebrowser/javascript/webelem.js +++ b/qutebrowser/javascript/webelem.js @@ -53,7 +53,6 @@ window._qutebrowser.webelem = (function() { "text": elem.text, "value": elem.value, "outer_xml": elem.outerHTML, - "class_name": elem.className, "rects": [], // Gets filled up later }; @@ -66,6 +65,13 @@ window._qutebrowser.webelem = (function() { out.tag_name = ""; } + if (typeof elem.className === "string") { + out.class_name = elem.className; + } else { + // e.g. SVG elements + out.class_name = ""; + } + var attributes = {}; for (var i = 0; i < elem.attributes.length; ++i) { var attr = elem.attributes[i]; diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index 171fdf476..b65939ec1 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -735,3 +735,9 @@ Feature: Various utility commands. And I run :click-element id theform And I wait for "Clicked editable element!" in the log Then no crash should happen + + Scenario: Clicking on svg element + When I open data/issue2569.html + And I run :click-element id icon + And I wait for "Clicked non-editable element!" in the log + Then no crash should happen From 571f0c448695dd35576a44e4056dd0c844c43ee6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 28 Apr 2017 14:57:14 +0200 Subject: [PATCH 173/825] Loosen JS value type check --- qutebrowser/browser/webengine/webengineelem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/browser/webengine/webengineelem.py b/qutebrowser/browser/webengine/webengineelem.py index 9c15884f1..b033fc90c 100644 --- a/qutebrowser/browser/webengine/webengineelem.py +++ b/qutebrowser/browser/webengine/webengineelem.py @@ -43,7 +43,7 @@ class WebEngineElement(webelem.AbstractWebElement): js_dict_types = { 'id': int, 'text': str, - 'value': str, + 'value': (str, int, float), 'tag_name': str, 'outer_xml': str, 'class_name': str, From 6458c692cbb97cc28f8d88e8098b54074b47e009 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 28 Apr 2017 15:15:32 +0200 Subject: [PATCH 174/825] Improve JS value type checks --- qutebrowser/browser/commands.py | 1 + qutebrowser/browser/webkit/webkitelem.py | 4 +++- tests/end2end/features/misc.feature | 6 ++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 8d49ddef8..d2fd1bca0 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1505,6 +1505,7 @@ class CommandDispatcher: if text is None: message.error("Could not get text from the focused element.") return + assert isinstance(text, str), text ed = editor.ExternalEditor(self._tabbed_browser) ed.editing_finished.connect(functools.partial( diff --git a/qutebrowser/browser/webkit/webkitelem.py b/qutebrowser/browser/webkit/webkitelem.py index 34209c290..ba15e4a66 100644 --- a/qutebrowser/browser/webkit/webkitelem.py +++ b/qutebrowser/browser/webkit/webkitelem.py @@ -112,7 +112,9 @@ class WebKitElement(webelem.AbstractWebElement): def value(self): self._check_vanished() - return self._elem.evaluateJavaScript('this.value') + val = self._elem.evaluateJavaScript('this.value') + assert isinstance(val, (int, float, str)), val + return val def set_value(self, value): self._check_vanished() diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index b65939ec1..2260298d7 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -741,3 +741,9 @@ Feature: Various utility commands. And I run :click-element id icon And I wait for "Clicked non-editable element!" in the log Then no crash should happen + + Scenario: Clicking on li element + When I open data/issue2569.html + And I run :click-element id listitem + And I wait for "Clicked non-editable element!" in the log + Then no crash should happen From a2c8e093f4c7765d2619f82fe0662e15be633645 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 28 Apr 2017 15:16:36 +0200 Subject: [PATCH 175/825] Move webelement checks to javascript.feature --- tests/end2end/features/javascript.feature | 21 +++++++++++++++++++++ tests/end2end/features/misc.feature | 21 --------------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/tests/end2end/features/javascript.feature b/tests/end2end/features/javascript.feature index ab96866be..7c601f4e1 100644 --- a/tests/end2end/features/javascript.feature +++ b/tests/end2end/features/javascript.feature @@ -73,3 +73,24 @@ Feature: Javascript stuff When I set content -> allow-javascript to false And I run :jseval console.log('jseval executed') Then the javascript message "jseval executed" should be logged + + ## webelement issues (mostly with QtWebEngine) + + # https://github.com/qutebrowser/qutebrowser/issues/2569 + Scenario: Clicking on form element with tagName child + When I open data/issue2569.html + And I run :click-element id theform + And I wait for "Clicked editable element!" in the log + Then no crash should happen + + Scenario: Clicking on svg element + When I open data/issue2569.html + And I run :click-element id icon + And I wait for "Clicked non-editable element!" in the log + Then no crash should happen + + Scenario: Clicking on li element + When I open data/issue2569.html + And I run :click-element id listitem + And I wait for "Clicked non-editable element!" in the log + Then no crash should happen diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index 2260298d7..83de1c822 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -726,24 +726,3 @@ Feature: Various utility commands. Then no crash should happen And the following tabs should be open: - data/numbers/3.txt (active) - - ## Bugs - - # https://github.com/qutebrowser/qutebrowser/issues/2569 - Scenario: Clicking on form element with tagName child - When I open data/issue2569.html - And I run :click-element id theform - And I wait for "Clicked editable element!" in the log - Then no crash should happen - - Scenario: Clicking on svg element - When I open data/issue2569.html - And I run :click-element id icon - And I wait for "Clicked non-editable element!" in the log - Then no crash should happen - - Scenario: Clicking on li element - When I open data/issue2569.html - And I run :click-element id listitem - And I wait for "Clicked non-editable element!" in the log - Then no crash should happen From 20da495376f9131d305b69c90a1b4d32efd136c3 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 28 Apr 2017 15:22:29 +0200 Subject: [PATCH 176/825] Add missing file --- tests/end2end/data/issue2569.html | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 tests/end2end/data/issue2569.html diff --git a/tests/end2end/data/issue2569.html b/tests/end2end/data/issue2569.html new file mode 100644 index 000000000..4bff70e14 --- /dev/null +++ b/tests/end2end/data/issue2569.html @@ -0,0 +1,16 @@ + + + Form with tagName child + + +
+ + + + + + + +
  • List item
+ + From 5ed870e0c64149a57d9cbe254d5e2bdf235a3709 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 28 Apr 2017 16:29:44 +0200 Subject: [PATCH 177/825] Fix lint --- qutebrowser/browser/webengine/webengineelem.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/webengine/webengineelem.py b/qutebrowser/browser/webengine/webengineelem.py index b033fc90c..11d663df2 100644 --- a/qutebrowser/browser/webengine/webengineelem.py +++ b/qutebrowser/browser/webengine/webengineelem.py @@ -53,8 +53,9 @@ class WebEngineElement(webelem.AbstractWebElement): assert set(js_dict.keys()).issubset(js_dict_types.keys()) for name, typ in js_dict_types.items(): if name in js_dict and not isinstance(js_dict[name], typ): - raise TypeError("Got {} for {} from JS but expected {}: {}".format( - type(js_dict[name]), name, typ, js_dict)) + raise TypeError("Got {} for {} from JS but expected {}: " + "{}".format(type(js_dict[name]), name, typ, + js_dict)) for name, value in js_dict['attributes'].items(): if not isinstance(name, str): raise TypeError("Got {} ({}) for attribute name from JS: " From 11383169d83f1ff14d2df40e6963221e46ab29d1 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Fri, 28 Apr 2017 17:27:11 +0200 Subject: [PATCH 178/825] Update codecov from 2.0.8 to 2.0.9 --- misc/requirements/requirements-codecov.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-codecov.txt b/misc/requirements/requirements-codecov.txt index 8337d667d..8514ce608 100644 --- a/misc/requirements/requirements-codecov.txt +++ b/misc/requirements/requirements-codecov.txt @@ -1,5 +1,5 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -codecov==2.0.8 +codecov==2.0.9 coverage==4.3.4 requests==2.13.0 From c8090b5388f6f18c5cfb6ad1f5392eceb11ea7b4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 28 Apr 2017 17:29:19 +0200 Subject: [PATCH 179/825] Don't wait for click message in webelem tests Looks like we get this sometimes: ----> Waiting for 'Clicked non-editable element!' in the log 14:02:14.976 DEBUG webview webkittab:find_at_pos:618 Hit test result element is null! 14:02:14.976 DEBUG mouse mouse:_mousepress_insertmode_cb:149 Got None element, scheduling check on mouse release 14:02:14.977 DEBUG mouse webview:mousePressEvent:299 Normal click, setting normal target 14:02:14.978 DEBUG mouse mouse:mouserelease_insertmode_cb:173 Element vanished! --- tests/end2end/features/javascript.feature | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/end2end/features/javascript.feature b/tests/end2end/features/javascript.feature index 7c601f4e1..c0c5344d2 100644 --- a/tests/end2end/features/javascript.feature +++ b/tests/end2end/features/javascript.feature @@ -80,17 +80,17 @@ Feature: Javascript stuff Scenario: Clicking on form element with tagName child When I open data/issue2569.html And I run :click-element id theform - And I wait for "Clicked editable element!" in the log + And I wait for "Sending fake click to *" in the log Then no crash should happen Scenario: Clicking on svg element When I open data/issue2569.html And I run :click-element id icon - And I wait for "Clicked non-editable element!" in the log + And I wait for "Sending fake click to *" in the log Then no crash should happen Scenario: Clicking on li element When I open data/issue2569.html And I run :click-element id listitem - And I wait for "Clicked non-editable element!" in the log + And I wait for "Sending fake click to *" in the log Then no crash should happen From 76ec465f6703bc7daa81ccb71a8331474dc99489 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 28 Apr 2017 17:39:36 +0200 Subject: [PATCH 180/825] Allow to set cookies-store at runtime with Qt 5.9 Fixes #2588 --- CHANGELOG.asciidoc | 2 ++ doc/help/settings.asciidoc | 4 ++-- qutebrowser/browser/webengine/webenginesettings.py | 14 +++++++++----- qutebrowser/config/configdata.py | 2 +- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 5ec1d26be..dd79a1b2b 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -50,6 +50,8 @@ Changed - When ui -> message-timeout is set to 0, messages are now never cleared. - Middle/right-clicking the blank parts of the tab bar (when vertical) now closes the current tab. +- With Qt 5.9, `content -> cookies-store` can now be set on QtWebEngine without + a restart. Fixed ~~~~~ diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 68ca0a118..b2479f959 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -175,7 +175,7 @@ |<>|Whether locally loaded documents are allowed to access remote urls. |<>|Whether locally loaded documents are allowed to access other local urls. |<>|Control which cookies to accept. -|<>|Whether to store cookies. Note this option needs a restart with QtWebEngine. +|<>|Whether to store cookies. Note this option needs a restart with QtWebEngine on Qt < 5.9. |<>|List of URLs of lists which contain hosts to block. |<>|Whether host blocking is enabled. |<>|List of domains that should always be loaded, despite being ad-blocked. @@ -1616,7 +1616,7 @@ This setting is only available with the QtWebKit backend. [[content-cookies-store]] === cookies-store -Whether to store cookies. Note this option needs a restart with QtWebEngine. +Whether to store cookies. Note this option needs a restart with QtWebEngine on Qt < 5.9. Valid values: diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index b029b4fd5..740db489b 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -34,7 +34,8 @@ from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile, from qutebrowser.browser import shared from qutebrowser.config import config, websettings -from qutebrowser.utils import objreg, utils, standarddir, javascript, log +from qutebrowser.utils import (objreg, utils, standarddir, javascript, log, + qtutils) class Attribute(websettings.Attribute): @@ -177,7 +178,8 @@ def init(args): _init_stylesheet(profile) # We need to do this here as a WORKAROUND for # https://bugreports.qt.io/browse/QTBUG-58650 - PersistentCookiePolicy().set(config.get('content', 'cookies-store')) + if not qtutils.version_check('5.9'): + PersistentCookiePolicy().set(config.get('content', 'cookies-store')) Attribute(QWebEngineSettings.FullScreenSupportEnabled).set(True) @@ -221,9 +223,6 @@ MAPPINGS = { Attribute(QWebEngineSettings.LocalContentCanAccessRemoteUrls), 'local-content-can-access-file-urls': Attribute(QWebEngineSettings.LocalContentCanAccessFileUrls), - # https://bugreports.qt.io/browse/QTBUG-58650 - # 'cookies-store': - # PersistentCookiePolicy(), 'webgl': Attribute(QWebEngineSettings.WebGLEnabled), }, @@ -301,3 +300,8 @@ try: except AttributeError: # Added in Qt 5.8 pass + + +if qtutils.version_check('5.9'): + # https://bugreports.qt.io/browse/QTBUG-58650 + MAPPINGS['content']['cookies-store'] = PersistentCookiePolicy() diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 2de0c3eb2..347edc53f 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -915,7 +915,7 @@ def data(readonly=False): ('cookies-store', SettingValue(typ.Bool(), 'true'), "Whether to store cookies. Note this option needs a restart with " - "QtWebEngine."), + "QtWebEngine on Qt < 5.9."), ('host-block-lists', SettingValue( From 421aa0d319c62c37db6433d2acb0e665250d4c75 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 28 Apr 2017 19:11:02 +0200 Subject: [PATCH 181/825] Also try harder to get text content --- qutebrowser/javascript/webelem.js | 7 ++++++- tests/end2end/data/issue2569.html | 12 +++++++++++- tests/end2end/features/javascript.feature | 8 +++++++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/qutebrowser/javascript/webelem.js b/qutebrowser/javascript/webelem.js index 3b0e324dd..f9bd7d1af 100644 --- a/qutebrowser/javascript/webelem.js +++ b/qutebrowser/javascript/webelem.js @@ -50,7 +50,6 @@ window._qutebrowser.webelem = (function() { var out = { "id": id, - "text": elem.text, "value": elem.value, "outer_xml": elem.outerHTML, "rects": [], // Gets filled up later @@ -72,6 +71,12 @@ window._qutebrowser.webelem = (function() { out.class_name = ""; } + if (typeof elem.textContent === "string") { + out.text = elem.textContent; + } else if (typeof elem.text === "string") { + out.text = elem.text; + } // else: don't add the text at all + var attributes = {}; for (var i = 0; i < elem.attributes.length; ++i) { var attr = elem.attributes[i]; diff --git a/tests/end2end/data/issue2569.html b/tests/end2end/data/issue2569.html index 4bff70e14..8f613be2d 100644 --- a/tests/end2end/data/issue2569.html +++ b/tests/end2end/data/issue2569.html @@ -1,11 +1,21 @@ + Form with tagName child -
+ + + +
+ + + diff --git a/tests/end2end/features/javascript.feature b/tests/end2end/features/javascript.feature index c0c5344d2..7ea712d15 100644 --- a/tests/end2end/features/javascript.feature +++ b/tests/end2end/features/javascript.feature @@ -79,7 +79,13 @@ Feature: Javascript stuff # https://github.com/qutebrowser/qutebrowser/issues/2569 Scenario: Clicking on form element with tagName child When I open data/issue2569.html - And I run :click-element id theform + And I run :click-element id tagnameform + And I wait for "Sending fake click to *" in the log + Then no crash should happen + + Scenario: Clicking on form element with text child + When I open data/issue2569.html + And I run :click-element id textform And I wait for "Sending fake click to *" in the log Then no crash should happen From a5b1c293a4ad6d20c0c6165868cdb06ad2bec4ea Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 28 Apr 2017 20:29:20 +0200 Subject: [PATCH 182/825] Ignore comment position with eslint --- qutebrowser/javascript/.eslintrc.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qutebrowser/javascript/.eslintrc.yaml b/qutebrowser/javascript/.eslintrc.yaml index ef7ed00b5..0fbeea904 100644 --- a/qutebrowser/javascript/.eslintrc.yaml +++ b/qutebrowser/javascript/.eslintrc.yaml @@ -38,3 +38,5 @@ rules: max-len: ["error", {"ignoreUrls": true}] capitalized-comments: "off" prefer-destructuring: "off" + line-comment-position: "off" + no-inline-comments: "off" From 8101fe99a8c483af4d868972c354d3cee63bd1a2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 28 Apr 2017 20:51:38 +0200 Subject: [PATCH 183/825] Fix starting with Python 2 Fixes #2567 --- qutebrowser/qutebrowser.py | 4 ++-- tests/end2end/test_invocations.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index cd9ec5938..01054f14d 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -23,7 +23,6 @@ import sys import json import qutebrowser -from qutebrowser.utils import log try: from qutebrowser.misc.checkpyver import check_python_version except ImportError: @@ -38,6 +37,7 @@ except ImportError: sys.stderr.flush() sys.exit(100) check_python_version() +from qutebrowser.utils import log import argparse from qutebrowser.misc import earlyinit @@ -130,7 +130,7 @@ def directory(arg): raise argparse.ArgumentTypeError("Invalid empty value") -def logfilter_error(logfilter: str): +def logfilter_error(logfilter): """Validate logger names passed to --logfilter. Args: diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py index ed0222014..38d0e6eb9 100644 --- a/tests/end2end/test_invocations.py +++ b/tests/end2end/test_invocations.py @@ -19,6 +19,7 @@ """Test starting qutebrowser with special arguments/environments.""" +import subprocess import socket import sys import logging @@ -254,3 +255,15 @@ def test_command_on_start(request, quteproc_new): quteproc_new.start(args) quteproc_new.send_cmd(':quit') quteproc_new.wait_for_quit() + + +def test_launching_with_python2(): + try: + proc = subprocess.Popen(['python2', '-m', 'qutebrowser', + '--no-err-windows'], stderr=subprocess.PIPE) + except FileNotFoundError: + pytest.skip("python2 not found") + _stdout, stderr = proc.communicate() + assert proc.returncode == 1 + error = "At least Python 3.4 is required to run qutebrowser" + assert stderr.decode('ascii').startswith(error) From bffdea671998f66ec0add9a6c88adc2ba3c152a9 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 28 Apr 2017 21:36:02 +0200 Subject: [PATCH 184/825] Read qWebKitVersion in qtutils.is_webkit_ng. This means we need to try and import qWebKitVersion in qtutils, but better there than at every place which calls it. --- qutebrowser/browser/qutescheme.py | 9 ++------- qutebrowser/browser/webkit/tabhistory.py | 3 +-- qutebrowser/browser/webkit/webkitsettings.py | 4 ++-- qutebrowser/utils/qtutils.py | 14 +++++++------- qutebrowser/utils/version.py | 3 +-- tests/end2end/conftest.py | 14 ++------------ tests/unit/utils/test_qtutils.py | 5 +++-- tests/unit/utils/test_version.py | 6 +++--- 8 files changed, 21 insertions(+), 37 deletions(-) diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index bca93e3c8..accdab4ba 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -276,15 +276,10 @@ def qute_history(url): return 'text/html', json.dumps(history_data(start_time)) else: - try: - from PyQt5.QtWebKit import qWebKitVersion - is_webkit_ng = qtutils.is_qtwebkit_ng(qWebKitVersion()) - except ImportError: # pragma: no cover - is_webkit_ng = False - if ( config.get('content', 'allow-javascript') and - (objects.backend == usertypes.Backend.QtWebEngine or is_webkit_ng) + (objects.backend == usertypes.Backend.QtWebEngine or + qtutils.is_qtwebkit_ng()) ): return 'text/html', jinja.render( 'history.html', diff --git a/qutebrowser/browser/webkit/tabhistory.py b/qutebrowser/browser/webkit/tabhistory.py index d2efca257..3f80676a9 100644 --- a/qutebrowser/browser/webkit/tabhistory.py +++ b/qutebrowser/browser/webkit/tabhistory.py @@ -21,7 +21,6 @@ from PyQt5.QtCore import QByteArray, QDataStream, QIODevice, QUrl -from PyQt5.QtWebKit import qWebKitVersion from qutebrowser.utils import qtutils @@ -181,7 +180,7 @@ def serialize(items): else: current_idx = 0 - if qtutils.is_qtwebkit_ng(qWebKitVersion()): + if qtutils.is_qtwebkit_ng(): _serialize_ng(items, current_idx, stream) else: _serialize_old(items, current_idx, stream) diff --git a/qutebrowser/browser/webkit/webkitsettings.py b/qutebrowser/browser/webkit/webkitsettings.py index caeef296f..747af9b5d 100644 --- a/qutebrowser/browser/webkit/webkitsettings.py +++ b/qutebrowser/browser/webkit/webkitsettings.py @@ -26,7 +26,7 @@ Module attributes: import os.path -from PyQt5.QtWebKit import QWebSettings, qWebKitVersion +from PyQt5.QtWebKit import QWebSettings from qutebrowser.config import config, websettings from qutebrowser.utils import standarddir, objreg, urlutils, qtutils, message @@ -90,7 +90,7 @@ def _set_user_stylesheet(): def _init_private_browsing(): if config.get('general', 'private-browsing'): - if qtutils.is_qtwebkit_ng(qWebKitVersion()): + if qtutils.is_qtwebkit_ng(): message.warning("Private browsing is not fully implemented by " "QtWebKit-NG!") QWebSettings.setIconDatabasePath('') diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index adf0b2737..72092cfee 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -36,6 +36,10 @@ import contextlib from PyQt5.QtCore import (qVersion, QEventLoop, QDataStream, QByteArray, QIODevice, QSaveFile, QT_VERSION_STR) from PyQt5.QtWidgets import QApplication +try: + from PyQt5.QtWebKit import qWebKitVersion +except ImportError: + pass from qutebrowser.utils import log @@ -100,13 +104,9 @@ def version_check(version, exact=False, strict=False): return result -def is_qtwebkit_ng(version): - """Check if the given version is QtWebKit-NG. - - This is typically used as is_webkit_ng(qWebKitVersion) but we don't want to - have QtWebKit imports in here. - """ - return (pkg_resources.parse_version(version) > +def is_qtwebkit_ng(): + """Check if the given version is QtWebKit-NG.""" + return (pkg_resources.parse_version(qWebKitVersion()) > pkg_resources.parse_version('538.1')) diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index e6a011f44..0565281bc 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -257,8 +257,7 @@ def _backend(): """Get the backend line with relevant information.""" if objects.backend == usertypes.Backend.QtWebKit: return 'QtWebKit{} (WebKit {})'.format( - '-NG' if qtutils.is_qtwebkit_ng(qWebKitVersion()) else '', - qWebKitVersion()) + '-NG' if qtutils.is_qtwebkit_ng() else '', qWebKitVersion()) else: webengine = usertypes.Backend.QtWebEngine assert objects.backend == webengine, objects.backend diff --git a/tests/end2end/conftest.py b/tests/end2end/conftest.py index 7d1c0e90d..b374f53dd 100644 --- a/tests/end2end/conftest.py +++ b/tests/end2end/conftest.py @@ -136,16 +136,6 @@ if not getattr(sys, 'frozen', False): def pytest_collection_modifyitems(config, items): """Apply @qtwebengine_* markers; skip unittests with QUTE_BDD_WEBENGINE.""" - if config.webengine: - qtwebkit_ng_used = False - else: - try: - from PyQt5.QtWebKit import qWebKitVersion - except ImportError: - qtwebkit_ng_used = False - else: - qtwebkit_ng_used = qtutils.is_qtwebkit_ng(qWebKitVersion()) - markers = [ ('qtwebengine_todo', 'QtWebEngine TODO', pytest.mark.xfail, config.webengine), @@ -154,9 +144,9 @@ def pytest_collection_modifyitems(config, items): ('qtwebkit_skip', 'Skipped with QtWebKit', pytest.mark.skipif, not config.webengine), ('qtwebkit_ng_xfail', 'Failing with QtWebKit-NG', pytest.mark.xfail, - qtwebkit_ng_used), + not config.webengine and qtutils.is_qtwebkit_ng()), ('qtwebkit_ng_skip', 'Skipped with QtWebKit-NG', pytest.mark.skipif, - qtwebkit_ng_used), + not config.webengine and qtutils.is_qtwebkit_ng()), ('qtwebengine_flaky', 'Flaky with QtWebEngine', pytest.mark.skipif, config.webengine), ('qtwebengine_osx_xfail', 'Fails on OS X with QtWebEngine', diff --git a/tests/unit/utils/test_qtutils.py b/tests/unit/utils/test_qtutils.py index 74da2ac4f..e0b4bf934 100644 --- a/tests/unit/utils/test_qtutils.py +++ b/tests/unit/utils/test_qtutils.py @@ -88,8 +88,9 @@ def test_version_check(monkeypatch, qversion, compiled, version, exact, ('538.1', False), # Qt 5.8 ('602.1', True) # QtWebKit-NG TP5 ]) -def test_is_qtwebkit_ng(version, ng): - assert qtutils.is_qtwebkit_ng(version) == ng +def test_is_qtwebkit_ng(monkeypatch, version, ng): + monkeypatch.setattr(qtutils, 'qWebKitVersion', lambda: version) + assert qtutils.is_qtwebkit_ng() == ng class TestCheckOverflow: diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py index c6e629ef1..13c0a5d13 100644 --- a/tests/unit/utils/test_version.py +++ b/tests/unit/utils/test_version.py @@ -725,13 +725,13 @@ def test_version_output(git_commit, frozen, style, with_webkit, stubs, patches['objects.backend'] = usertypes.Backend.QtWebKit patches['QWebEngineProfile'] = None if with_webkit == 'ng': - patches['qtutils.is_qtwebkit_ng'] = lambda v: True + patches['qtutils.is_qtwebkit_ng'] = lambda: True substitutions['backend'] = 'QtWebKit-NG (WebKit WEBKIT VERSION)' else: - patches['qtutils.is_qtwebkit_ng'] = lambda v: False + patches['qtutils.is_qtwebkit_ng'] = lambda: False substitutions['backend'] = 'QtWebKit (WebKit WEBKIT VERSION)' else: - patches['qWebKitVersion'] = None + monkeypatch.delattr(version, 'qtutils.qWebKitVersion', raising=False) patches['objects.backend'] = usertypes.Backend.QtWebEngine patches['QWebEngineProfile'] = FakeWebEngineProfile substitutions['backend'] = 'QtWebEngine (Chromium CHROMIUMVERSION)' From 5bbd16c92a2b25574a21b7482c08b6f8d962bd64 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 28 Apr 2017 22:59:24 +0200 Subject: [PATCH 185/825] Fix qWebKitVersion issues --- qutebrowser/utils/qtutils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 72092cfee..9bd606f02 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -38,8 +38,8 @@ from PyQt5.QtCore import (qVersion, QEventLoop, QDataStream, QByteArray, from PyQt5.QtWidgets import QApplication try: from PyQt5.QtWebKit import qWebKitVersion -except ImportError: - pass +except ImportError: # pragma: no cover + qWebKitVersion = None from qutebrowser.utils import log @@ -106,6 +106,7 @@ def version_check(version, exact=False, strict=False): def is_qtwebkit_ng(): """Check if the given version is QtWebKit-NG.""" + assert qWebKitVersion is not None return (pkg_resources.parse_version(qWebKitVersion()) > pkg_resources.parse_version('538.1')) From 6e0d138d230ef0fb64e73ea8e900784e0a91f1b4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 29 Apr 2017 13:45:14 +0200 Subject: [PATCH 186/825] flake8 requirements: Pin flake8-docstrings to < 1.1.0 Closes #2593 --- misc/requirements/requirements-flake8.txt | 2 +- misc/requirements/requirements-flake8.txt-raw | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt index d30cb2dd9..f0205d262 100644 --- a/misc/requirements/requirements-flake8.txt +++ b/misc/requirements/requirements-flake8.txt @@ -4,7 +4,7 @@ flake8==2.6.2 # rq.filter: < 3.0.0 flake8-copyright==0.2.0 flake8-debugger==1.4.0 # rq.filter: != 2.0.0 flake8-deprecated==1.1 -flake8-docstrings==1.0.3 +flake8-docstrings==1.0.3 # rq.filter: < 1.1.0 flake8-future-import==0.4.3 flake8-mock==0.3 flake8-pep3101==1.0 diff --git a/misc/requirements/requirements-flake8.txt-raw b/misc/requirements/requirements-flake8.txt-raw index e235df395..aadae5e8b 100644 --- a/misc/requirements/requirements-flake8.txt-raw +++ b/misc/requirements/requirements-flake8.txt-raw @@ -2,7 +2,7 @@ flake8<3.0.0 flake8-copyright flake8-debugger!=2.0.0 flake8-deprecated -flake8-docstrings +flake8-docstrings<1.1.0 flake8-future-import flake8-mock flake8-pep3101 @@ -22,6 +22,7 @@ mccabe==0.6.1 # Waiting until flake8-putty updated #@ filter: flake8 < 3.0.0 #@ filter: pydocstyle < 2.0.0 +#@ filter: flake8-docstrings < 1.1.0 # https://github.com/JBKahn/flake8-debugger/issues/5 #@ filter: flake8-debugger != 2.0.0 From 64a4e33caa539b751acfc7e0eb76c871e8ab423d Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sun, 30 Apr 2017 12:29:12 +0200 Subject: [PATCH 187/825] Update flake8-tuple from 0.2.12 to 0.2.13 --- misc/requirements/requirements-flake8.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt index f0205d262..8cd1eb296 100644 --- a/misc/requirements/requirements-flake8.txt +++ b/misc/requirements/requirements-flake8.txt @@ -12,7 +12,7 @@ flake8-polyfill==1.0.1 flake8-putty==0.4.0 flake8-string-format==0.2.3 flake8-tidy-imports==1.0.6 -flake8-tuple==0.2.12 +flake8-tuple==0.2.13 mccabe==0.6.1 pep8-naming==0.4.1 pycodestyle==2.3.1 From 6ae6df9d74b9d94f56489b5d6a4586995aeda340 Mon Sep 17 00:00:00 2001 From: Steve Peak Date: Sun, 30 Apr 2017 14:55:35 -0400 Subject: [PATCH 188/825] Update codecov.yml --- codecov.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/codecov.yml b/codecov.yml index 6d4f1abce..47e3c919c 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,9 +1,7 @@ -status: - project: - enabled: no - patch: - enabled: no - changes: - enabled: no +coverage: + status: + project: off + patch: off + changes: off comment: off From fe02267de2f9bc164cb265c357770d8f56c68554 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 1 May 2017 12:43:14 +0200 Subject: [PATCH 189/825] Update pytest-bdd from 2.18.1 to 2.18.2 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 511300e96..e73d9fdf6 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -18,7 +18,7 @@ parse==1.8.0 parse-type==0.3.4 py==1.4.33 pytest==3.0.7 -pytest-bdd==2.18.1 +pytest-bdd==2.18.2 pytest-benchmark==3.0.0 pytest-catchlog==1.2.2 pytest-cov==2.4.0 From 64e144f3eb0fc0a14b06293b99423389c22e1c0a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 1 May 2017 13:52:46 +0200 Subject: [PATCH 190/825] Make text selectable in prompts --- qutebrowser/mainwindow/prompt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 358bcc80b..db91cc71a 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -475,6 +475,7 @@ class _BasePrompt(QWidget): if question.text is not None: # Not doing any HTML escaping here as the text can be formatted text_label = QLabel(question.text) + text_label.setTextInteractionFlags(Qt.TextSelectableByMouse) self._vbox.addWidget(text_label) def _init_key_label(self): From 7b4ab901e9e2896c02d2b6bb0700d2c687d8c7f3 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 2 May 2017 08:24:57 +0200 Subject: [PATCH 191/825] tests: Fix Chromium message matching --- tests/end2end/fixtures/quteprocess.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py index cc835693c..13d9c1043 100644 --- a/tests/end2end/fixtures/quteprocess.py +++ b/tests/end2end/fixtures/quteprocess.py @@ -77,9 +77,9 @@ def is_ignored_lowlevel_message(message): return True elif 'Unable to locate theme engine in module_path:' in message: return True - elif message == 'getrlimit(RLIMIT_NOFILE) failed': + elif 'getrlimit(RLIMIT_NOFILE) failed' in message: return True - elif message == 'Could not bind NETLINK socket: Address already in use': + elif 'Could not bind NETLINK socket: Address already in use' in message: return True return False From d5c5d09b18abcbc8250dea7c46566a253fce7b49 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 2 May 2017 09:12:06 +0200 Subject: [PATCH 192/825] Hopefully stabilize test_version When using QuteProcess here, we fight with it over who can read the output. Just use a raw QProcess instead. --- tests/end2end/test_invocations.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py index 38d0e6eb9..9c1e993cb 100644 --- a/tests/end2end/test_invocations.py +++ b/tests/end2end/test_invocations.py @@ -162,23 +162,24 @@ def test_optimize(request, quteproc_new, capfd, level): quteproc_new.wait_for_quit() +@pytest.mark.not_frozen def test_version(request): """Test invocation with --version argument.""" - args = ['--version'] + _base_args(request.config) + args = ['-m', 'qutebrowser', '--version'] + _base_args(request.config) # can't use quteproc_new here because it's confused by # early process termination - proc = quteprocess.QuteProc(request) - proc.proc.setProcessChannelMode(QProcess.SeparateChannels) + proc = QProcess() + proc.setProcessChannelMode(QProcess.SeparateChannels) - try: - proc.start(args) - proc.wait_for_quit() - except testprocess.ProcessExited: - assert proc.proc.exitStatus() == QProcess.NormalExit - else: - pytest.fail("Process did not exit!") + proc.start(sys.executable, args) + ok = proc.waitForStarted(2000) + assert ok + ok = proc.waitForFinished(2000) + assert ok + assert proc.exitStatus() == QProcess.NormalExit - output = bytes(proc.proc.readAllStandardOutput()).decode('utf-8') + output = bytes(proc.readAllStandardOutput()).decode('utf-8') + print(output) assert re.search(r'^qutebrowser\s+v\d+(\.\d+)', output) is not None From 7c6981e5129f5e514a1a6666485e59a9f7c182df Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 2 May 2017 10:41:51 +0200 Subject: [PATCH 193/825] Remove unused imports --- tests/end2end/test_invocations.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py index 9c1e993cb..d683e9c85 100644 --- a/tests/end2end/test_invocations.py +++ b/tests/end2end/test_invocations.py @@ -29,7 +29,6 @@ import pytest from PyQt5.QtCore import QProcess -from end2end.fixtures import quteprocess, testprocess from qutebrowser.utils import qtutils From 3b0bb6a831791c22a77f25aa2f3223c24e6ba628 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 2 May 2017 11:43:35 +0200 Subject: [PATCH 194/825] Update cheroot from 5.4.0 to 5.5.0 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index e73d9fdf6..627912f34 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -1,7 +1,7 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py beautifulsoup4==4.5.3 -cheroot==5.4.0 +cheroot==5.5.0 click==6.7 coverage==4.3.4 decorator==4.0.11 From a77cb447234e25c1680b231adc62616f0ad89f3a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 3 May 2017 08:41:49 +0200 Subject: [PATCH 195/825] Block all request methods in host blocker --- CHANGELOG.asciidoc | 1 + qutebrowser/browser/webengine/interceptor.py | 3 +-- qutebrowser/browser/webkit/network/networkmanager.py | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index dd79a1b2b..2d7937d22 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -52,6 +52,7 @@ Changed closes the current tab. - With Qt 5.9, `content -> cookies-store` can now be set on QtWebEngine without a restart. +- The adblocker now also blocks non-GET requests (e.g. POST) Fixed ~~~~~ diff --git a/qutebrowser/browser/webengine/interceptor.py b/qutebrowser/browser/webengine/interceptor.py index 350378147..76c29a6eb 100644 --- a/qutebrowser/browser/webengine/interceptor.py +++ b/qutebrowser/browser/webengine/interceptor.py @@ -57,8 +57,7 @@ class RequestInterceptor(QWebEngineUrlRequestInterceptor): info: QWebEngineUrlRequestInfo &info """ # FIXME:qtwebengine only block ads for NavigationTypeOther? - if (bytes(info.requestMethod()) == b'GET' and - self._host_blocker.is_blocked(info.requestUrl())): + if self._host_blocker.is_blocked(info.requestUrl()): log.webview.info("Request to {} blocked by host blocker.".format( info.requestUrl().host())) info.block(True) diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index 779d778bc..915a2082f 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -416,8 +416,7 @@ class NetworkManager(QNetworkAccessManager): req.setRawHeader(header, value) host_blocker = objreg.get('host-blocker') - if (op == QNetworkAccessManager.GetOperation and - host_blocker.is_blocked(req.url())): + if host_blocker.is_blocked(req.url()): log.webview.info("Request to {} blocked by host blocker.".format( req.url().host())) return networkreply.ErrorNetworkReply( From 120379dd217d8daaa6b48cb21c8cb3de7679bbec Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Wed, 22 Feb 2017 20:43:48 -0500 Subject: [PATCH 196/825] Benchmark url completion. This benchmark simulates what I expect to be the most common use-case for url completion: opening completion and entering several letters. --- tests/unit/completion/test_models.py | 42 +++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py index 84c87b270..a767476a4 100644 --- a/tests/unit/completion/test_models.py +++ b/tests/unit/completion/test_models.py @@ -26,7 +26,8 @@ import pytest from PyQt5.QtCore import QUrl from PyQt5.QtWidgets import QTreeView -from qutebrowser.completion.models import miscmodels, urlmodel, configmodel +from qutebrowser.completion.models import (miscmodels, urlmodel, configmodel, + sortfilter) from qutebrowser.browser import history from qutebrowser.config import sections, value @@ -498,3 +499,42 @@ def test_bind_completion(qtmodeltester, monkeypatch, stubs, config_stub, ('rock', "Alias for 'roll'", 'ro'), ] }) + + +def test_url_completion_benchmark(benchmark, config_stub, + quickmark_manager_stub, + bookmark_manager_stub, + web_history_stub): + """Benchmark url completion.""" + config_stub.data['completion'] = {'timestamp-format': '%Y-%m-%d', + 'web-history-max-items': 1000} + + entries = [history.Entry( + atime=i, + url=QUrl('http://example.com/{}'.format(i)), + title='title{}'.format(i)) + for i in range(100000)] + + web_history_stub.history_dict = collections.OrderedDict( + ((e.url_str(), e) for e in entries)) + + quickmark_manager_stub.marks = collections.OrderedDict( + (e.title, e.url_str()) + for e in entries[0:1000]) + + bookmark_manager_stub.marks = collections.OrderedDict( + (e.url_str(), e.title) + for e in entries[0:1000]) + + def bench(): + model = urlmodel.UrlCompletionModel() + filtermodel = sortfilter.CompletionFilterModel(model) + filtermodel.set_pattern('') + filtermodel.set_pattern('e') + filtermodel.set_pattern('ex') + filtermodel.set_pattern('ex ') + filtermodel.set_pattern('ex 1') + filtermodel.set_pattern('ex 12') + filtermodel.set_pattern('ex 123') + + benchmark(bench) From 2a4af0666bad617707a63c4b22678bf08362bb82 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 3 May 2017 18:36:32 +0200 Subject: [PATCH 197/825] Regenerate authors --- README.asciidoc | 3 ++- scripts/dev/src2asciidoc.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.asciidoc b/README.asciidoc index 7aa1afa66..b3457876b 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -183,7 +183,7 @@ Contributors, sorted by the number of commits in descending order: * Jimmy * Niklas Haas * Maciej Wołczyk -* Spreadyy +* sandrosc * Alexey "Averrin" Nabrodov * pkill9 * nanjekyejoannah @@ -273,6 +273,7 @@ Contributors, sorted by the number of commits in descending order: * Tobias Werth * Tim Harder * Thiago Barroso Perrotta +* Steve Peak * Sorokin Alexei * Simon Désaulniers * Rok Mandeljc diff --git a/scripts/dev/src2asciidoc.py b/scripts/dev/src2asciidoc.py index a8f019d9d..1b99022a7 100755 --- a/scripts/dev/src2asciidoc.py +++ b/scripts/dev/src2asciidoc.py @@ -437,6 +437,7 @@ def _get_authors(): 'Claire C.C': 'Claire Cavanaugh', 'Rahid': 'Maciej Wołczyk', 'Fritz V155 Reichwald': 'Fritz Reichwald', + 'Spreadyy': 'sandrosc', } ignored = ['pyup-bot'] commits = subprocess.check_output(['git', 'log', '--format=%aN']) From a320aa5ef7cb02a11ae36304d407f8170fd85364 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 3 May 2017 21:24:25 +0200 Subject: [PATCH 198/825] Disable renderer process crash workaround on Qt 5.9 --- qutebrowser/mainwindow/tabbedbrowser.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index f56bfc5a6..885c4dc78 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -673,11 +673,11 @@ class TabbedBrowser(tabwidget.TabWidget): else: raise ValueError("Invalid status {}".format(status)) - # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58698 - # FIXME:qtwebengine can we disable this with Qt 5.8.1? - self._remove_tab(tab, crashed=True) - if self.count() == 0: - self.tabopen(QUrl('about:blank')) + if not qtutils.version_check('5.9'): + # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58698 + self._remove_tab(tab, crashed=True) + if self.count() == 0: + self.tabopen(QUrl('about:blank')) def resizeEvent(self, e): """Extend resizeEvent of QWidget to emit a resized signal afterwards. From ebf3d208f60f6365d60aa44517365cb2e0b40d8f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 3 May 2017 21:25:00 +0200 Subject: [PATCH 199/825] Adjust Qt 5.8.1 check There's never going to be a 5.8.1 --- qutebrowser/browser/webengine/webenginedownloads.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/browser/webengine/webenginedownloads.py b/qutebrowser/browser/webengine/webenginedownloads.py index ca6e04c04..26b23d77f 100644 --- a/qutebrowser/browser/webengine/webenginedownloads.py +++ b/qutebrowser/browser/webengine/webenginedownloads.py @@ -147,7 +147,7 @@ def _get_suggested_filename(path): """ filename = os.path.basename(path) filename = re.sub(r'\([0-9]+\)(?=\.|$)', '', filename) - if not qtutils.version_check('5.8.1'): + if not qtutils.version_check('5.9'): # https://bugreports.qt.io/browse/QTBUG-58155 filename = urllib.parse.unquote(filename) # Doing basename a *second* time because there could be a %2F in From ea2d5e97e2b7dd9ec787c6501daa7c3142980e2d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 3 May 2017 21:31:09 +0200 Subject: [PATCH 200/825] Disable serialization crash check on Qt 5.9 --- qutebrowser/browser/webengine/webenginetab.py | 20 +++++++++---------- tests/end2end/features/tabs.feature | 8 +++++++- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index cedd59b1e..00c205b73 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -378,16 +378,16 @@ class WebEngineHistory(browsertab.AbstractHistory): return self._history.canGoForward() def serialize(self): - # WORKAROUND (remove this when we bump the requirements to 5.9) - # https://bugreports.qt.io/browse/QTBUG-59599 - if self._history.count() == 0: - raise browsertab.WebTabError("Can't serialize page without " - "history!") - # WORKAROUND (FIXME: remove this when we bump the requirements to 5.9?) - # https://github.com/qutebrowser/qutebrowser/issues/2289 - scheme = self._history.currentItem().url().scheme() - if scheme in ['view-source', 'chrome']: - raise browsertab.WebTabError("Can't serialize special URL!") + if not qtutils.version_check('5.9'): + # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-59599 + if self._history.count() == 0: + raise browsertab.WebTabError("Can't serialize page without " + "history!") + # WORKAROUND for + # https://github.com/qutebrowser/qutebrowser/issues/2289 + scheme = self._history.currentItem().url().scheme() + if scheme in ['view-source', 'chrome']: + raise browsertab.WebTabError("Can't serialize special URL!") return qtutils.serialize(self._history) def deserialize(self, data): diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature index 5e571d42d..748360bc3 100644 --- a/tests/end2end/features/tabs.feature +++ b/tests/end2end/features/tabs.feature @@ -607,12 +607,18 @@ Feature: Tab management title: Test title # https://github.com/qutebrowser/qutebrowser/issues/2289 - @qtwebkit_skip @qt>=5.8 + @qtwebkit_skip @qt==5.8.0 Scenario: Cloning a tab with a special URL When I open chrome://gpu And I run :tab-clone Then the error "Can't serialize special URL!" should be shown + @qtwebkit_skip @qt>=5.9 + Scenario: Cloning a tab with a special URL (Qt 5.9) + When I open chrome://gpu + And I run :tab-clone + Then no crash should happen + # :tab-detach Scenario: Detaching a tab From 90b0af97ce4d9f0952c201f47413a5b1bb93877d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 3 May 2017 21:31:09 +0200 Subject: [PATCH 201/825] Improve serialization crash check It now works correctly with view-source URLs and is disabled on Qt 5.9. Fixes #2289 See #2458 --- CHANGELOG.asciidoc | 1 + qutebrowser/browser/webengine/webenginetab.py | 9 ++++----- tests/end2end/features/conftest.py | 4 +++- tests/end2end/features/tabs.feature | 19 +++++++------------ tests/end2end/fixtures/quteprocess.py | 2 +- 5 files changed, 16 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 2d7937d22..7ff888acd 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -68,6 +68,7 @@ Fixed - Crash when unbinding an unbound key in the key config - Crash when using `:debug-log-filter` when `--filter` wasn't given on startup. - Crash with some invalid setting values +- Crash when cloning a view-source tab with QtWebEngine - Various rare crashes - Various styling issues with the tabbar and a crash with qt5ct diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 00c205b73..cc3dcf108 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -379,13 +379,12 @@ class WebEngineHistory(browsertab.AbstractHistory): def serialize(self): if not qtutils.version_check('5.9'): - # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-59599 - if self._history.count() == 0: - raise browsertab.WebTabError("Can't serialize page without " - "history!") # WORKAROUND for # https://github.com/qutebrowser/qutebrowser/issues/2289 - scheme = self._history.currentItem().url().scheme() + # Don't use the history's currentItem here, because of + # https://bugreports.qt.io/browse/QTBUG-59599 and because it doesn't + # contain view-source. + scheme = self._tab.url().scheme() if scheme in ['view-source', 'chrome']: raise browsertab.WebTabError("Can't serialize special URL!") return qtutils.serialize(self._history) diff --git a/tests/end2end/features/conftest.py b/tests/end2end/features/conftest.py index c65ea33ad..1e9c34462 100644 --- a/tests/end2end/features/conftest.py +++ b/tests/end2end/features/conftest.py @@ -174,13 +174,15 @@ def pdfjs_available(): @bdd.when(bdd.parsers.parse("I open {path}")) -def open_path(quteproc, path): +def open_path(quteproc, httpbin, path): """Open a URL. If used like "When I open ... in a new tab", the URL is opened in a new tab. With "... in a new window", it's opened in a new window. With "... as a URL", it's opened according to new-instance-open-target. """ + path = path.replace('(port)', str(httpbin.port)) + new_tab = False new_bg_tab = False new_window = False diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature index 748360bc3..19007428c 100644 --- a/tests/end2end/features/tabs.feature +++ b/tests/end2end/features/tabs.feature @@ -607,12 +607,19 @@ Feature: Tab management title: Test title # https://github.com/qutebrowser/qutebrowser/issues/2289 + @qtwebkit_skip @qt==5.8.0 Scenario: Cloning a tab with a special URL When I open chrome://gpu And I run :tab-clone Then the error "Can't serialize special URL!" should be shown + @qtwebkit_skip @qt<5.9 + Scenario: Cloning a tab with a view-source URL + When I open view-source:http://localhost:(port) + And I run :tab-clone + Then the error "Can't serialize special URL!" should be shown + @qtwebkit_skip @qt>=5.9 Scenario: Cloning a tab with a special URL (Qt 5.9) When I open chrome://gpu @@ -774,18 +781,6 @@ Feature: Tab management - data/numbers/2.txt - data/numbers/3.txt - # https://github.com/qutebrowser/qutebrowser/issues/2289 - @qtwebkit_skip @qt>=5.8 - Scenario: Undoing a tab with a special URL - Given I have a fresh instance - When I open data/numbers/1.txt - And I open chrome://gpu in a new tab - And I run :tab-close - And I run :undo - Then the error "Nothing to undo!" should be shown - And the following tabs should be open: - - data/numbers/1.txt (active) - # last-close # FIXME:qtwebengine diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py index 13d9c1043..5f99990d8 100644 --- a/tests/end2end/fixtures/quteprocess.py +++ b/tests/end2end/fixtures/quteprocess.py @@ -314,7 +314,7 @@ class QuteProc(testprocess.Process): URLs like about:... and qute:... are handled specially and returned verbatim. """ - special_schemes = ['about:', 'qute:', 'chrome:'] + special_schemes = ['about:', 'qute:', 'chrome:', 'view-source:'] if any(path.startswith(scheme) for scheme in special_schemes): return path else: From caa3be2277f33c692c4b1093d87a49128efe2fcb Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 4 May 2017 08:36:25 +0200 Subject: [PATCH 202/825] Update lazy-object-proxy from 1.2.2 to 1.3.0 --- misc/requirements/requirements-pylint-master.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-pylint-master.txt b/misc/requirements/requirements-pylint-master.txt index e438f86ed..9e9609c30 100644 --- a/misc/requirements/requirements-pylint-master.txt +++ b/misc/requirements/requirements-pylint-master.txt @@ -4,7 +4,7 @@ editdistance==0.3.1 github3.py==0.9.6 isort==4.2.5 -lazy-object-proxy==1.2.2 +lazy-object-proxy==1.3.0 mccabe==0.6.1 -e git+https://github.com/PyCQA/pylint.git#egg=pylint ./scripts/dev/pylint_checkers From 88545dec4da7b66986e47afb0cc6663c6ce1d33a Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 4 May 2017 08:36:26 +0200 Subject: [PATCH 203/825] Update lazy-object-proxy from 1.2.2 to 1.3.0 --- misc/requirements/requirements-pylint.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index 807ca5643..585b3f1a6 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -3,7 +3,7 @@ astroid==1.5.2 github3.py==0.9.6 isort==4.2.5 -lazy-object-proxy==1.2.2 +lazy-object-proxy==1.3.0 mccabe==0.6.1 pylint==1.7.1 ./scripts/dev/pylint_checkers From 2beba920e725faa74871524f91d67bede6b4859b Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Fri, 5 May 2017 15:51:18 +0200 Subject: [PATCH 204/825] Update lazy-object-proxy from 1.3.0 to 1.3.1 --- misc/requirements/requirements-pylint-master.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-pylint-master.txt b/misc/requirements/requirements-pylint-master.txt index 9e9609c30..8c08b4e7b 100644 --- a/misc/requirements/requirements-pylint-master.txt +++ b/misc/requirements/requirements-pylint-master.txt @@ -4,7 +4,7 @@ editdistance==0.3.1 github3.py==0.9.6 isort==4.2.5 -lazy-object-proxy==1.3.0 +lazy-object-proxy==1.3.1 mccabe==0.6.1 -e git+https://github.com/PyCQA/pylint.git#egg=pylint ./scripts/dev/pylint_checkers From 00c48427bcee63921705f85251d71795dab980bd Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Fri, 5 May 2017 15:51:20 +0200 Subject: [PATCH 205/825] Update lazy-object-proxy from 1.3.0 to 1.3.1 --- misc/requirements/requirements-pylint.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index 585b3f1a6..647661e2f 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -3,7 +3,7 @@ astroid==1.5.2 github3.py==0.9.6 isort==4.2.5 -lazy-object-proxy==1.3.0 +lazy-object-proxy==1.3.1 mccabe==0.6.1 pylint==1.7.1 ./scripts/dev/pylint_checkers From 7a3651426f0152c6b23ed4f83bab0eab2e763d91 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sun, 7 May 2017 16:42:20 +0200 Subject: [PATCH 206/825] Update beautifulsoup4 from 4.5.3 to 4.6.0 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 627912f34..a0580f49f 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -1,6 +1,6 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -beautifulsoup4==4.5.3 +beautifulsoup4==4.6.0 cheroot==5.5.0 click==6.7 coverage==4.3.4 From ee2a6ae6f026985a43d09307bd775991c23882d9 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 8 May 2017 07:26:25 +0200 Subject: [PATCH 207/825] Update coverage from 4.3.4 to 4.4 --- misc/requirements/requirements-codecov.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-codecov.txt b/misc/requirements/requirements-codecov.txt index 8514ce608..6c3864d96 100644 --- a/misc/requirements/requirements-codecov.txt +++ b/misc/requirements/requirements-codecov.txt @@ -1,5 +1,5 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py codecov==2.0.9 -coverage==4.3.4 +coverage==4.4 requests==2.13.0 From 2b0fc0f52efccd0004b2cd8f90f930852947f403 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 8 May 2017 07:26:27 +0200 Subject: [PATCH 208/825] Update coverage from 4.3.4 to 4.4 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 627912f34..eedfd3f50 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -3,7 +3,7 @@ beautifulsoup4==4.5.3 cheroot==5.5.0 click==6.7 -coverage==4.3.4 +coverage==4.4 decorator==4.0.11 EasyProcess==0.2.3 Flask==0.12.1 From 8052249b1b683c552219b082da5e15271203ba26 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 May 2017 06:13:35 +0200 Subject: [PATCH 209/825] Make check_coverage.py work with coverage 4.4 With coverage 4.4, the source name (qutebrowser/) is not added to the filename anymore. To adjust for that, we remove qutebrowser/ from all paths, and also make sure to remove it from what coverage returns (in case someone is running an older version). --- scripts/dev/check_coverage.py | 118 +++++++++++----------- tests/unit/scripts/test_check_coverage.py | 5 +- 2 files changed, 63 insertions(+), 60 deletions(-) diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py index c876acd1a..9025d8740 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -41,132 +41,132 @@ MsgType = enum.Enum('MsgType', 'insufficent_coverage, perfect_file') # A list of (test_file, tested_file) tuples. test_file can be None. PERFECT_FILES = [ (None, - 'qutebrowser/commands/cmdexc.py'), + 'commands/cmdexc.py'), ('tests/unit/commands/test_cmdutils.py', - 'qutebrowser/commands/cmdutils.py'), + 'commands/cmdutils.py'), ('tests/unit/commands/test_argparser.py', - 'qutebrowser/commands/argparser.py'), + 'commands/argparser.py'), ('tests/unit/browser/webkit/test_cache.py', - 'qutebrowser/browser/webkit/cache.py'), + 'browser/webkit/cache.py'), ('tests/unit/browser/webkit/test_cookies.py', - 'qutebrowser/browser/webkit/cookies.py'), + 'browser/webkit/cookies.py'), ('tests/unit/browser/webkit/test_history.py', - 'qutebrowser/browser/history.py'), + 'browser/history.py'), ('tests/unit/browser/webkit/test_history.py', - 'qutebrowser/browser/webkit/webkithistory.py'), + 'browser/webkit/webkithistory.py'), ('tests/unit/browser/webkit/http/test_http.py', - 'qutebrowser/browser/webkit/http.py'), + 'browser/webkit/http.py'), ('tests/unit/browser/webkit/http/test_content_disposition.py', - 'qutebrowser/browser/webkit/rfc6266.py'), + 'browser/webkit/rfc6266.py'), # ('tests/unit/browser/webkit/test_webkitelem.py', - # 'qutebrowser/browser/webkit/webkitelem.py'), + # 'browser/webkit/webkitelem.py'), # ('tests/unit/browser/webkit/test_webkitelem.py', - # 'qutebrowser/browser/webelem.py'), + # 'browser/webelem.py'), ('tests/unit/browser/webkit/network/test_schemehandler.py', - 'qutebrowser/browser/webkit/network/schemehandler.py'), + 'browser/webkit/network/schemehandler.py'), ('tests/unit/browser/webkit/network/test_filescheme.py', - 'qutebrowser/browser/webkit/network/filescheme.py'), + 'browser/webkit/network/filescheme.py'), ('tests/unit/browser/webkit/network/test_networkreply.py', - 'qutebrowser/browser/webkit/network/networkreply.py'), + 'browser/webkit/network/networkreply.py'), ('tests/unit/browser/test_signalfilter.py', - 'qutebrowser/browser/signalfilter.py'), + 'browser/signalfilter.py'), (None, - 'qutebrowser/browser/webkit/certificateerror.py'), + 'browser/webkit/certificateerror.py'), # ('tests/unit/browser/test_tab.py', - # 'qutebrowser/browser/tab.py'), + # 'browser/tab.py'), ('tests/unit/keyinput/test_basekeyparser.py', - 'qutebrowser/keyinput/basekeyparser.py'), + 'keyinput/basekeyparser.py'), ('tests/unit/misc/test_autoupdate.py', - 'qutebrowser/misc/autoupdate.py'), + 'misc/autoupdate.py'), ('tests/unit/misc/test_readline.py', - 'qutebrowser/misc/readline.py'), + 'misc/readline.py'), ('tests/unit/misc/test_split.py', - 'qutebrowser/misc/split.py'), + 'misc/split.py'), ('tests/unit/misc/test_msgbox.py', - 'qutebrowser/misc/msgbox.py'), + 'misc/msgbox.py'), ('tests/unit/misc/test_checkpyver.py', - 'qutebrowser/misc/checkpyver.py'), + 'misc/checkpyver.py'), ('tests/unit/misc/test_guiprocess.py', - 'qutebrowser/misc/guiprocess.py'), + 'misc/guiprocess.py'), ('tests/unit/misc/test_editor.py', - 'qutebrowser/misc/editor.py'), + 'misc/editor.py'), ('tests/unit/misc/test_cmdhistory.py', - 'qutebrowser/misc/cmdhistory.py'), + 'misc/cmdhistory.py'), ('tests/unit/misc/test_ipc.py', - 'qutebrowser/misc/ipc.py'), + 'misc/ipc.py'), ('tests/unit/misc/test_keyhints.py', - 'qutebrowser/misc/keyhintwidget.py'), + 'misc/keyhintwidget.py'), ('tests/unit/misc/test_pastebin.py', - 'qutebrowser/misc/pastebin.py'), + 'misc/pastebin.py'), (None, - 'qutebrowser/misc/objects.py'), + 'misc/objects.py'), (None, - 'qutebrowser/mainwindow/statusbar/keystring.py'), + 'mainwindow/statusbar/keystring.py'), ('tests/unit/mainwindow/statusbar/test_percentage.py', - 'qutebrowser/mainwindow/statusbar/percentage.py'), + 'mainwindow/statusbar/percentage.py'), ('tests/unit/mainwindow/statusbar/test_progress.py', - 'qutebrowser/mainwindow/statusbar/progress.py'), + 'mainwindow/statusbar/progress.py'), ('tests/unit/mainwindow/statusbar/test_tabindex.py', - 'qutebrowser/mainwindow/statusbar/tabindex.py'), + 'mainwindow/statusbar/tabindex.py'), ('tests/unit/mainwindow/statusbar/test_textbase.py', - 'qutebrowser/mainwindow/statusbar/textbase.py'), + 'mainwindow/statusbar/textbase.py'), ('tests/unit/mainwindow/statusbar/test_url.py', - 'qutebrowser/mainwindow/statusbar/url.py'), + 'mainwindow/statusbar/url.py'), ('tests/unit/mainwindow/test_messageview.py', - 'qutebrowser/mainwindow/messageview.py'), + 'mainwindow/messageview.py'), ('tests/unit/config/test_configtypes.py', - 'qutebrowser/config/configtypes.py'), + 'config/configtypes.py'), ('tests/unit/config/test_configdata.py', - 'qutebrowser/config/configdata.py'), + 'config/configdata.py'), ('tests/unit/config/test_configexc.py', - 'qutebrowser/config/configexc.py'), + 'config/configexc.py'), ('tests/unit/config/test_textwrapper.py', - 'qutebrowser/config/textwrapper.py'), + 'config/textwrapper.py'), ('tests/unit/config/test_style.py', - 'qutebrowser/config/style.py'), + 'config/style.py'), ('tests/unit/utils/test_qtutils.py', - 'qutebrowser/utils/qtutils.py'), + 'utils/qtutils.py'), ('tests/unit/utils/test_standarddir.py', - 'qutebrowser/utils/standarddir.py'), + 'utils/standarddir.py'), ('tests/unit/utils/test_urlutils.py', - 'qutebrowser/utils/urlutils.py'), + 'utils/urlutils.py'), ('tests/unit/utils/usertypes', - 'qutebrowser/utils/usertypes.py'), + 'utils/usertypes.py'), ('tests/unit/utils/test_utils.py', - 'qutebrowser/utils/utils.py'), + 'utils/utils.py'), ('tests/unit/utils/test_version.py', - 'qutebrowser/utils/version.py'), + 'utils/version.py'), ('tests/unit/utils/test_debug.py', - 'qutebrowser/utils/debug.py'), + 'utils/debug.py'), ('tests/unit/utils/test_jinja.py', - 'qutebrowser/utils/jinja.py'), + 'utils/jinja.py'), ('tests/unit/utils/test_error.py', - 'qutebrowser/utils/error.py'), + 'utils/error.py'), ('tests/unit/utils/test_typing.py', - 'qutebrowser/utils/typing.py'), + 'utils/typing.py'), ('tests/unit/utils/test_javascript.py', - 'qutebrowser/utils/javascript.py'), + 'utils/javascript.py'), ('tests/unit/completion/test_models.py', - 'qutebrowser/completion/models/base.py'), + 'completion/models/base.py'), ('tests/unit/completion/test_sortfilter.py', - 'qutebrowser/completion/models/sortfilter.py'), + 'completion/models/sortfilter.py'), ] # 100% coverage because of end2end tests, but no perfect unit tests yet. WHITELISTED_FILES = [ - 'qutebrowser/browser/webkit/webkitinspector.py', - 'qutebrowser/keyinput/macros.py', - 'qutebrowser/browser/webkit/webkitelem.py', + 'browser/webkit/webkitinspector.py', + 'keyinput/macros.py', + 'browser/webkit/webkitelem.py', ] @@ -187,6 +187,8 @@ def _get_filename(filename): common_path = os.path.commonprefix([basedir, filename]) if common_path: filename = filename[len(common_path):].lstrip('/') + if filename.startswith('qutebrowser/'): + filename = filename.split('/', maxsplit=1)[1] return filename diff --git a/tests/unit/scripts/test_check_coverage.py b/tests/unit/scripts/test_check_coverage.py index 70f429aa2..16182bcf7 100644 --- a/tests/unit/scripts/test_check_coverage.py +++ b/tests/unit/scripts/test_check_coverage.py @@ -185,11 +185,12 @@ def test_skipped_windows(covtest, monkeypatch): def _generate_files(): """Get filenames from WHITELISTED_/PERFECT_FILES.""" - yield from iter(check_coverage.WHITELISTED_FILES) + for src_file in check_coverage.WHITELISTED_FILES: + yield os.path.join('qutebrowser', src_file) for test_file, src_file in check_coverage.PERFECT_FILES: if test_file is not None: yield test_file - yield src_file + yield os.path.join('qutebrowser', src_file) @pytest.mark.parametrize('filename', list(_generate_files())) From 9db92de2d536b1262c6585ef22f8cfd1da699b80 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 May 2017 06:15:21 +0200 Subject: [PATCH 210/825] Add a no cover pragma for certificate error hashing --- qutebrowser/browser/webkit/certificateerror.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/browser/webkit/certificateerror.py b/qutebrowser/browser/webkit/certificateerror.py index 41cf2866f..7ebcb3b2f 100644 --- a/qutebrowser/browser/webkit/certificateerror.py +++ b/qutebrowser/browser/webkit/certificateerror.py @@ -41,7 +41,7 @@ class CertificateErrorWrapper(usertypes.AbstractCertificateErrorWrapper): try: # Qt >= 5.4 return hash(self._error) - except TypeError: + except TypeError: # pragma: no cover return hash((self._error.certificate().toDer(), self._error.error())) From e07a1045a8fe2128eedf30051aa9606ebcf20bb1 Mon Sep 17 00:00:00 2001 From: Jay Kamat Date: Mon, 8 May 2017 22:00:11 -0700 Subject: [PATCH 211/825] Add is_link method to webelem --- qutebrowser/browser/webelem.py | 10 +++++++--- qutebrowser/browser/webkit/webkitelem.py | 3 +-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py index 5dd263da3..de6b9cfda 100644 --- a/qutebrowser/browser/webelem.py +++ b/qutebrowser/browser/webelem.py @@ -306,6 +306,11 @@ class AbstractWebElement(collections.abc.MutableMapping): qtutils.ensure_valid(url) return url + def is_link(self): + """Return True if this AbstractWebElement is a link.""" + href_tags = ['a', 'area', 'link'] + return self.tag_name() in href_tags + def _mouse_pos(self): """Get the position to click/hover.""" # Click the center of the largest square fitting into the top/left @@ -403,9 +408,8 @@ class AbstractWebElement(collections.abc.MutableMapping): self._click_fake_event(click_target) return - href_tags = ['a', 'area', 'link'] if click_target == usertypes.ClickTarget.normal: - if self.tag_name() in href_tags: + if self.is_link(): log.webelem.debug("Clicking via JS click()") self._click_js(click_target) elif self.is_editable(strict=True): @@ -418,7 +422,7 @@ class AbstractWebElement(collections.abc.MutableMapping): elif click_target in [usertypes.ClickTarget.tab, usertypes.ClickTarget.tab_bg, usertypes.ClickTarget.window]: - if self.tag_name() in href_tags: + if self.is_link(): self._click_href(click_target) else: self._click_fake_event(click_target) diff --git a/qutebrowser/browser/webkit/webkitelem.py b/qutebrowser/browser/webkit/webkitelem.py index ba15e4a66..a570eae3c 100644 --- a/qutebrowser/browser/webkit/webkitelem.py +++ b/qutebrowser/browser/webkit/webkitelem.py @@ -285,8 +285,7 @@ class WebKitElement(webelem.AbstractWebElement): for _ in range(5): if elem is None: break - tag = elem.tag_name() - if tag == 'a' or tag == 'area': + if elem.is_link(): if elem.get('target', None) == '_blank': elem['target'] = '_top' break From c9953b9f0da094c8a5bde0fd92db8a487db16579 Mon Sep 17 00:00:00 2001 From: Jay Kamat Date: Mon, 8 May 2017 22:31:29 -0700 Subject: [PATCH 212/825] Add support for follow_selected via fake-clicks --- qutebrowser/browser/browsertab.py | 4 ++ qutebrowser/browser/webengine/webenginetab.py | 54 +++++++++++++++++-- qutebrowser/browser/webkit/webkittab.py | 8 +++ qutebrowser/javascript/webelem.js | 9 ++++ 4 files changed, 71 insertions(+), 4 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index ac1f73a63..672f20d68 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -196,6 +196,10 @@ class AbstractSearch(QObject): """ raise NotImplementedError + def searching(self): + """Return True if we are currently searching or not.""" + raise NotImplementedError + class AbstractZoom(QObject): diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index cc3dcf108..c083176fd 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -127,9 +127,12 @@ class WebEngineSearch(browsertab.AbstractSearch): def __init__(self, parent=None): super().__init__(parent) self._flags = QWebEnginePage.FindFlags(0) + self._searching = False def _find(self, text, flags, callback, caller): """Call findText on the widget.""" + self._searching = True + def wrapped_callback(found): """Wrap the callback to do debug logging.""" found_text = 'found' if found else "didn't find" @@ -160,8 +163,12 @@ class WebEngineSearch(browsertab.AbstractSearch): self._find(text, flags, result_cb, 'search') def clear(self): + self._searching = False self._widget.findText('') + def searching(self): + return self._searching + def prev_result(self, *, result_cb=None): # The int() here makes sure we get a copy of the flags. flags = QWebEnginePage.FindFlags(int(self._flags)) @@ -246,8 +253,47 @@ class WebEngineCaret(browsertab.AbstractCaret): raise browsertab.UnsupportedOperationError return self._widget.selectedText() + def _follow_selected_cb(self, js_elem, tab=False): + """Callback for javascript which clicks the selected element. + + Args: + js_elems: The elements serialized from javascript. + tab: Open in a new tab or not. + """ + if js_elem is None: + return + assert isinstance(js_elem, dict), js_elem + elem = webengineelem.WebEngineElement(js_elem, tab=self._tab) + if tab: + click_type = usertypes.ClickTarget.tab + else: + click_type = usertypes.ClickTarget.normal + + # Only click if we see a link + if elem.is_link(): + log.webview.debug("Found link in selection, clicking. Tab: " + + str(click_type) + "Link: " + str(elem)) + elem.click(click_type) + def follow_selected(self, *, tab=False): - log.stub() + if self._tab.search.searching(): + # We are currently in search mode. + # let's click the link via a fake-click + # https://bugreports.qt.io/browse/QTBUG-60673 + self._tab.search.clear() + + log.webview.debug("Clicking a searched link via fake key press.") + # send a fake enter, clicking the orange selection box + if not tab: + self._tab.scroller._key_press(Qt.Key_Enter) + else: + self._tab.scroller._key_press(Qt.Key_Enter, modifier=Qt.ControlModifier) + + else: + # click an existing blue selection + js_code = javascript.assemble('webelem', 'find_selected_link') + self._tab.run_js_async(js_code, lambda jsret: + self._follow_selected_cb(jsret, tab)) class WebEngineScroller(browsertab.AbstractScroller): @@ -265,10 +311,10 @@ class WebEngineScroller(browsertab.AbstractScroller): page = widget.page() page.scrollPositionChanged.connect(self._update_pos) - def _key_press(self, key, count=1): + def _key_press(self, key, count=1, modifier=Qt.NoModifier): for _ in range(min(count, 5000)): - press_evt = QKeyEvent(QEvent.KeyPress, key, Qt.NoModifier, 0, 0, 0) - release_evt = QKeyEvent(QEvent.KeyRelease, key, Qt.NoModifier, + press_evt = QKeyEvent(QEvent.KeyPress, key, modifier, 0, 0, 0) + release_evt = QKeyEvent(QEvent.KeyRelease, key, modifier, 0, 0, 0) self._tab.send_event(press_evt) self._tab.send_event(release_evt) diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index 0a844ddb0..54b3d3378 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -103,6 +103,7 @@ class WebKitSearch(browsertab.AbstractSearch): def __init__(self, parent=None): super().__init__(parent) self._flags = QWebPage.FindFlags(0) + self._searching = True def _call_cb(self, callback, found, text, flags, caller): """Call the given callback if it's non-None. @@ -132,12 +133,14 @@ class WebKitSearch(browsertab.AbstractSearch): QTimer.singleShot(0, functools.partial(callback, found)) def clear(self): + self._searching = False # We first clear the marked text, then the highlights self._widget.findText('') self._widget.findText('', QWebPage.HighlightAllOccurrences) def search(self, text, *, ignore_case=False, reverse=False, result_cb=None): + self._searching = True flags = QWebPage.FindWrapsAroundDocument if ignore_case == 'smart': if not text.islower(): @@ -155,10 +158,12 @@ class WebKitSearch(browsertab.AbstractSearch): self._call_cb(result_cb, found, text, flags, 'search') def next_result(self, *, result_cb=None): + self._searching = True found = self._widget.findText(self.text, self._flags) self._call_cb(result_cb, found, self.text, self._flags, 'next_result') def prev_result(self, *, result_cb=None): + self._searching = True # The int() here makes sure we get a copy of the flags. flags = QWebPage.FindFlags(int(self._flags)) if flags & QWebPage.FindBackward: @@ -168,6 +173,9 @@ class WebKitSearch(browsertab.AbstractSearch): found = self._widget.findText(self.text, flags) self._call_cb(result_cb, found, self.text, flags, 'prev_result') + def searching(self): + return self._searching + class WebKitCaret(browsertab.AbstractCaret): diff --git a/qutebrowser/javascript/webelem.js b/qutebrowser/javascript/webelem.js index f9bd7d1af..8c7883b3b 100644 --- a/qutebrowser/javascript/webelem.js +++ b/qutebrowser/javascript/webelem.js @@ -169,6 +169,15 @@ window._qutebrowser.webelem = (function() { return serialize_elem(elem); }; + // Function for returning a selection to python (so we can click it) + funcs.find_selected_link = function() { + var elem = window.getSelection().anchorNode; + if (!elem) { + return null; + } + return serialize_elem(elem.parentNode); + }; + funcs.set_value = function(id, value) { elements[id].value = value; }; From 5ba81e3611ca231728e7114b4e191f6b5189cb8d Mon Sep 17 00:00:00 2001 From: Jay Kamat Date: Mon, 8 May 2017 22:32:53 -0700 Subject: [PATCH 213/825] Add tests for follow_selected --- tests/end2end/data/search.html | 1 + tests/end2end/features/search.feature | 33 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/tests/end2end/data/search.html b/tests/end2end/data/search.html index 2eec560d6..3ca3e2e22 100644 --- a/tests/end2end/data/search.html +++ b/tests/end2end/data/search.html @@ -16,6 +16,7 @@ BAZ
space travel
/slash
+ follow me!

diff --git a/tests/end2end/features/search.feature b/tests/end2end/features/search.feature index 285b3cbf8..35a99394a 100644 --- a/tests/end2end/features/search.feature +++ b/tests/end2end/features/search.feature @@ -191,3 +191,36 @@ Feature: Searching on a page # TODO: wrapping message with scrolling # TODO: wrapping message without scrolling + + ## follow searched links + Scenario: Follow a searched link + When I run :search follow + And I wait for "search found follow" in the log + And I run :follow-selected + Then data/hello.txt should be loaded + + Scenario: Follow a searched link in a new tab + When I run :window-only + And I run :search follow + And I wait for "search found follow" in the log + And I run :follow-selected -t + And I wait until data/hello.txt is loaded + Then the following tabs should be open: + - data/search.html + - data/hello.txt (active) + + Scenario: Don't follow searched text + When I run :window-only + And I run :search foo + And I wait for "search found foo" in the log + And I run :follow-selected + Then the following tabs should be open: + - data/search.html (active) + + Scenario: Don't follow searched text in a new tab + When I run :window-only + And I run :search foo + And I wait for "search found foo" in the log + And I run :follow-selected -t + Then the following tabs should be open: + - data/search.html (active) From 63cffaf558acb65952a286e9f93d7b36c5edfa6d Mon Sep 17 00:00:00 2001 From: Jay Kamat Date: Mon, 8 May 2017 22:49:14 -0700 Subject: [PATCH 214/825] Refactor _key_press from WebEngineScroller to WebEngineTab --- qutebrowser/browser/webengine/webenginetab.py | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index c083176fd..0ffe06664 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -285,9 +285,9 @@ class WebEngineCaret(browsertab.AbstractCaret): log.webview.debug("Clicking a searched link via fake key press.") # send a fake enter, clicking the orange selection box if not tab: - self._tab.scroller._key_press(Qt.Key_Enter) + self._tab.key_press(Qt.Key_Enter) else: - self._tab.scroller._key_press(Qt.Key_Enter, modifier=Qt.ControlModifier) + self._tab.key_press(Qt.Key_Enter, modifier=Qt.ControlModifier) else: # click an existing blue selection @@ -311,14 +311,6 @@ class WebEngineScroller(browsertab.AbstractScroller): page = widget.page() page.scrollPositionChanged.connect(self._update_pos) - def _key_press(self, key, count=1, modifier=Qt.NoModifier): - for _ in range(min(count, 5000)): - press_evt = QKeyEvent(QEvent.KeyPress, key, modifier, 0, 0, 0) - release_evt = QKeyEvent(QEvent.KeyRelease, key, modifier, - 0, 0, 0) - self._tab.send_event(press_evt) - self._tab.send_event(release_evt) - @pyqtSlot() def _update_pos(self): """Update the scroll position attributes when it changed.""" @@ -374,28 +366,28 @@ class WebEngineScroller(browsertab.AbstractScroller): self._tab.run_js_async(js_code) def up(self, count=1): - self._key_press(Qt.Key_Up, count) + self._tab.key_press(Qt.Key_Up, count) def down(self, count=1): - self._key_press(Qt.Key_Down, count) + self._tab.key_press(Qt.Key_Down, count) def left(self, count=1): - self._key_press(Qt.Key_Left, count) + self._tab.key_press(Qt.Key_Left, count) def right(self, count=1): - self._key_press(Qt.Key_Right, count) + self._tab.key_press(Qt.Key_Right, count) def top(self): - self._key_press(Qt.Key_Home) + self._tab.key_press(Qt.Key_Home) def bottom(self): - self._key_press(Qt.Key_End) + self._tab.key_press(Qt.Key_End) def page_up(self, count=1): - self._key_press(Qt.Key_PageUp, count) + self._tab.key_press(Qt.Key_PageUp, count) def page_down(self, count=1): - self._key_press(Qt.Key_PageDown, count) + self._tab.key_press(Qt.Key_PageDown, count) def at_top(self): return self.pos_px().y() == 0 @@ -655,6 +647,14 @@ class WebEngineTab(browsertab.AbstractTab): def clear_ssl_errors(self): raise browsertab.UnsupportedOperationError + def key_press(self, key, count=1, modifier=Qt.NoModifier): + for _ in range(min(count, 5000)): + press_evt = QKeyEvent(QEvent.KeyPress, key, modifier, 0, 0, 0) + release_evt = QKeyEvent(QEvent.KeyRelease, key, modifier, + 0, 0, 0) + self.send_event(press_evt) + self.send_event(release_evt) + @pyqtSlot() def _on_history_trigger(self): url = self.url() From 5bdd291d28730a27bc1fbf89a3cde3d00fed4af0 Mon Sep 17 00:00:00 2001 From: Jay Kamat Date: Mon, 8 May 2017 23:11:50 -0700 Subject: [PATCH 215/825] Refactor key_press into _repeated_key_press Also split off generic key pressing ability from WebKitScroller to WebKitTab --- qutebrowser/browser/webengine/webenginetab.py | 29 ++++++++++--------- qutebrowser/browser/webkit/webkittab.py | 13 +++++---- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 0ffe06664..bb3f017ab 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -311,6 +311,10 @@ class WebEngineScroller(browsertab.AbstractScroller): page = widget.page() page.scrollPositionChanged.connect(self._update_pos) + def _repeated_key_press(self, key, count=1, modifier=Qt.NoModifier): + for _ in range(min(count, 5000)): + self._tab.key_press(key, modifier) + @pyqtSlot() def _update_pos(self): """Update the scroll position attributes when it changed.""" @@ -366,16 +370,16 @@ class WebEngineScroller(browsertab.AbstractScroller): self._tab.run_js_async(js_code) def up(self, count=1): - self._tab.key_press(Qt.Key_Up, count) + self._repeated_key_press(Qt.Key_Up, count) def down(self, count=1): - self._tab.key_press(Qt.Key_Down, count) + self._repeated_key_press(Qt.Key_Down, count) def left(self, count=1): - self._tab.key_press(Qt.Key_Left, count) + self._repeated_key_press(Qt.Key_Left, count) def right(self, count=1): - self._tab.key_press(Qt.Key_Right, count) + self._repeated_key_press(Qt.Key_Right, count) def top(self): self._tab.key_press(Qt.Key_Home) @@ -384,10 +388,10 @@ class WebEngineScroller(browsertab.AbstractScroller): self._tab.key_press(Qt.Key_End) def page_up(self, count=1): - self._tab.key_press(Qt.Key_PageUp, count) + self._repeated_key_press(Qt.Key_PageUp, count) def page_down(self, count=1): - self._tab.key_press(Qt.Key_PageDown, count) + self._repeated_key_press(Qt.Key_PageDown, count) def at_top(self): return self.pos_px().y() == 0 @@ -647,13 +651,12 @@ class WebEngineTab(browsertab.AbstractTab): def clear_ssl_errors(self): raise browsertab.UnsupportedOperationError - def key_press(self, key, count=1, modifier=Qt.NoModifier): - for _ in range(min(count, 5000)): - press_evt = QKeyEvent(QEvent.KeyPress, key, modifier, 0, 0, 0) - release_evt = QKeyEvent(QEvent.KeyRelease, key, modifier, - 0, 0, 0) - self.send_event(press_evt) - self.send_event(release_evt) + def key_press(self, key, modifier=Qt.NoModifier): + press_evt = QKeyEvent(QEvent.KeyPress, key, modifier, 0, 0, 0) + release_evt = QKeyEvent(QEvent.KeyRelease, key, modifier, + 0, 0, 0) + self.send_event(press_evt) + self.send_event(release_evt) @pyqtSlot() def _on_history_trigger(self): diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index 54b3d3378..f1f876a6e 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -468,15 +468,11 @@ class WebKitScroller(browsertab.AbstractScroller): # self._widget.setFocus() for _ in range(min(count, 5000)): - press_evt = QKeyEvent(QEvent.KeyPress, key, Qt.NoModifier, 0, 0, 0) - release_evt = QKeyEvent(QEvent.KeyRelease, key, Qt.NoModifier, - 0, 0, 0) # Abort scrolling if the minimum/maximum was reached. if (getter is not None and frame.scrollBarValue(direction) == getter(direction)): return - self._widget.keyPressEvent(press_evt) - self._widget.keyReleaseEvent(release_evt) + self._tab.key_press(key) def up(self, count=1): self._key_press(Qt.Key_Up, count, 'scrollBarMinimum', Qt.Vertical) @@ -702,6 +698,13 @@ class WebKitTab(browsertab.AbstractTab): def clear_ssl_errors(self): self.networkaccessmanager().clear_all_ssl_errors() + def key_press(self, key, modifier=Qt.NoModifier): + press_evt = QKeyEvent(QEvent.KeyPress, key, modifier, 0, 0, 0) + release_evt = QKeyEvent(QEvent.KeyRelease, key, modifier, + 0, 0, 0) + self.send_event(press_evt) + self.send_event(release_evt) + @pyqtSlot() def _on_history_trigger(self): url = self.url() From d1aac9e9e932668199c0835890ceb2c31bee038b Mon Sep 17 00:00:00 2001 From: Jay Kamat Date: Mon, 8 May 2017 23:43:21 -0700 Subject: [PATCH 216/825] Add docstrings to key_press methods --- qutebrowser/browser/webengine/webenginetab.py | 2 ++ qutebrowser/browser/webkit/webkittab.py | 1 + 2 files changed, 3 insertions(+) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index bb3f017ab..657b6e6da 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -312,6 +312,7 @@ class WebEngineScroller(browsertab.AbstractScroller): page.scrollPositionChanged.connect(self._update_pos) def _repeated_key_press(self, key, count=1, modifier=Qt.NoModifier): + """Send count fake key presses to this scroller's WebEngineTab.""" for _ in range(min(count, 5000)): self._tab.key_press(key, modifier) @@ -652,6 +653,7 @@ class WebEngineTab(browsertab.AbstractTab): raise browsertab.UnsupportedOperationError def key_press(self, key, modifier=Qt.NoModifier): + """Send a fake key event to this WebKitTab.""" press_evt = QKeyEvent(QEvent.KeyPress, key, modifier, 0, 0, 0) release_evt = QKeyEvent(QEvent.KeyRelease, key, modifier, 0, 0, 0) diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index f1f876a6e..44df236a2 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -699,6 +699,7 @@ class WebKitTab(browsertab.AbstractTab): self.networkaccessmanager().clear_all_ssl_errors() def key_press(self, key, modifier=Qt.NoModifier): + """Send a fake key event to this WebKitTab.""" press_evt = QKeyEvent(QEvent.KeyPress, key, modifier, 0, 0, 0) release_evt = QKeyEvent(QEvent.KeyRelease, key, modifier, 0, 0, 0) From 02f79c2990228d13b8111f5ae6fca6da523408d0 Mon Sep 17 00:00:00 2001 From: Jay Kamat Date: Tue, 9 May 2017 00:11:25 -0700 Subject: [PATCH 217/825] Add tests for manual selection --- tests/end2end/data/search.html | 2 +- tests/end2end/data/search_select.js | 13 +++++++++++++ tests/end2end/features/search.feature | 14 ++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 tests/end2end/data/search_select.js diff --git a/tests/end2end/data/search.html b/tests/end2end/data/search.html index 3ca3e2e22..003751cfd 100644 --- a/tests/end2end/data/search.html +++ b/tests/end2end/data/search.html @@ -16,7 +16,7 @@ BAZ
space travel
/slash
- follow me!
+ follow me!

diff --git a/tests/end2end/data/search_select.js b/tests/end2end/data/search_select.js new file mode 100644 index 000000000..8a816c7b7 --- /dev/null +++ b/tests/end2end/data/search_select.js @@ -0,0 +1,13 @@ +/* Select all elements marked with toselect */ + + +var toSelect = document.getElementsByClassName("toselect"); +var s = window.getSelection(); + +if(s.rangeCount > 0) s.removeAllRanges(); + +for(var i = 0; i < toSelect.length; i++) { + var range = document.createRange(); + range.selectNode(toSelect[i]); + s.addRange(range); +} diff --git a/tests/end2end/features/search.feature b/tests/end2end/features/search.feature index 35a99394a..7e0151976 100644 --- a/tests/end2end/features/search.feature +++ b/tests/end2end/features/search.feature @@ -224,3 +224,17 @@ Feature: Searching on a page And I run :follow-selected -t Then the following tabs should be open: - data/search.html (active) + + Scenario: Follow a manually selected link + When I run :jseval --file (testdata)/search_select.js + And I run :follow-selected + Then data/hello.txt should be loaded + + Scenario: Follow a manually selected link in a new tab + When I run :window-only + And I run :jseval --file (testdata)/search_select.js + And I run :follow-selected -t + And I wait until data/hello.txt is loaded + Then the following tabs should be open: + - data/search.html + - data/hello.txt (active) From a3d41c046774586dccc4b4d1ab1786490eee496c Mon Sep 17 00:00:00 2001 From: Jay Kamat Date: Tue, 9 May 2017 00:14:59 -0700 Subject: [PATCH 218/825] Refactor search method of AbstractBrowserTab into a field --- qutebrowser/browser/browsertab.py | 6 ++---- qutebrowser/browser/webengine/webenginetab.py | 19 ++++++++----------- qutebrowser/browser/webkit/webkittab.py | 13 +++++-------- tests/end2end/data/search_select.js | 7 +++---- 4 files changed, 18 insertions(+), 27 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 672f20d68..f133a1646 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -163,6 +163,8 @@ class AbstractSearch(QObject): super().__init__(parent) self._widget = None self.text = None + # Implementing classes will set this. + self.search_displayed = False def search(self, text, *, ignore_case=False, reverse=False, result_cb=None): @@ -196,10 +198,6 @@ class AbstractSearch(QObject): """ raise NotImplementedError - def searching(self): - """Return True if we are currently searching or not.""" - raise NotImplementedError - class AbstractZoom(QObject): diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 657b6e6da..e460af049 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -127,11 +127,11 @@ class WebEngineSearch(browsertab.AbstractSearch): def __init__(self, parent=None): super().__init__(parent) self._flags = QWebEnginePage.FindFlags(0) - self._searching = False + self.search_displayed = False def _find(self, text, flags, callback, caller): """Call findText on the widget.""" - self._searching = True + self.search_displayed = True def wrapped_callback(found): """Wrap the callback to do debug logging.""" @@ -163,12 +163,9 @@ class WebEngineSearch(browsertab.AbstractSearch): self._find(text, flags, result_cb, 'search') def clear(self): - self._searching = False + self.search_displayed = False self._widget.findText('') - def searching(self): - return self._searching - def prev_result(self, *, result_cb=None): # The int() here makes sure we get a copy of the flags. flags = QWebEnginePage.FindFlags(int(self._flags)) @@ -256,10 +253,10 @@ class WebEngineCaret(browsertab.AbstractCaret): def _follow_selected_cb(self, js_elem, tab=False): """Callback for javascript which clicks the selected element. - Args: - js_elems: The elements serialized from javascript. - tab: Open in a new tab or not. - """ + Args: + js_elems: The elements serialized from javascript. + tab: Open in a new tab or not. + """ if js_elem is None: return assert isinstance(js_elem, dict), js_elem @@ -276,7 +273,7 @@ class WebEngineCaret(browsertab.AbstractCaret): elem.click(click_type) def follow_selected(self, *, tab=False): - if self._tab.search.searching(): + if self._tab.search.search_displayed: # We are currently in search mode. # let's click the link via a fake-click # https://bugreports.qt.io/browse/QTBUG-60673 diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index 44df236a2..0cf83e2a7 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -103,7 +103,7 @@ class WebKitSearch(browsertab.AbstractSearch): def __init__(self, parent=None): super().__init__(parent) self._flags = QWebPage.FindFlags(0) - self._searching = True + self.search_displayed = True def _call_cb(self, callback, found, text, flags, caller): """Call the given callback if it's non-None. @@ -133,14 +133,14 @@ class WebKitSearch(browsertab.AbstractSearch): QTimer.singleShot(0, functools.partial(callback, found)) def clear(self): - self._searching = False + self.search_displayed = False # We first clear the marked text, then the highlights self._widget.findText('') self._widget.findText('', QWebPage.HighlightAllOccurrences) def search(self, text, *, ignore_case=False, reverse=False, result_cb=None): - self._searching = True + self.search_displayed = True flags = QWebPage.FindWrapsAroundDocument if ignore_case == 'smart': if not text.islower(): @@ -158,12 +158,12 @@ class WebKitSearch(browsertab.AbstractSearch): self._call_cb(result_cb, found, text, flags, 'search') def next_result(self, *, result_cb=None): - self._searching = True + self.search_displayed = True found = self._widget.findText(self.text, self._flags) self._call_cb(result_cb, found, self.text, self._flags, 'next_result') def prev_result(self, *, result_cb=None): - self._searching = True + self.search_displayed = True # The int() here makes sure we get a copy of the flags. flags = QWebPage.FindFlags(int(self._flags)) if flags & QWebPage.FindBackward: @@ -173,9 +173,6 @@ class WebKitSearch(browsertab.AbstractSearch): found = self._widget.findText(self.text, flags) self._call_cb(result_cb, found, self.text, flags, 'prev_result') - def searching(self): - return self._searching - class WebKitCaret(browsertab.AbstractCaret): diff --git a/tests/end2end/data/search_select.js b/tests/end2end/data/search_select.js index 8a816c7b7..874e9e9fe 100644 --- a/tests/end2end/data/search_select.js +++ b/tests/end2end/data/search_select.js @@ -1,13 +1,12 @@ /* Select all elements marked with toselect */ - var toSelect = document.getElementsByClassName("toselect"); var s = window.getSelection(); if(s.rangeCount > 0) s.removeAllRanges(); for(var i = 0; i < toSelect.length; i++) { - var range = document.createRange(); - range.selectNode(toSelect[i]); - s.addRange(range); + var range = document.createRange(); + range.selectNode(toSelect[i]); + s.addRange(range); } From e10d636ca09e1385b9bf0661062a62cef6227da6 Mon Sep 17 00:00:00 2001 From: Jay Kamat Date: Tue, 9 May 2017 08:08:05 -0700 Subject: [PATCH 219/825] Fix a few small issues - Remove an unused warnings - Reverse if statement arguments to simplify logic --- qutebrowser/browser/webengine/webenginetab.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index e460af049..d4e3feaa2 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -17,9 +17,6 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -# FIXME:qtwebengine remove this once the stubs are gone -# pylint: disable=unused-argument - """Wrapper over a QWebEngineView.""" import functools @@ -281,10 +278,10 @@ class WebEngineCaret(browsertab.AbstractCaret): log.webview.debug("Clicking a searched link via fake key press.") # send a fake enter, clicking the orange selection box - if not tab: - self._tab.key_press(Qt.Key_Enter) - else: + if tab: self._tab.key_press(Qt.Key_Enter, modifier=Qt.ControlModifier) + else: + self._tab.key_press(Qt.Key_Enter) else: # click an existing blue selection From 5f2fb2c4fcffa5dc052f308639d87e09925aef79 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 May 2017 17:48:40 +0200 Subject: [PATCH 220/825] Update docs --- CHANGELOG.asciidoc | 1 + README.asciidoc | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 7ff888acd..bf01a2a17 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -53,6 +53,7 @@ Changed - With Qt 5.9, `content -> cookies-store` can now be set on QtWebEngine without a restart. - The adblocker now also blocks non-GET requests (e.g. POST) +- `:follow-selected` now also works with QtWebEngine Fixed ~~~~~ diff --git a/README.asciidoc b/README.asciidoc index b3457876b..215e2e437 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -191,6 +191,7 @@ Contributors, sorted by the number of commits in descending order: * ZDarian * Milan Svoboda * John ShaggyTwoDope Jenkins +* Jay Kamat * Clayton Craft * Peter Vilim * Jacob Sword @@ -285,7 +286,6 @@ Contributors, sorted by the number of commits in descending order: * Ján Kobezda * Johannes Martinsson * Jean-Christophe Petkovich -* Jay Kamat * Helen Sherwood-Taylor * HalosGhost * Gregor Pohl From e3eda28d88b959e3fca7c83455deae0d13438d4b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 May 2017 17:49:38 +0200 Subject: [PATCH 221/825] Update docstrings --- qutebrowser/browser/browsertab.py | 3 ++- qutebrowser/browser/webengine/webenginetab.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index f133a1646..21c0a080e 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -155,6 +155,8 @@ class AbstractSearch(QObject): Attributes: text: The last thing this view was searched for. + search_displayed: Whether we're currently displaying search results in + this view. _flags: The flags of the last search (needs to be set by subclasses). _widget: The underlying WebView widget. """ @@ -163,7 +165,6 @@ class AbstractSearch(QObject): super().__init__(parent) self._widget = None self.text = None - # Implementing classes will set this. self.search_displayed = False def search(self, text, *, ignore_case=False, reverse=False, diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index d4e3feaa2..3d6d381cb 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -251,8 +251,8 @@ class WebEngineCaret(browsertab.AbstractCaret): """Callback for javascript which clicks the selected element. Args: - js_elems: The elements serialized from javascript. - tab: Open in a new tab or not. + js_elem: The element serialized from javascript. + tab: Open in a new tab. """ if js_elem is None: return From 76fa126133a1565ebc210ae0a42d03e553450d82 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 May 2017 17:55:51 +0200 Subject: [PATCH 222/825] Simplify debug string --- qutebrowser/browser/webengine/webenginetab.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 3d6d381cb..bc7a5bf35 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -265,8 +265,8 @@ class WebEngineCaret(browsertab.AbstractCaret): # Only click if we see a link if elem.is_link(): - log.webview.debug("Found link in selection, clicking. Tab: " + - str(click_type) + "Link: " + str(elem)) + log.webview.debug("Found link in selection, clicking. ClickTarget " + "{}, elem {}".format(click_type, elem)) elem.click(click_type) def follow_selected(self, *, tab=False): From 4b5e528d0556ca0c86d7ff7f70158aacf2404463 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 May 2017 17:56:07 +0200 Subject: [PATCH 223/825] Add AbstractTab.key_press --- qutebrowser/browser/browsertab.py | 6 +++++- qutebrowser/browser/webengine/webenginetab.py | 1 - qutebrowser/browser/webkit/webkittab.py | 1 - 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 21c0a080e..aa7025edf 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -21,7 +21,7 @@ import itertools -from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF, Qt from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QWidget, QApplication @@ -743,6 +743,10 @@ class AbstractTab(QWidget): def clear_ssl_errors(self): raise NotImplementedError + def key_press(self, key, modifier=Qt.NoModifier): + """Send a fake key event to this tab.""" + raise NotImplementedError + def dump_async(self, callback, *, plain=False): """Dump the current page to a file ascync. diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index bc7a5bf35..c97035c0b 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -647,7 +647,6 @@ class WebEngineTab(browsertab.AbstractTab): raise browsertab.UnsupportedOperationError def key_press(self, key, modifier=Qt.NoModifier): - """Send a fake key event to this WebKitTab.""" press_evt = QKeyEvent(QEvent.KeyPress, key, modifier, 0, 0, 0) release_evt = QKeyEvent(QEvent.KeyRelease, key, modifier, 0, 0, 0) diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index 0cf83e2a7..cf33098e0 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -696,7 +696,6 @@ class WebKitTab(browsertab.AbstractTab): self.networkaccessmanager().clear_all_ssl_errors() def key_press(self, key, modifier=Qt.NoModifier): - """Send a fake key event to this WebKitTab.""" press_evt = QKeyEvent(QEvent.KeyPress, key, modifier, 0, 0, 0) release_evt = QKeyEvent(QEvent.KeyRelease, key, modifier, 0, 0, 0) From 905032924a2a1b110a62e5fd828dbd356b447ae1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 May 2017 17:58:28 +0200 Subject: [PATCH 224/825] Remove search_displayed initialization in subclasses We set this in BrowserTab anyways, and the value in WebKitTab was wrong. --- qutebrowser/browser/webengine/webenginetab.py | 1 - qutebrowser/browser/webkit/webkittab.py | 1 - 2 files changed, 2 deletions(-) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index c97035c0b..ffecb5686 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -124,7 +124,6 @@ class WebEngineSearch(browsertab.AbstractSearch): def __init__(self, parent=None): super().__init__(parent) self._flags = QWebEnginePage.FindFlags(0) - self.search_displayed = False def _find(self, text, flags, callback, caller): """Call findText on the widget.""" diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index cf33098e0..a5a02338e 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -103,7 +103,6 @@ class WebKitSearch(browsertab.AbstractSearch): def __init__(self, parent=None): super().__init__(parent) self._flags = QWebPage.FindFlags(0) - self.search_displayed = True def _call_cb(self, callback, found, text, flags, caller): """Call the given callback if it's non-None. From 822623f2ed6f0346365ff065fbd7b4c522d933c9 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 May 2017 21:37:03 +0200 Subject: [PATCH 225/825] Finally update copyrights... --- misc/userscripts/dmenu_qutebrowser | 1 + misc/userscripts/openfeeds | 1 + misc/userscripts/qutebrowser_viewsource | 1 + qutebrowser/__init__.py | 4 ++-- qutebrowser/__main__.py | 2 +- qutebrowser/app.py | 2 +- qutebrowser/browser/__init__.py | 2 +- qutebrowser/browser/adblock.py | 2 +- qutebrowser/browser/browsertab.py | 2 +- qutebrowser/browser/commands.py | 2 +- qutebrowser/browser/downloads.py | 2 +- qutebrowser/browser/downloadview.py | 2 +- qutebrowser/browser/hints.py | 2 +- qutebrowser/browser/history.py | 2 +- qutebrowser/browser/inspector.py | 2 +- qutebrowser/browser/mouse.py | 2 +- qutebrowser/browser/navigate.py | 2 +- qutebrowser/browser/network/pac.py | 2 +- qutebrowser/browser/network/proxy.py | 2 +- qutebrowser/browser/pdfjs.py | 1 + qutebrowser/browser/qtnetworkdownloads.py | 2 +- qutebrowser/browser/qutescheme.py | 2 +- qutebrowser/browser/shared.py | 2 +- qutebrowser/browser/signalfilter.py | 2 +- qutebrowser/browser/urlmarks.py | 4 ++-- qutebrowser/browser/webelem.py | 2 +- qutebrowser/browser/webengine/__init__.py | 2 +- qutebrowser/browser/webengine/certificateerror.py | 2 +- qutebrowser/browser/webengine/interceptor.py | 2 +- qutebrowser/browser/webengine/tabhistory.py | 2 +- qutebrowser/browser/webengine/webenginedownloads.py | 2 +- qutebrowser/browser/webengine/webengineelem.py | 2 +- qutebrowser/browser/webengine/webengineinspector.py | 2 +- qutebrowser/browser/webengine/webenginequtescheme.py | 2 +- qutebrowser/browser/webengine/webenginesettings.py | 2 +- qutebrowser/browser/webengine/webenginetab.py | 2 +- qutebrowser/browser/webengine/webview.py | 2 +- qutebrowser/browser/webkit/__init__.py | 2 +- qutebrowser/browser/webkit/cache.py | 2 +- qutebrowser/browser/webkit/certificateerror.py | 2 +- qutebrowser/browser/webkit/cookies.py | 2 +- qutebrowser/browser/webkit/http.py | 2 +- qutebrowser/browser/webkit/mhtml.py | 2 +- qutebrowser/browser/webkit/network/filescheme.py | 4 ++-- qutebrowser/browser/webkit/network/networkmanager.py | 2 +- qutebrowser/browser/webkit/network/networkreply.py | 2 +- qutebrowser/browser/webkit/network/schemehandler.py | 2 +- qutebrowser/browser/webkit/network/webkitqutescheme.py | 2 +- qutebrowser/browser/webkit/rfc6266.py | 2 +- qutebrowser/browser/webkit/tabhistory.py | 2 +- qutebrowser/browser/webkit/webkitelem.py | 2 +- qutebrowser/browser/webkit/webkithistory.py | 2 +- qutebrowser/browser/webkit/webkitinspector.py | 2 +- qutebrowser/browser/webkit/webkitsettings.py | 2 +- qutebrowser/browser/webkit/webkittab.py | 2 +- qutebrowser/browser/webkit/webpage.py | 2 +- qutebrowser/browser/webkit/webview.py | 2 +- qutebrowser/commands/__init__.py | 2 +- qutebrowser/commands/argparser.py | 2 +- qutebrowser/commands/cmdexc.py | 2 +- qutebrowser/commands/cmdutils.py | 2 +- qutebrowser/commands/command.py | 2 +- qutebrowser/commands/runners.py | 2 +- qutebrowser/commands/userscripts.py | 2 +- qutebrowser/completion/__init__.py | 2 +- qutebrowser/completion/completer.py | 2 +- qutebrowser/completion/completiondelegate.py | 2 +- qutebrowser/completion/completionwidget.py | 2 +- qutebrowser/completion/models/__init__.py | 2 +- qutebrowser/completion/models/base.py | 2 +- qutebrowser/completion/models/configmodel.py | 2 +- qutebrowser/completion/models/instances.py | 2 +- qutebrowser/completion/models/miscmodels.py | 2 +- qutebrowser/completion/models/sortfilter.py | 2 +- qutebrowser/completion/models/urlmodel.py | 2 +- qutebrowser/config/__init__.py | 2 +- qutebrowser/config/config.py | 2 +- qutebrowser/config/configdata.py | 2 +- qutebrowser/config/configexc.py | 2 +- qutebrowser/config/configtypes.py | 2 +- qutebrowser/config/parsers/__init__.py | 2 +- qutebrowser/config/parsers/ini.py | 2 +- qutebrowser/config/parsers/keyconf.py | 2 +- qutebrowser/config/sections.py | 2 +- qutebrowser/config/style.py | 2 +- qutebrowser/config/textwrapper.py | 2 +- qutebrowser/config/value.py | 2 +- qutebrowser/config/websettings.py | 2 +- qutebrowser/javascript/position_caret.js | 2 +- qutebrowser/javascript/scroll.js | 2 +- qutebrowser/javascript/webelem.js | 2 +- qutebrowser/keyinput/__init__.py | 2 +- qutebrowser/keyinput/basekeyparser.py | 2 +- qutebrowser/keyinput/keyparser.py | 2 +- qutebrowser/keyinput/macros.py | 2 +- qutebrowser/keyinput/modeman.py | 2 +- qutebrowser/keyinput/modeparsers.py | 2 +- qutebrowser/mainwindow/__init__.py | 2 +- qutebrowser/mainwindow/mainwindow.py | 2 +- qutebrowser/mainwindow/messageview.py | 2 +- qutebrowser/mainwindow/prompt.py | 2 +- qutebrowser/mainwindow/statusbar/__init__.py | 2 +- qutebrowser/mainwindow/statusbar/bar.py | 2 +- qutebrowser/mainwindow/statusbar/command.py | 2 +- qutebrowser/mainwindow/statusbar/keystring.py | 2 +- qutebrowser/mainwindow/statusbar/percentage.py | 2 +- qutebrowser/mainwindow/statusbar/progress.py | 2 +- qutebrowser/mainwindow/statusbar/tabindex.py | 2 +- qutebrowser/mainwindow/statusbar/text.py | 2 +- qutebrowser/mainwindow/statusbar/textbase.py | 2 +- qutebrowser/mainwindow/statusbar/url.py | 2 +- qutebrowser/mainwindow/tabbedbrowser.py | 2 +- qutebrowser/mainwindow/tabwidget.py | 2 +- qutebrowser/misc/__init__.py | 2 +- qutebrowser/misc/autoupdate.py | 2 +- qutebrowser/misc/checkpyver.py | 2 +- qutebrowser/misc/cmdhistory.py | 2 +- qutebrowser/misc/consolewidget.py | 2 +- qutebrowser/misc/crashdialog.py | 2 +- qutebrowser/misc/crashsignal.py | 2 +- qutebrowser/misc/earlyinit.py | 2 +- qutebrowser/misc/editor.py | 2 +- qutebrowser/misc/guiprocess.py | 2 +- qutebrowser/misc/httpclient.py | 2 +- qutebrowser/misc/ipc.py | 2 +- qutebrowser/misc/keyhintwidget.py | 2 +- qutebrowser/misc/lineparser.py | 2 +- qutebrowser/misc/miscwidgets.py | 2 +- qutebrowser/misc/msgbox.py | 2 +- qutebrowser/misc/pastebin.py | 2 +- qutebrowser/misc/readline.py | 2 +- qutebrowser/misc/savemanager.py | 2 +- qutebrowser/misc/sessions.py | 2 +- qutebrowser/misc/split.py | 2 +- qutebrowser/misc/utilcmds.py | 2 +- qutebrowser/qutebrowser.py | 2 +- qutebrowser/utils/__init__.py | 2 +- qutebrowser/utils/debug.py | 2 +- qutebrowser/utils/docutils.py | 2 +- qutebrowser/utils/error.py | 2 +- qutebrowser/utils/javascript.py | 2 +- qutebrowser/utils/jinja.py | 2 +- qutebrowser/utils/log.py | 2 +- qutebrowser/utils/message.py | 2 +- qutebrowser/utils/objreg.py | 2 +- qutebrowser/utils/qtutils.py | 2 +- qutebrowser/utils/standarddir.py | 2 +- qutebrowser/utils/typing.py | 2 +- qutebrowser/utils/urlutils.py | 2 +- qutebrowser/utils/usertypes.py | 2 +- qutebrowser/utils/utils.py | 2 +- qutebrowser/utils/version.py | 2 +- scripts/asciidoc2html.py | 2 +- scripts/dev/build_release.py | 2 +- scripts/dev/check_coverage.py | 2 +- scripts/dev/check_doc_changes.py | 2 +- scripts/dev/ci/appveyor_install.py | 2 +- scripts/dev/ci/travis_install.sh | 2 +- scripts/dev/cleanup.py | 2 +- scripts/dev/freeze.py | 2 +- scripts/dev/freeze_tests.py | 2 +- scripts/dev/get_coredumpctl_traces.py | 2 +- scripts/dev/misc_checks.py | 2 +- scripts/dev/pylint_checkers/qute_pylint/config.py | 2 +- scripts/dev/pylint_checkers/qute_pylint/modeline.py | 2 +- scripts/dev/pylint_checkers/qute_pylint/openencoding.py | 2 +- scripts/dev/pylint_checkers/qute_pylint/settrace.py | 2 +- scripts/dev/pylint_checkers/setup.py | 2 +- scripts/dev/recompile_requirements.py | 2 +- scripts/dev/run_frozen_tests.py | 2 +- scripts/dev/run_profile.py | 2 +- scripts/dev/run_pylint_on_tests.py | 2 +- scripts/dev/run_vulture.py | 2 +- scripts/dev/segfault_test.py | 2 +- scripts/dev/src2asciidoc.py | 2 +- scripts/dev/ua_fetch.py | 5 +++-- scripts/dev/update_3rdparty.py | 1 + scripts/hostblock_blame.py | 2 +- scripts/importer.py | 3 ++- scripts/keytester.py | 2 +- scripts/link_pyqt.py | 2 +- scripts/setupcommon.py | 2 +- scripts/testbrowser.py | 2 +- scripts/utils.py | 2 +- setup.py | 2 +- tests/conftest.py | 2 +- tests/end2end/conftest.py | 2 +- tests/end2end/features/conftest.py | 2 +- tests/end2end/features/test_adblock_bdd.py | 2 +- tests/end2end/features/test_backforward_bdd.py | 2 +- tests/end2end/features/test_caret_bdd.py | 2 +- tests/end2end/features/test_completion_bdd.py | 2 +- tests/end2end/features/test_downloads_bdd.py | 2 +- tests/end2end/features/test_editor_bdd.py | 2 +- tests/end2end/features/test_hints_bdd.py | 2 +- tests/end2end/features/test_history_bdd.py | 2 +- tests/end2end/features/test_invoke_bdd.py | 2 +- tests/end2end/features/test_javascript_bdd.py | 2 +- tests/end2end/features/test_keyinput_bdd.py | 2 +- tests/end2end/features/test_marks_bdd.py | 2 +- tests/end2end/features/test_misc_bdd.py | 2 +- tests/end2end/features/test_navigate_bdd.py | 2 +- tests/end2end/features/test_open_bdd.py | 2 +- tests/end2end/features/test_prompts_bdd.py | 2 +- tests/end2end/features/test_scroll_bdd.py | 2 +- tests/end2end/features/test_search_bdd.py | 2 +- tests/end2end/features/test_sessions_bdd.py | 2 +- tests/end2end/features/test_set_bdd.py | 2 +- tests/end2end/features/test_spawn_bdd.py | 2 +- tests/end2end/features/test_tabs_bdd.py | 2 +- tests/end2end/features/test_urlmarks_bdd.py | 2 +- tests/end2end/features/test_utilcmds_bdd.py | 2 +- tests/end2end/features/test_yankpaste_bdd.py | 2 +- tests/end2end/features/test_zoom_bdd.py | 2 +- tests/end2end/fixtures/quteprocess.py | 2 +- tests/end2end/fixtures/test_quteprocess.py | 2 +- tests/end2end/fixtures/test_testprocess.py | 2 +- tests/end2end/fixtures/test_webserver.py | 2 +- tests/end2end/fixtures/testprocess.py | 2 +- tests/end2end/fixtures/webserver.py | 2 +- tests/end2end/fixtures/webserver_sub.py | 2 +- tests/end2end/fixtures/webserver_sub_ssl.py | 2 +- tests/end2end/test_dirbrowser.py | 2 +- tests/end2end/test_hints_html.py | 2 +- tests/end2end/test_insert_mode.py | 2 +- tests/end2end/test_invocations.py | 2 +- tests/end2end/test_mhtml_e2e.py | 2 +- tests/helpers/fixtures.py | 2 +- tests/helpers/logfail.py | 2 +- tests/helpers/messagemock.py | 2 +- tests/helpers/stubs.py | 2 +- tests/helpers/test_helper_utils.py | 2 +- tests/helpers/test_logfail.py | 2 +- tests/helpers/test_stubs.py | 2 +- tests/helpers/utils.py | 2 +- tests/test_conftest.py | 2 +- tests/unit/browser/test_commands.py | 2 +- tests/unit/browser/test_shared.py | 2 +- tests/unit/browser/test_signalfilter.py | 2 +- tests/unit/browser/test_tab.py | 2 +- tests/unit/browser/webengine/test_webenginedownloads.py | 2 +- tests/unit/browser/webkit/http/test_content_disposition.py | 2 +- tests/unit/browser/webkit/http/test_http.py | 2 +- tests/unit/browser/webkit/http/test_http_hypothesis.py | 2 +- tests/unit/browser/webkit/network/test_filescheme.py | 2 +- tests/unit/browser/webkit/network/test_networkmanager.py | 2 +- tests/unit/browser/webkit/network/test_networkreply.py | 2 +- tests/unit/browser/webkit/network/test_pac.py | 2 +- tests/unit/browser/webkit/network/test_schemehandler.py | 2 +- tests/unit/browser/webkit/network/test_webkitqutescheme.py | 3 ++- tests/unit/browser/webkit/test_cache.py | 2 +- tests/unit/browser/webkit/test_cookies.py | 2 +- tests/unit/browser/webkit/test_downloads.py | 2 +- tests/unit/browser/webkit/test_history.py | 2 +- tests/unit/browser/webkit/test_mhtml.py | 2 +- tests/unit/browser/webkit/test_qt_javascript.py | 2 +- tests/unit/browser/webkit/test_tabhistory.py | 2 +- tests/unit/browser/webkit/test_webkitelem.py | 2 +- tests/unit/commands/test_argparser.py | 2 +- tests/unit/commands/test_cmdutils.py | 2 +- tests/unit/commands/test_runners.py | 2 +- tests/unit/commands/test_userscripts.py | 2 +- tests/unit/completion/test_column_widths.py | 2 +- tests/unit/completion/test_completer.py | 2 +- tests/unit/completion/test_completionwidget.py | 2 +- tests/unit/completion/test_models.py | 2 +- tests/unit/completion/test_sortfilter.py | 2 +- tests/unit/config/test_config.py | 2 +- tests/unit/config/test_configdata.py | 2 +- tests/unit/config/test_configexc.py | 2 +- tests/unit/config/test_configtypes.py | 2 +- tests/unit/config/test_configtypes_hypothesis.py | 2 +- tests/unit/config/test_style.py | 2 +- tests/unit/config/test_textwrapper.py | 2 +- tests/unit/javascript/conftest.py | 2 +- tests/unit/javascript/position_caret/test_position_caret.py | 2 +- tests/unit/keyinput/conftest.py | 2 +- tests/unit/keyinput/test_basekeyparser.py | 2 +- tests/unit/keyinput/test_modeman.py | 2 +- tests/unit/keyinput/test_modeparsers.py | 2 +- tests/unit/mainwindow/statusbar/test_percentage.py | 2 +- tests/unit/mainwindow/statusbar/test_progress.py | 2 +- tests/unit/mainwindow/statusbar/test_tabindex.py | 2 +- tests/unit/mainwindow/statusbar/test_textbase.py | 2 +- tests/unit/mainwindow/statusbar/test_url.py | 2 +- tests/unit/mainwindow/test_messageview.py | 2 +- tests/unit/mainwindow/test_prompt.py | 2 +- tests/unit/mainwindow/test_tabwidget.py | 2 +- tests/unit/misc/test_autoupdate.py | 2 +- tests/unit/misc/test_checkpyver.py | 2 +- tests/unit/misc/test_cmdhistory.py | 4 ++-- tests/unit/misc/test_crashdialog.py | 2 +- tests/unit/misc/test_earlyinit.py | 2 +- tests/unit/misc/test_editor.py | 2 +- tests/unit/misc/test_guiprocess.py | 2 +- tests/unit/misc/test_ipc.py | 2 +- tests/unit/misc/test_keyhints.py | 2 +- tests/unit/misc/test_lineparser.py | 2 +- tests/unit/misc/test_miscwidgets.py | 2 +- tests/unit/misc/test_msgbox.py | 2 +- tests/unit/misc/test_pastebin.py | 2 +- tests/unit/misc/test_readline.py | 2 +- tests/unit/misc/test_sessions.py | 2 +- tests/unit/misc/test_split.py | 2 +- tests/unit/misc/test_split_hypothesis.py | 2 +- tests/unit/misc/test_utilcmds.py | 2 +- tests/unit/scripts/test_check_coverage.py | 2 +- tests/unit/scripts/test_run_vulture.py | 2 +- tests/unit/test_app.py | 2 +- tests/unit/utils/overflow_test_cases.py | 2 +- tests/unit/utils/test_debug.py | 2 +- tests/unit/utils/test_error.py | 2 +- tests/unit/utils/test_javascript.py | 2 +- tests/unit/utils/test_jinja.py | 2 +- tests/unit/utils/test_log.py | 2 +- tests/unit/utils/test_qtutils.py | 2 +- tests/unit/utils/test_standarddir.py | 2 +- tests/unit/utils/test_typing.py | 2 +- tests/unit/utils/test_urlutils.py | 2 +- tests/unit/utils/test_utils.py | 2 +- tests/unit/utils/test_version.py | 2 +- tests/unit/utils/usertypes/test_enum.py | 2 +- tests/unit/utils/usertypes/test_misc.py | 2 +- tests/unit/utils/usertypes/test_neighborlist.py | 2 +- tests/unit/utils/usertypes/test_question.py | 2 +- tests/unit/utils/usertypes/test_timer.py | 2 +- 326 files changed, 334 insertions(+), 326 deletions(-) diff --git a/misc/userscripts/dmenu_qutebrowser b/misc/userscripts/dmenu_qutebrowser index dbc7c05bf..6917dae98 100755 --- a/misc/userscripts/dmenu_qutebrowser +++ b/misc/userscripts/dmenu_qutebrowser @@ -1,6 +1,7 @@ #!/usr/bin/env bash # Copyright 2015 Zach-Button +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/misc/userscripts/openfeeds b/misc/userscripts/openfeeds index 085bdbe67..8bc4c2d33 100755 --- a/misc/userscripts/openfeeds +++ b/misc/userscripts/openfeeds @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # Copyright 2015 jnphilipp +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/misc/userscripts/qutebrowser_viewsource b/misc/userscripts/qutebrowser_viewsource index b528c41e8..a8ad71de3 100755 --- a/misc/userscripts/qutebrowser_viewsource +++ b/misc/userscripts/qutebrowser_viewsource @@ -1,6 +1,7 @@ #!/usr/bin/env bash # Copyright 2015 Zach-Button +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/__init__.py b/qutebrowser/__init__.py index 348cf407a..e61419c0c 100644 --- a/qutebrowser/__init__.py +++ b/qutebrowser/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -22,7 +22,7 @@ import os.path __author__ = "Florian Bruhin" -__copyright__ = "Copyright 2014-2016 Florian Bruhin (The Compiler)" +__copyright__ = "Copyright 2014-2017 Florian Bruhin (The Compiler)" __license__ = "GPL" __maintainer__ = __author__ __email__ = "mail@qutebrowser.org" diff --git a/qutebrowser/__main__.py b/qutebrowser/__main__.py index 4f977d2d2..506039890 100644 --- a/qutebrowser/__main__.py +++ b/qutebrowser/__main__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 8ca322078..cca9e1a76 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/__init__.py b/qutebrowser/browser/__init__.py index dbc790589..c5d5e6c92 100644 --- a/qutebrowser/browser/__init__.py +++ b/qutebrowser/browser/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/adblock.py b/qutebrowser/browser/adblock.py index a7d0d43bb..276dec08b 100644 --- a/qutebrowser/browser/adblock.py +++ b/qutebrowser/browser/adblock.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index aa7025edf..8e705d9df 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index d2fd1bca0..12de21b33 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index 8182aabfd..c7cb5ad8e 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/downloadview.py b/qutebrowser/browser/downloadview.py index 715fe2a54..199f4d1d8 100644 --- a/qutebrowser/browser/downloadview.py +++ b/qutebrowser/browser/downloadview.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index 144b13f76..a7a5941f9 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py index 2615fd393..294425549 100644 --- a/qutebrowser/browser/history.py +++ b/qutebrowser/browser/history.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/inspector.py b/qutebrowser/browser/inspector.py index a18d1ecf2..e225c31da 100644 --- a/qutebrowser/browser/inspector.py +++ b/qutebrowser/browser/inspector.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/mouse.py b/qutebrowser/browser/mouse.py index 79b6816bb..d1cd889d3 100644 --- a/qutebrowser/browser/mouse.py +++ b/qutebrowser/browser/mouse.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/navigate.py b/qutebrowser/browser/navigate.py index aacec9d3c..b1ab6f9b1 100644 --- a/qutebrowser/browser/navigate.py +++ b/qutebrowser/browser/navigate.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/network/pac.py b/qutebrowser/browser/network/pac.py index 63220d9b5..217cf21d9 100644 --- a/qutebrowser/browser/network/pac.py +++ b/qutebrowser/browser/network/pac.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/network/proxy.py b/qutebrowser/browser/network/proxy.py index 719c33178..1bdbc7b0b 100644 --- a/qutebrowser/browser/network/proxy.py +++ b/qutebrowser/browser/network/proxy.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/pdfjs.py b/qutebrowser/browser/pdfjs.py index d11cf3098..d003cefb1 100644 --- a/qutebrowser/browser/pdfjs.py +++ b/qutebrowser/browser/pdfjs.py @@ -1,6 +1,7 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: # Copyright 2015 Daniel Schadt +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py index 56149a8cc..f3bc4dfe2 100644 --- a/qutebrowser/browser/qtnetworkdownloads.py +++ b/qutebrowser/browser/qtnetworkdownloads.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index accdab4ba..919e6e186 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py index 885e2809d..005d652a3 100644 --- a/qutebrowser/browser/shared.py +++ b/qutebrowser/browser/shared.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/signalfilter.py b/qutebrowser/browser/signalfilter.py index 78aa18b42..90b85c586 100644 --- a/qutebrowser/browser/signalfilter.py +++ b/qutebrowser/browser/signalfilter.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/urlmarks.py b/qutebrowser/browser/urlmarks.py index 991ba7fc5..013de408c 100644 --- a/qutebrowser/browser/urlmarks.py +++ b/qutebrowser/browser/urlmarks.py @@ -1,7 +1,7 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) -# Copyright 2015-2016 Antoni Boucher +# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Antoni Boucher # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py index de6b9cfda..dce808871 100644 --- a/qutebrowser/browser/webelem.py +++ b/qutebrowser/browser/webelem.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webengine/__init__.py b/qutebrowser/browser/webengine/__init__.py index d7c910b36..60d140540 100644 --- a/qutebrowser/browser/webengine/__init__.py +++ b/qutebrowser/browser/webengine/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webengine/certificateerror.py b/qutebrowser/browser/webengine/certificateerror.py index 19b59c522..07c4bbe1b 100644 --- a/qutebrowser/browser/webengine/certificateerror.py +++ b/qutebrowser/browser/webengine/certificateerror.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webengine/interceptor.py b/qutebrowser/browser/webengine/interceptor.py index 76c29a6eb..e02f621a3 100644 --- a/qutebrowser/browser/webengine/interceptor.py +++ b/qutebrowser/browser/webengine/interceptor.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webengine/tabhistory.py b/qutebrowser/browser/webengine/tabhistory.py index ec43aa07b..5db6faeb1 100644 --- a/qutebrowser/browser/webengine/tabhistory.py +++ b/qutebrowser/browser/webengine/tabhistory.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webengine/webenginedownloads.py b/qutebrowser/browser/webengine/webenginedownloads.py index 26b23d77f..f1ca2e6e3 100644 --- a/qutebrowser/browser/webengine/webenginedownloads.py +++ b/qutebrowser/browser/webengine/webenginedownloads.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webengine/webengineelem.py b/qutebrowser/browser/webengine/webengineelem.py index 11d663df2..dc170115d 100644 --- a/qutebrowser/browser/webengine/webengineelem.py +++ b/qutebrowser/browser/webengine/webengineelem.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webengine/webengineinspector.py b/qutebrowser/browser/webengine/webengineinspector.py index b822bd253..cbb258039 100644 --- a/qutebrowser/browser/webengine/webengineinspector.py +++ b/qutebrowser/browser/webengine/webengineinspector.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webengine/webenginequtescheme.py b/qutebrowser/browser/webengine/webenginequtescheme.py index cebf31356..5acf6fcba 100644 --- a/qutebrowser/browser/webengine/webenginequtescheme.py +++ b/qutebrowser/browser/webengine/webenginequtescheme.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index 740db489b..e1f4a22c9 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index ffecb5686..5d2c171c3 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py index 67e5fc259..ee6e099bf 100644 --- a/qutebrowser/browser/webengine/webview.py +++ b/qutebrowser/browser/webengine/webview.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/__init__.py b/qutebrowser/browser/webkit/__init__.py index b67442be7..93b53cdba 100644 --- a/qutebrowser/browser/webkit/__init__.py +++ b/qutebrowser/browser/webkit/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/cache.py b/qutebrowser/browser/webkit/cache.py index be7dba486..8ae9aa0f2 100644 --- a/qutebrowser/browser/webkit/cache.py +++ b/qutebrowser/browser/webkit/cache.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/certificateerror.py b/qutebrowser/browser/webkit/certificateerror.py index 7ebcb3b2f..d02ded76c 100644 --- a/qutebrowser/browser/webkit/certificateerror.py +++ b/qutebrowser/browser/webkit/certificateerror.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/cookies.py b/qutebrowser/browser/webkit/cookies.py index 113eb661a..79f7a67fa 100644 --- a/qutebrowser/browser/webkit/cookies.py +++ b/qutebrowser/browser/webkit/cookies.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/http.py b/qutebrowser/browser/webkit/http.py index f55542c67..08cad7a44 100644 --- a/qutebrowser/browser/webkit/http.py +++ b/qutebrowser/browser/webkit/http.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/mhtml.py b/qutebrowser/browser/webkit/mhtml.py index bc9ea2695..ed357f2bd 100644 --- a/qutebrowser/browser/webkit/mhtml.py +++ b/qutebrowser/browser/webkit/mhtml.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Daniel Schadt +# Copyright 2015-2017 Daniel Schadt # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/network/filescheme.py b/qutebrowser/browser/webkit/network/filescheme.py index cd0a6d489..2f3a3ff18 100644 --- a/qutebrowser/browser/webkit/network/filescheme.py +++ b/qutebrowser/browser/webkit/network/filescheme.py @@ -1,7 +1,7 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) -# Copyright 2015-2016 Antoni Boucher (antoyo) +# Copyright 2014-2017 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Antoni Boucher (antoyo) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index 915a2082f..30adf3170 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/network/networkreply.py b/qutebrowser/browser/webkit/network/networkreply.py index dc1f09f0c..a4a4f59ca 100644 --- a/qutebrowser/browser/webkit/network/networkreply.py +++ b/qutebrowser/browser/webkit/network/networkreply.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # Based on the Eric5 helpviewer, # Copyright (c) 2009 - 2014 Detlev Offenbach diff --git a/qutebrowser/browser/webkit/network/schemehandler.py b/qutebrowser/browser/webkit/network/schemehandler.py index 9975db121..c6337efa3 100644 --- a/qutebrowser/browser/webkit/network/schemehandler.py +++ b/qutebrowser/browser/webkit/network/schemehandler.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # Based on the Eric5 helpviewer, # Copyright (c) 2009 - 2014 Detlev Offenbach diff --git a/qutebrowser/browser/webkit/network/webkitqutescheme.py b/qutebrowser/browser/webkit/network/webkitqutescheme.py index 34db29ee9..6e83e60a0 100644 --- a/qutebrowser/browser/webkit/network/webkitqutescheme.py +++ b/qutebrowser/browser/webkit/network/webkitqutescheme.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/rfc6266.py b/qutebrowser/browser/webkit/rfc6266.py index 11a1d76a1..c3c8f7a4b 100644 --- a/qutebrowser/browser/webkit/rfc6266.py +++ b/qutebrowser/browser/webkit/rfc6266.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/tabhistory.py b/qutebrowser/browser/webkit/tabhistory.py index 3f80676a9..19e4ef15c 100644 --- a/qutebrowser/browser/webkit/tabhistory.py +++ b/qutebrowser/browser/webkit/tabhistory.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/webkitelem.py b/qutebrowser/browser/webkit/webkitelem.py index a570eae3c..a6fa1ba4f 100644 --- a/qutebrowser/browser/webkit/webkitelem.py +++ b/qutebrowser/browser/webkit/webkitelem.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/webkithistory.py b/qutebrowser/browser/webkit/webkithistory.py index abd80eb39..0f9d64460 100644 --- a/qutebrowser/browser/webkit/webkithistory.py +++ b/qutebrowser/browser/webkit/webkithistory.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/webkitinspector.py b/qutebrowser/browser/webkit/webkitinspector.py index 9e612b28e..9a056a896 100644 --- a/qutebrowser/browser/webkit/webkitinspector.py +++ b/qutebrowser/browser/webkit/webkitinspector.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/webkitsettings.py b/qutebrowser/browser/webkit/webkitsettings.py index 747af9b5d..0d4564e7e 100644 --- a/qutebrowser/browser/webkit/webkitsettings.py +++ b/qutebrowser/browser/webkit/webkitsettings.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index a5a02338e..3de652887 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index 7fc891c4c..a687dd5e2 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/browser/webkit/webview.py b/qutebrowser/browser/webkit/webview.py index 0c3aa179e..01abca639 100644 --- a/qutebrowser/browser/webkit/webview.py +++ b/qutebrowser/browser/webkit/webview.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/commands/__init__.py b/qutebrowser/commands/__init__.py index 70f058d82..7bc59ae40 100644 --- a/qutebrowser/commands/__init__.py +++ b/qutebrowser/commands/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/commands/argparser.py b/qutebrowser/commands/argparser.py index c0e08ccb6..9dfe841ce 100644 --- a/qutebrowser/commands/argparser.py +++ b/qutebrowser/commands/argparser.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/commands/cmdexc.py b/qutebrowser/commands/cmdexc.py index 766d20620..51e20fec7 100644 --- a/qutebrowser/commands/cmdexc.py +++ b/qutebrowser/commands/cmdexc.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py index 3641c3cb9..9b1539b71 100644 --- a/qutebrowser/commands/cmdutils.py +++ b/qutebrowser/commands/cmdutils.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/commands/command.py b/qutebrowser/commands/command.py index d46cc5c77..b40f7ab16 100644 --- a/qutebrowser/commands/command.py +++ b/qutebrowser/commands/command.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index 0a2d6085f..cc967ce86 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/commands/userscripts.py b/qutebrowser/commands/userscripts.py index 4533e86b1..f3802d04c 100644 --- a/qutebrowser/commands/userscripts.py +++ b/qutebrowser/commands/userscripts.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/completion/__init__.py b/qutebrowser/completion/__init__.py index d3caf8703..8b8b9d88d 100644 --- a/qutebrowser/completion/__init__.py +++ b/qutebrowser/completion/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py index 74c759c0d..96d937829 100644 --- a/qutebrowser/completion/completer.py +++ b/qutebrowser/completion/completer.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/completion/completiondelegate.py b/qutebrowser/completion/completiondelegate.py index dfb479b3f..1d5dfadf0 100644 --- a/qutebrowser/completion/completiondelegate.py +++ b/qutebrowser/completion/completiondelegate.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index e9fad39fd..490fcd6c0 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/completion/models/__init__.py b/qutebrowser/completion/models/__init__.py index 99f1954fe..5812545eb 100644 --- a/qutebrowser/completion/models/__init__.py +++ b/qutebrowser/completion/models/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/completion/models/base.py b/qutebrowser/completion/models/base.py index 1ee45af71..b1cad276a 100644 --- a/qutebrowser/completion/models/base.py +++ b/qutebrowser/completion/models/base.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/completion/models/configmodel.py b/qutebrowser/completion/models/configmodel.py index 4058a5f00..c9e9850d1 100644 --- a/qutebrowser/completion/models/configmodel.py +++ b/qutebrowser/completion/models/configmodel.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/completion/models/instances.py b/qutebrowser/completion/models/instances.py index 6359d3771..f7eaaca86 100644 --- a/qutebrowser/completion/models/instances.py +++ b/qutebrowser/completion/models/instances.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/completion/models/miscmodels.py b/qutebrowser/completion/models/miscmodels.py index 76c1a8997..5ab381c43 100644 --- a/qutebrowser/completion/models/miscmodels.py +++ b/qutebrowser/completion/models/miscmodels.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/completion/models/sortfilter.py b/qutebrowser/completion/models/sortfilter.py index 3df787539..2bb454bf9 100644 --- a/qutebrowser/completion/models/sortfilter.py +++ b/qutebrowser/completion/models/sortfilter.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/completion/models/urlmodel.py b/qutebrowser/completion/models/urlmodel.py index a222df4f6..98f68c08c 100644 --- a/qutebrowser/completion/models/urlmodel.py +++ b/qutebrowser/completion/models/urlmodel.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/config/__init__.py b/qutebrowser/config/__init__.py index e2a04ee47..bf0bce0ec 100644 --- a/qutebrowser/config/__init__.py +++ b/qutebrowser/config/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 73aa2ae22..ccd5afd56 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 347edc53f..6c3c61187 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/config/configexc.py b/qutebrowser/config/configexc.py index a8fd0af2e..b19d45d7b 100644 --- a/qutebrowser/config/configexc.py +++ b/qutebrowser/config/configexc.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 2dad85117..9c390b2b0 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/config/parsers/__init__.py b/qutebrowser/config/parsers/__init__.py index 5e2183794..1c316078d 100644 --- a/qutebrowser/config/parsers/__init__.py +++ b/qutebrowser/config/parsers/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/config/parsers/ini.py b/qutebrowser/config/parsers/ini.py index 56640e299..0ae485f4b 100644 --- a/qutebrowser/config/parsers/ini.py +++ b/qutebrowser/config/parsers/ini.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/config/parsers/keyconf.py b/qutebrowser/config/parsers/keyconf.py index 2f0d8df69..53f23d7c0 100644 --- a/qutebrowser/config/parsers/keyconf.py +++ b/qutebrowser/config/parsers/keyconf.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/config/sections.py b/qutebrowser/config/sections.py index 254348fe9..04a735647 100644 --- a/qutebrowser/config/sections.py +++ b/qutebrowser/config/sections.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/config/style.py b/qutebrowser/config/style.py index b2697daac..15215c398 100644 --- a/qutebrowser/config/style.py +++ b/qutebrowser/config/style.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/config/textwrapper.py b/qutebrowser/config/textwrapper.py index a36a73b6f..b5744f60b 100644 --- a/qutebrowser/config/textwrapper.py +++ b/qutebrowser/config/textwrapper.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/config/value.py b/qutebrowser/config/value.py index 388d8febc..b23674606 100644 --- a/qutebrowser/config/value.py +++ b/qutebrowser/config/value.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py index b6e010499..b2a54e58b 100644 --- a/qutebrowser/config/websettings.py +++ b/qutebrowser/config/websettings.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/javascript/position_caret.js b/qutebrowser/javascript/position_caret.js index 09d2301ad..4f6c32380 100644 --- a/qutebrowser/javascript/position_caret.js +++ b/qutebrowser/javascript/position_caret.js @@ -1,6 +1,6 @@ /** * Copyright 2015 Artur Shaik -* Copyright 2015-2016 Florian Bruhin (The Compiler) +* Copyright 2015-2017 Florian Bruhin (The Compiler) * * This file is part of qutebrowser. * diff --git a/qutebrowser/javascript/scroll.js b/qutebrowser/javascript/scroll.js index ac19d175c..35f412783 100644 --- a/qutebrowser/javascript/scroll.js +++ b/qutebrowser/javascript/scroll.js @@ -1,5 +1,5 @@ /** - * Copyright 2016 Florian Bruhin (The Compiler) + * Copyright 2016-2017 Florian Bruhin (The Compiler) * * This file is part of qutebrowser. * diff --git a/qutebrowser/javascript/webelem.js b/qutebrowser/javascript/webelem.js index 8c7883b3b..6d763b8df 100644 --- a/qutebrowser/javascript/webelem.js +++ b/qutebrowser/javascript/webelem.js @@ -1,5 +1,5 @@ /** - * Copyright 2016 Florian Bruhin (The Compiler) + * Copyright 2016-2017 Florian Bruhin (The Compiler) * * This file is part of qutebrowser. * diff --git a/qutebrowser/keyinput/__init__.py b/qutebrowser/keyinput/__init__.py index 6fb35e5d6..c6b95b8a9 100644 --- a/qutebrowser/keyinput/__init__.py +++ b/qutebrowser/keyinput/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/keyinput/basekeyparser.py b/qutebrowser/keyinput/basekeyparser.py index 7325223a3..670cde853 100644 --- a/qutebrowser/keyinput/basekeyparser.py +++ b/qutebrowser/keyinput/basekeyparser.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/keyinput/keyparser.py b/qutebrowser/keyinput/keyparser.py index 0b46dffc4..f9a64edca 100644 --- a/qutebrowser/keyinput/keyparser.py +++ b/qutebrowser/keyinput/keyparser.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/keyinput/macros.py b/qutebrowser/keyinput/macros.py index 8176e5652..9e3667590 100644 --- a/qutebrowser/keyinput/macros.py +++ b/qutebrowser/keyinput/macros.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Jan Verbeek (blyxxyz) +# Copyright 2016-2017 Jan Verbeek (blyxxyz) # # This file is part of qutebrowser. # diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index 542081719..5ce55e670 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index db540b58e..4ef393b03 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/__init__.py b/qutebrowser/mainwindow/__init__.py index 178413514..43eb563a9 100644 --- a/qutebrowser/mainwindow/__init__.py +++ b/qutebrowser/mainwindow/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index fbc86d010..96489fbac 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/messageview.py b/qutebrowser/mainwindow/messageview.py index 7b2d64e07..7d0d2b682 100644 --- a/qutebrowser/mainwindow/messageview.py +++ b/qutebrowser/mainwindow/messageview.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index db91cc71a..c38a41caa 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/statusbar/__init__.py b/qutebrowser/mainwindow/statusbar/__init__.py index c6a25fe0c..eb3ed7193 100644 --- a/qutebrowser/mainwindow/statusbar/__init__.py +++ b/qutebrowser/mainwindow/statusbar/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index eaf8e6ffc..cddf4ec35 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/statusbar/command.py b/qutebrowser/mainwindow/statusbar/command.py index a5abaa290..d59c87dbc 100644 --- a/qutebrowser/mainwindow/statusbar/command.py +++ b/qutebrowser/mainwindow/statusbar/command.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/statusbar/keystring.py b/qutebrowser/mainwindow/statusbar/keystring.py index 0baa8137c..dd9825ab2 100644 --- a/qutebrowser/mainwindow/statusbar/keystring.py +++ b/qutebrowser/mainwindow/statusbar/keystring.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/statusbar/percentage.py b/qutebrowser/mainwindow/statusbar/percentage.py index 9c2d2dcd9..050b0747a 100644 --- a/qutebrowser/mainwindow/statusbar/percentage.py +++ b/qutebrowser/mainwindow/statusbar/percentage.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/statusbar/progress.py b/qutebrowser/mainwindow/statusbar/progress.py index 17892fe33..e78d5307c 100644 --- a/qutebrowser/mainwindow/statusbar/progress.py +++ b/qutebrowser/mainwindow/statusbar/progress.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/statusbar/tabindex.py b/qutebrowser/mainwindow/statusbar/tabindex.py index 7dda5f806..6a4cc987c 100644 --- a/qutebrowser/mainwindow/statusbar/tabindex.py +++ b/qutebrowser/mainwindow/statusbar/tabindex.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/statusbar/text.py b/qutebrowser/mainwindow/statusbar/text.py index 2791385b7..e99891ecd 100644 --- a/qutebrowser/mainwindow/statusbar/text.py +++ b/qutebrowser/mainwindow/statusbar/text.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/statusbar/textbase.py b/qutebrowser/mainwindow/statusbar/textbase.py index eb3064286..0ae271191 100644 --- a/qutebrowser/mainwindow/statusbar/textbase.py +++ b/qutebrowser/mainwindow/statusbar/textbase.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/statusbar/url.py b/qutebrowser/mainwindow/statusbar/url.py index c7bee2ae9..b54e020fa 100644 --- a/qutebrowser/mainwindow/statusbar/url.py +++ b/qutebrowser/mainwindow/statusbar/url.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 885c4dc78..896ea5c9d 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/mainwindow/tabwidget.py b/qutebrowser/mainwindow/tabwidget.py index 4e57c2dd6..e1cc2f5ef 100644 --- a/qutebrowser/mainwindow/tabwidget.py +++ b/qutebrowser/mainwindow/tabwidget.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/__init__.py b/qutebrowser/misc/__init__.py index 3dc51e6a9..03ad27aa8 100644 --- a/qutebrowser/misc/__init__.py +++ b/qutebrowser/misc/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/autoupdate.py b/qutebrowser/misc/autoupdate.py index 860064672..15a3b6670 100644 --- a/qutebrowser/misc/autoupdate.py +++ b/qutebrowser/misc/autoupdate.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/checkpyver.py b/qutebrowser/misc/checkpyver.py index 364b8bb57..34183041b 100644 --- a/qutebrowser/misc/checkpyver.py +++ b/qutebrowser/misc/checkpyver.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The-Compiler) +# Copyright 2014-2017 Florian Bruhin (The-Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/cmdhistory.py b/qutebrowser/misc/cmdhistory.py index 7f3dfd52f..b0990a67e 100644 --- a/qutebrowser/misc/cmdhistory.py +++ b/qutebrowser/misc/cmdhistory.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/consolewidget.py b/qutebrowser/misc/consolewidget.py index 485da0fe4..537621b4d 100644 --- a/qutebrowser/misc/consolewidget.py +++ b/qutebrowser/misc/consolewidget.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py index 0add7932a..045406c13 100644 --- a/qutebrowser/misc/crashdialog.py +++ b/qutebrowser/misc/crashdialog.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py index d8d8bb385..5f161312a 100644 --- a/qutebrowser/misc/crashsignal.py +++ b/qutebrowser/misc/crashsignal.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py index 3622dc36a..99fd3e1a3 100644 --- a/qutebrowser/misc/earlyinit.py +++ b/qutebrowser/misc/earlyinit.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The-Compiler) +# Copyright 2014-2017 Florian Bruhin (The-Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/editor.py b/qutebrowser/misc/editor.py index a6f2854d8..58a08daf1 100644 --- a/qutebrowser/misc/editor.py +++ b/qutebrowser/misc/editor.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/guiprocess.py b/qutebrowser/misc/guiprocess.py index 8f986d1b1..e8d224f2e 100644 --- a/qutebrowser/misc/guiprocess.py +++ b/qutebrowser/misc/guiprocess.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/httpclient.py b/qutebrowser/misc/httpclient.py index fc6f94f59..4d33d487e 100644 --- a/qutebrowser/misc/httpclient.py +++ b/qutebrowser/misc/httpclient.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/ipc.py b/qutebrowser/misc/ipc.py index f6567a11f..eb9aa4a3b 100644 --- a/qutebrowser/misc/ipc.py +++ b/qutebrowser/misc/ipc.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/keyhintwidget.py b/qutebrowser/misc/keyhintwidget.py index d32b24eb9..b42612b1b 100644 --- a/qutebrowser/misc/keyhintwidget.py +++ b/qutebrowser/misc/keyhintwidget.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Ryan Roden-Corrent (rcorre) +# Copyright 2016-2017 Ryan Roden-Corrent (rcorre) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/lineparser.py b/qutebrowser/misc/lineparser.py index 55bae7142..ea9d100b7 100644 --- a/qutebrowser/misc/lineparser.py +++ b/qutebrowser/misc/lineparser.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/miscwidgets.py b/qutebrowser/misc/miscwidgets.py index 956d60d4a..48f775b85 100644 --- a/qutebrowser/misc/miscwidgets.py +++ b/qutebrowser/misc/miscwidgets.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/msgbox.py b/qutebrowser/misc/msgbox.py index f6f29c38d..2c8aaf85e 100644 --- a/qutebrowser/misc/msgbox.py +++ b/qutebrowser/misc/msgbox.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/pastebin.py b/qutebrowser/misc/pastebin.py index 40dd77f33..9c30ca067 100644 --- a/qutebrowser/misc/pastebin.py +++ b/qutebrowser/misc/pastebin.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/readline.py b/qutebrowser/misc/readline.py index 0089ebe7c..2bc999c4d 100644 --- a/qutebrowser/misc/readline.py +++ b/qutebrowser/misc/readline.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/savemanager.py b/qutebrowser/misc/savemanager.py index 7e85b013e..509e5489a 100644 --- a/qutebrowser/misc/savemanager.py +++ b/qutebrowser/misc/savemanager.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index 8bf55016f..cba3629ef 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/split.py b/qutebrowser/misc/split.py index cb7a38d81..3f3b2d362 100644 --- a/qutebrowser/misc/split.py +++ b/qutebrowser/misc/split.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index de43c71f4..41b44de1f 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index 01054f14d..b4673ecff 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/__init__.py b/qutebrowser/utils/__init__.py index 763345a7b..c28a40b10 100644 --- a/qutebrowser/utils/__init__.py +++ b/qutebrowser/utils/__init__.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/debug.py b/qutebrowser/utils/debug.py index a516c43f3..5da5234a9 100644 --- a/qutebrowser/utils/debug.py +++ b/qutebrowser/utils/debug.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/docutils.py b/qutebrowser/utils/docutils.py index 40cb0cb70..1a3b4312d 100644 --- a/qutebrowser/utils/docutils.py +++ b/qutebrowser/utils/docutils.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/error.py b/qutebrowser/utils/error.py index 6a818857f..0d045bf19 100644 --- a/qutebrowser/utils/error.py +++ b/qutebrowser/utils/error.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/javascript.py b/qutebrowser/utils/javascript.py index 4fc7e546c..f536fed1f 100644 --- a/qutebrowser/utils/javascript.py +++ b/qutebrowser/utils/javascript.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/jinja.py b/qutebrowser/utils/jinja.py index 2ad2be448..731dec8e4 100644 --- a/qutebrowser/utils/jinja.py +++ b/qutebrowser/utils/jinja.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py index 19b4f506f..1cea959ee 100644 --- a/qutebrowser/utils/log.py +++ b/qutebrowser/utils/log.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py index 8f39e8174..35ab604b0 100644 --- a/qutebrowser/utils/message.py +++ b/qutebrowser/utils/message.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/objreg.py b/qutebrowser/utils/objreg.py index e54502ef8..4df08ba0d 100644 --- a/qutebrowser/utils/objreg.py +++ b/qutebrowser/utils/objreg.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 9bd606f02..15c9e72ce 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/standarddir.py b/qutebrowser/utils/standarddir.py index 7c756b805..a1cedddd0 100644 --- a/qutebrowser/utils/standarddir.py +++ b/qutebrowser/utils/standarddir.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/typing.py b/qutebrowser/utils/typing.py index dca42acd6..358a1a5a3 100644 --- a/qutebrowser/utils/typing.py +++ b/qutebrowser/utils/typing.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 23bf97f9f..4cd0e94d0 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/usertypes.py b/qutebrowser/utils/usertypes.py index 34148f9af..7d31ba6ac 100644 --- a/qutebrowser/utils/usertypes.py +++ b/qutebrowser/utils/usertypes.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index c34df896e..d2de174a8 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 0565281bc..a8e115ca5 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/scripts/asciidoc2html.py b/scripts/asciidoc2html.py index 613f622a8..cfdc1b8d5 100755 --- a/scripts/asciidoc2html.py +++ b/scripts/asciidoc2html.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/scripts/dev/build_release.py b/scripts/dev/build_release.py index fb8fe000d..aca2d0ef0 100755 --- a/scripts/dev/build_release.py +++ b/scripts/dev/build_release.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py index 9025d8740..4abb43f8b 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/scripts/dev/check_doc_changes.py b/scripts/dev/check_doc_changes.py index d072bfb65..ab879b5ac 100755 --- a/scripts/dev/check_doc_changes.py +++ b/scripts/dev/check_doc_changes.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/scripts/dev/ci/appveyor_install.py b/scripts/dev/ci/appveyor_install.py index 131906248..60c07bad6 100644 --- a/scripts/dev/ci/appveyor_install.py +++ b/scripts/dev/ci/appveyor_install.py @@ -1,7 +1,7 @@ #!/usr/bin/env python2 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/scripts/dev/ci/travis_install.sh b/scripts/dev/ci/travis_install.sh index e752685f5..0ada134c1 100644 --- a/scripts/dev/ci/travis_install.sh +++ b/scripts/dev/ci/travis_install.sh @@ -1,6 +1,6 @@ # vim: ft=sh fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/scripts/dev/cleanup.py b/scripts/dev/cleanup.py index f08292017..49832eb3d 100755 --- a/scripts/dev/cleanup.py +++ b/scripts/dev/cleanup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/scripts/dev/freeze.py b/scripts/dev/freeze.py index f254f4d90..8f99d2d35 100755 --- a/scripts/dev/freeze.py +++ b/scripts/dev/freeze.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/scripts/dev/freeze_tests.py b/scripts/dev/freeze_tests.py index 9f9e2bbd2..97405b446 100755 --- a/scripts/dev/freeze_tests.py +++ b/scripts/dev/freeze_tests.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/scripts/dev/get_coredumpctl_traces.py b/scripts/dev/get_coredumpctl_traces.py index d1e962834..8cae2a190 100644 --- a/scripts/dev/get_coredumpctl_traces.py +++ b/scripts/dev/get_coredumpctl_traces.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/scripts/dev/misc_checks.py b/scripts/dev/misc_checks.py index 49ca8e48f..1bb263d15 100644 --- a/scripts/dev/misc_checks.py +++ b/scripts/dev/misc_checks.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/scripts/dev/pylint_checkers/qute_pylint/config.py b/scripts/dev/pylint_checkers/qute_pylint/config.py index b93b211f1..be8ac8da8 100644 --- a/scripts/dev/pylint_checkers/qute_pylint/config.py +++ b/scripts/dev/pylint_checkers/qute_pylint/config.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/scripts/dev/pylint_checkers/qute_pylint/modeline.py b/scripts/dev/pylint_checkers/qute_pylint/modeline.py index 580837b34..ee3de13c9 100644 --- a/scripts/dev/pylint_checkers/qute_pylint/modeline.py +++ b/scripts/dev/pylint_checkers/qute_pylint/modeline.py @@ -1,4 +1,4 @@ -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: # This file is part of qutebrowser. diff --git a/scripts/dev/pylint_checkers/qute_pylint/openencoding.py b/scripts/dev/pylint_checkers/qute_pylint/openencoding.py index 83926fc5e..eccc152ba 100644 --- a/scripts/dev/pylint_checkers/qute_pylint/openencoding.py +++ b/scripts/dev/pylint_checkers/qute_pylint/openencoding.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/scripts/dev/pylint_checkers/qute_pylint/settrace.py b/scripts/dev/pylint_checkers/qute_pylint/settrace.py index 9c1196daa..2bfa9f06f 100644 --- a/scripts/dev/pylint_checkers/qute_pylint/settrace.py +++ b/scripts/dev/pylint_checkers/qute_pylint/settrace.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/scripts/dev/pylint_checkers/setup.py b/scripts/dev/pylint_checkers/setup.py index eaecf406e..960fdd2b7 100644 --- a/scripts/dev/pylint_checkers/setup.py +++ b/scripts/dev/pylint_checkers/setup.py @@ -2,7 +2,7 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/scripts/dev/recompile_requirements.py b/scripts/dev/recompile_requirements.py index 1e3181344..44c56202a 100644 --- a/scripts/dev/recompile_requirements.py +++ b/scripts/dev/recompile_requirements.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/scripts/dev/run_frozen_tests.py b/scripts/dev/run_frozen_tests.py index e64325417..1684884a3 100644 --- a/scripts/dev/run_frozen_tests.py +++ b/scripts/dev/run_frozen_tests.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/scripts/dev/run_profile.py b/scripts/dev/run_profile.py index 87d2f0ed7..31fe539aa 100755 --- a/scripts/dev/run_profile.py +++ b/scripts/dev/run_profile.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/scripts/dev/run_pylint_on_tests.py b/scripts/dev/run_pylint_on_tests.py index e4412708d..b9fb4845d 100644 --- a/scripts/dev/run_pylint_on_tests.py +++ b/scripts/dev/run_pylint_on_tests.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/scripts/dev/run_vulture.py b/scripts/dev/run_vulture.py index f1eed1803..40fe2f591 100755 --- a/scripts/dev/run_vulture.py +++ b/scripts/dev/run_vulture.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/scripts/dev/segfault_test.py b/scripts/dev/segfault_test.py index cac8c6a14..c5e7c106f 100755 --- a/scripts/dev/segfault_test.py +++ b/scripts/dev/segfault_test.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/scripts/dev/src2asciidoc.py b/scripts/dev/src2asciidoc.py index 1b99022a7..7445a99c6 100755 --- a/scripts/dev/src2asciidoc.py +++ b/scripts/dev/src2asciidoc.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/scripts/dev/ua_fetch.py b/scripts/dev/ua_fetch.py index f37797041..14f71d525 100644 --- a/scripts/dev/ua_fetch.py +++ b/scripts/dev/ua_fetch.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 lamarpavel -# Copyright 2015-2016 Alexey Nabrodov (Averrin) +# Copyright 2015-2017 lamarpavel +# Copyright 2015-2017 Alexey Nabrodov (Averrin) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/scripts/dev/update_3rdparty.py b/scripts/dev/update_3rdparty.py index 4b88b165f..722ce2316 100755 --- a/scripts/dev/update_3rdparty.py +++ b/scripts/dev/update_3rdparty.py @@ -2,6 +2,7 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: # Copyright 2015 Daniel Schadt +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/scripts/hostblock_blame.py b/scripts/hostblock_blame.py index 7e444793b..dde83d91f 100644 --- a/scripts/hostblock_blame.py +++ b/scripts/hostblock_blame.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/scripts/importer.py b/scripts/importer.py index 5e0883cd2..1b3be4d32 100755 --- a/scripts/importer.py +++ b/scripts/importer.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Claude (longneck) +# Copyright 2014-2017 Claude (longneck) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/scripts/keytester.py b/scripts/keytester.py index ebed5f62c..b147599b6 100644 --- a/scripts/keytester.py +++ b/scripts/keytester.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/scripts/link_pyqt.py b/scripts/link_pyqt.py index fffe27fb3..a7de598cd 100644 --- a/scripts/link_pyqt.py +++ b/scripts/link_pyqt.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/scripts/setupcommon.py b/scripts/setupcommon.py index 9a447d280..494ab4c46 100644 --- a/scripts/setupcommon.py +++ b/scripts/setupcommon.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/scripts/testbrowser.py b/scripts/testbrowser.py index 75c36bd95..fbe48c451 100755 --- a/scripts/testbrowser.py +++ b/scripts/testbrowser.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/scripts/utils.py b/scripts/utils.py index 55eb679bd..6793bb2a7 100644 --- a/scripts/utils.py +++ b/scripts/utils.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/setup.py b/setup.py index 6e594af12..f594009a0 100755 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2015 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/conftest.py b/tests/conftest.py index 53dbbb752..2fdf8ab9a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/conftest.py b/tests/end2end/conftest.py index b374f53dd..8dac6a41d 100644 --- a/tests/end2end/conftest.py +++ b/tests/end2end/conftest.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/conftest.py b/tests/end2end/features/conftest.py index 1e9c34462..c2b5865a5 100644 --- a/tests/end2end/features/conftest.py +++ b/tests/end2end/features/conftest.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_adblock_bdd.py b/tests/end2end/features/test_adblock_bdd.py index 9f4ae63b3..069c127aa 100644 --- a/tests/end2end/features/test_adblock_bdd.py +++ b/tests/end2end/features/test_adblock_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_backforward_bdd.py b/tests/end2end/features/test_backforward_bdd.py index ede51988a..187882b67 100644 --- a/tests/end2end/features/test_backforward_bdd.py +++ b/tests/end2end/features/test_backforward_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_caret_bdd.py b/tests/end2end/features/test_caret_bdd.py index aa42241c4..9e4e1dedd 100644 --- a/tests/end2end/features/test_caret_bdd.py +++ b/tests/end2end/features/test_caret_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_completion_bdd.py b/tests/end2end/features/test_completion_bdd.py index 030a16ffc..f4ada848f 100644 --- a/tests/end2end/features/test_completion_bdd.py +++ b/tests/end2end/features/test_completion_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_downloads_bdd.py b/tests/end2end/features/test_downloads_bdd.py index 616c3cb14..25eb52aad 100644 --- a/tests/end2end/features/test_downloads_bdd.py +++ b/tests/end2end/features/test_downloads_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_editor_bdd.py b/tests/end2end/features/test_editor_bdd.py index f26e2956f..7d38be5af 100644 --- a/tests/end2end/features/test_editor_bdd.py +++ b/tests/end2end/features/test_editor_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_hints_bdd.py b/tests/end2end/features/test_hints_bdd.py index b5304cb74..f39a15391 100644 --- a/tests/end2end/features/test_hints_bdd.py +++ b/tests/end2end/features/test_hints_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_history_bdd.py b/tests/end2end/features/test_history_bdd.py index 871dbcb98..1fee533eb 100644 --- a/tests/end2end/features/test_history_bdd.py +++ b/tests/end2end/features/test_history_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_invoke_bdd.py b/tests/end2end/features/test_invoke_bdd.py index 86faf8107..5d463608e 100644 --- a/tests/end2end/features/test_invoke_bdd.py +++ b/tests/end2end/features/test_invoke_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_javascript_bdd.py b/tests/end2end/features/test_javascript_bdd.py index 78d45ab5a..ca65a9d1d 100644 --- a/tests/end2end/features/test_javascript_bdd.py +++ b/tests/end2end/features/test_javascript_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_keyinput_bdd.py b/tests/end2end/features/test_keyinput_bdd.py index d6b3134a3..ef5be0ee9 100644 --- a/tests/end2end/features/test_keyinput_bdd.py +++ b/tests/end2end/features/test_keyinput_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_marks_bdd.py b/tests/end2end/features/test_marks_bdd.py index 5e0e623f2..5b8e352dd 100644 --- a/tests/end2end/features/test_marks_bdd.py +++ b/tests/end2end/features/test_marks_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Ryan Roden-Corrent (rcorre) +# Copyright 2016-2017 Ryan Roden-Corrent (rcorre) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_misc_bdd.py b/tests/end2end/features/test_misc_bdd.py index 9f7ce63e6..e78840d68 100644 --- a/tests/end2end/features/test_misc_bdd.py +++ b/tests/end2end/features/test_misc_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_navigate_bdd.py b/tests/end2end/features/test_navigate_bdd.py index 5f922fedc..03812df83 100644 --- a/tests/end2end/features/test_navigate_bdd.py +++ b/tests/end2end/features/test_navigate_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_open_bdd.py b/tests/end2end/features/test_open_bdd.py index 48b7226f7..e5692c615 100644 --- a/tests/end2end/features/test_open_bdd.py +++ b/tests/end2end/features/test_open_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_prompts_bdd.py b/tests/end2end/features/test_prompts_bdd.py index 3ebd53e8c..7a95d48c8 100644 --- a/tests/end2end/features/test_prompts_bdd.py +++ b/tests/end2end/features/test_prompts_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_scroll_bdd.py b/tests/end2end/features/test_scroll_bdd.py index de1ed3e0a..69199ebf3 100644 --- a/tests/end2end/features/test_scroll_bdd.py +++ b/tests/end2end/features/test_scroll_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_search_bdd.py b/tests/end2end/features/test_search_bdd.py index caf592ce6..1b0c81488 100644 --- a/tests/end2end/features/test_search_bdd.py +++ b/tests/end2end/features/test_search_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_sessions_bdd.py b/tests/end2end/features/test_sessions_bdd.py index d05b4d434..052748f90 100644 --- a/tests/end2end/features/test_sessions_bdd.py +++ b/tests/end2end/features/test_sessions_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_set_bdd.py b/tests/end2end/features/test_set_bdd.py index 2eabd8c56..fa94de88f 100644 --- a/tests/end2end/features/test_set_bdd.py +++ b/tests/end2end/features/test_set_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_spawn_bdd.py b/tests/end2end/features/test_spawn_bdd.py index e9ddf0301..432f95a53 100644 --- a/tests/end2end/features/test_spawn_bdd.py +++ b/tests/end2end/features/test_spawn_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_tabs_bdd.py b/tests/end2end/features/test_tabs_bdd.py index bcae6d60d..e86204ba9 100644 --- a/tests/end2end/features/test_tabs_bdd.py +++ b/tests/end2end/features/test_tabs_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_urlmarks_bdd.py b/tests/end2end/features/test_urlmarks_bdd.py index 170fdd30b..554ede3ec 100644 --- a/tests/end2end/features/test_urlmarks_bdd.py +++ b/tests/end2end/features/test_urlmarks_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_utilcmds_bdd.py b/tests/end2end/features/test_utilcmds_bdd.py index f90d587f6..01bfb1440 100644 --- a/tests/end2end/features/test_utilcmds_bdd.py +++ b/tests/end2end/features/test_utilcmds_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_yankpaste_bdd.py b/tests/end2end/features/test_yankpaste_bdd.py index 9deb4b3cf..8f2f56938 100644 --- a/tests/end2end/features/test_yankpaste_bdd.py +++ b/tests/end2end/features/test_yankpaste_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/features/test_zoom_bdd.py b/tests/end2end/features/test_zoom_bdd.py index 3f8728222..3dc94a8cd 100644 --- a/tests/end2end/features/test_zoom_bdd.py +++ b/tests/end2end/features/test_zoom_bdd.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py index 5f99990d8..040110e77 100644 --- a/tests/end2end/fixtures/quteprocess.py +++ b/tests/end2end/fixtures/quteprocess.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/fixtures/test_quteprocess.py b/tests/end2end/fixtures/test_quteprocess.py index 748310769..b40b5ebb6 100644 --- a/tests/end2end/fixtures/test_quteprocess.py +++ b/tests/end2end/fixtures/test_quteprocess.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/fixtures/test_testprocess.py b/tests/end2end/fixtures/test_testprocess.py index ef5b445ca..b38e79f56 100644 --- a/tests/end2end/fixtures/test_testprocess.py +++ b/tests/end2end/fixtures/test_testprocess.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/fixtures/test_webserver.py b/tests/end2end/fixtures/test_webserver.py index 5f0fa8ab0..c885ea5b6 100644 --- a/tests/end2end/fixtures/test_webserver.py +++ b/tests/end2end/fixtures/test_webserver.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/fixtures/testprocess.py b/tests/end2end/fixtures/testprocess.py index c6f4edd22..4c3a6972e 100644 --- a/tests/end2end/fixtures/testprocess.py +++ b/tests/end2end/fixtures/testprocess.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/fixtures/webserver.py b/tests/end2end/fixtures/webserver.py index c43765bf6..8fc674e42 100644 --- a/tests/end2end/fixtures/webserver.py +++ b/tests/end2end/fixtures/webserver.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/fixtures/webserver_sub.py b/tests/end2end/fixtures/webserver_sub.py index 51a03e0b5..53c033f5b 100644 --- a/tests/end2end/fixtures/webserver_sub.py +++ b/tests/end2end/fixtures/webserver_sub.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/fixtures/webserver_sub_ssl.py b/tests/end2end/fixtures/webserver_sub_ssl.py index dadcb510e..d8a8f1025 100644 --- a/tests/end2end/fixtures/webserver_sub_ssl.py +++ b/tests/end2end/fixtures/webserver_sub_ssl.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/test_dirbrowser.py b/tests/end2end/test_dirbrowser.py index 2359020d7..3b27eebb3 100644 --- a/tests/end2end/test_dirbrowser.py +++ b/tests/end2end/test_dirbrowser.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Daniel Schadt +# Copyright 2015-2017 Daniel Schadt # # This file is part of qutebrowser. # diff --git a/tests/end2end/test_hints_html.py b/tests/end2end/test_hints_html.py index c2d5143bc..d155ffcc4 100644 --- a/tests/end2end/test_hints_html.py +++ b/tests/end2end/test_hints_html.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/test_insert_mode.py b/tests/end2end/test_insert_mode.py index 8b9ed3aca..3bd5d87fd 100644 --- a/tests/end2end/test_insert_mode.py +++ b/tests/end2end/test_insert_mode.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py index d683e9c85..e4470ac3a 100644 --- a/tests/end2end/test_invocations.py +++ b/tests/end2end/test_invocations.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/end2end/test_mhtml_e2e.py b/tests/end2end/test_mhtml_e2e.py index 46ece8f0e..f753e6fd4 100644 --- a/tests/end2end/test_mhtml_e2e.py +++ b/tests/end2end/test_mhtml_e2e.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/helpers/fixtures.py b/tests/helpers/fixtures.py index f9e829e40..3af7195f5 100644 --- a/tests/helpers/fixtures.py +++ b/tests/helpers/fixtures.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/helpers/logfail.py b/tests/helpers/logfail.py index 2967e3ccf..3d8e3afb8 100644 --- a/tests/helpers/logfail.py +++ b/tests/helpers/logfail.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/helpers/messagemock.py b/tests/helpers/messagemock.py index 7854aabcc..77116115f 100644 --- a/tests/helpers/messagemock.py +++ b/tests/helpers/messagemock.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index 3e028b0c9..df598e4ed 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/helpers/test_helper_utils.py b/tests/helpers/test_helper_utils.py index 65c7c29a1..d7eadb894 100644 --- a/tests/helpers/test_helper_utils.py +++ b/tests/helpers/test_helper_utils.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/helpers/test_logfail.py b/tests/helpers/test_logfail.py index 6bc4f364c..b95dec1d6 100644 --- a/tests/helpers/test_logfail.py +++ b/tests/helpers/test_logfail.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/helpers/test_stubs.py b/tests/helpers/test_stubs.py index d542afe54..10fa9e5db 100644 --- a/tests/helpers/test_stubs.py +++ b/tests/helpers/test_stubs.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/helpers/utils.py b/tests/helpers/utils.py index 52a843dbc..1ef469144 100644 --- a/tests/helpers/utils.py +++ b/tests/helpers/utils.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/test_conftest.py b/tests/test_conftest.py index 24fb67097..74db4bf78 100644 --- a/tests/test_conftest.py +++ b/tests/test_conftest.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/test_commands.py b/tests/unit/browser/test_commands.py index e7654a2d7..5a9d7a8da 100644 --- a/tests/unit/browser/test_commands.py +++ b/tests/unit/browser/test_commands.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/test_shared.py b/tests/unit/browser/test_shared.py index da81c2059..8cd1d6704 100644 --- a/tests/unit/browser/test_shared.py +++ b/tests/unit/browser/test_shared.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/test_signalfilter.py b/tests/unit/browser/test_signalfilter.py index bf8651d7f..4c02f89c4 100644 --- a/tests/unit/browser/test_signalfilter.py +++ b/tests/unit/browser/test_signalfilter.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/test_tab.py b/tests/unit/browser/test_tab.py index 7ff1edbde..bfd060956 100644 --- a/tests/unit/browser/test_tab.py +++ b/tests/unit/browser/test_tab.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/webengine/test_webenginedownloads.py b/tests/unit/browser/webengine/test_webenginedownloads.py index 735852cd3..4ad146306 100644 --- a/tests/unit/browser/webengine/test_webenginedownloads.py +++ b/tests/unit/browser/webengine/test_webenginedownloads.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/webkit/http/test_content_disposition.py b/tests/unit/browser/webkit/http/test_content_disposition.py index 7b817c5e9..e1f78eb74 100644 --- a/tests/unit/browser/webkit/http/test_content_disposition.py +++ b/tests/unit/browser/webkit/http/test_content_disposition.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/webkit/http/test_http.py b/tests/unit/browser/webkit/http/test_http.py index ee80baed7..1f9b7d75d 100644 --- a/tests/unit/browser/webkit/http/test_http.py +++ b/tests/unit/browser/webkit/http/test_http.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/webkit/http/test_http_hypothesis.py b/tests/unit/browser/webkit/http/test_http_hypothesis.py index 1fb4dd835..42290a8f4 100644 --- a/tests/unit/browser/webkit/http/test_http_hypothesis.py +++ b/tests/unit/browser/webkit/http/test_http_hypothesis.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/webkit/network/test_filescheme.py b/tests/unit/browser/webkit/network/test_filescheme.py index c3ef870d6..7a1bd9b19 100644 --- a/tests/unit/browser/webkit/network/test_filescheme.py +++ b/tests/unit/browser/webkit/network/test_filescheme.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Antoni Boucher (antoyo) +# Copyright 2015-2017 Antoni Boucher (antoyo) # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/webkit/network/test_networkmanager.py b/tests/unit/browser/webkit/network/test_networkmanager.py index 4ac93435d..3e8dd4d13 100644 --- a/tests/unit/browser/webkit/network/test_networkmanager.py +++ b/tests/unit/browser/webkit/network/test_networkmanager.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/webkit/network/test_networkreply.py b/tests/unit/browser/webkit/network/test_networkreply.py index 13e060c8a..8d24ebab8 100644 --- a/tests/unit/browser/webkit/network/test_networkreply.py +++ b/tests/unit/browser/webkit/network/test_networkreply.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/webkit/network/test_pac.py b/tests/unit/browser/webkit/network/test_pac.py index 2ad63a496..bc914bbfc 100644 --- a/tests/unit/browser/webkit/network/test_pac.py +++ b/tests/unit/browser/webkit/network/test_pac.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/webkit/network/test_schemehandler.py b/tests/unit/browser/webkit/network/test_schemehandler.py index d981b8412..1f5464a85 100644 --- a/tests/unit/browser/webkit/network/test_schemehandler.py +++ b/tests/unit/browser/webkit/network/test_schemehandler.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/webkit/network/test_webkitqutescheme.py b/tests/unit/browser/webkit/network/test_webkitqutescheme.py index f941abb6d..d45f1a31f 100644 --- a/tests/unit/browser/webkit/network/test_webkitqutescheme.py +++ b/tests/unit/browser/webkit/network/test_webkitqutescheme.py @@ -1,6 +1,7 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Daniel Schadt +# Copyright 2016-2017 Daniel Schadt +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/webkit/test_cache.py b/tests/unit/browser/webkit/test_cache.py index bc8efb675..f084a9869 100644 --- a/tests/unit/browser/webkit/test_cache.py +++ b/tests/unit/browser/webkit/test_cache.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 lamarpavel +# Copyright 2015-2017 lamarpavel # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/webkit/test_cookies.py b/tests/unit/browser/webkit/test_cookies.py index bcee01e60..85d045763 100644 --- a/tests/unit/browser/webkit/test_cookies.py +++ b/tests/unit/browser/webkit/test_cookies.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Alexander Cogneau (acogneau) : +# Copyright 2015-2017 Alexander Cogneau (acogneau) : # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/webkit/test_downloads.py b/tests/unit/browser/webkit/test_downloads.py index acee916b2..72b533cc7 100644 --- a/tests/unit/browser/webkit/test_downloads.py +++ b/tests/unit/browser/webkit/test_downloads.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/webkit/test_history.py b/tests/unit/browser/webkit/test_history.py index cc35dc4e4..37176dffe 100644 --- a/tests/unit/browser/webkit/test_history.py +++ b/tests/unit/browser/webkit/test_history.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/webkit/test_mhtml.py b/tests/unit/browser/webkit/test_mhtml.py index e5fdf4ffd..ad554cf3d 100644 --- a/tests/unit/browser/webkit/test_mhtml.py +++ b/tests/unit/browser/webkit/test_mhtml.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Daniel Schadt +# Copyright 2015-2017 Daniel Schadt # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/webkit/test_qt_javascript.py b/tests/unit/browser/webkit/test_qt_javascript.py index cb7c3a2ae..fd8a0c496 100644 --- a/tests/unit/browser/webkit/test_qt_javascript.py +++ b/tests/unit/browser/webkit/test_qt_javascript.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/webkit/test_tabhistory.py b/tests/unit/browser/webkit/test_tabhistory.py index b99d8376d..07b334771 100644 --- a/tests/unit/browser/webkit/test_tabhistory.py +++ b/tests/unit/browser/webkit/test_tabhistory.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/browser/webkit/test_webkitelem.py b/tests/unit/browser/webkit/test_webkitelem.py index c4384fa61..6258508ff 100644 --- a/tests/unit/browser/webkit/test_webkitelem.py +++ b/tests/unit/browser/webkit/test_webkitelem.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/commands/test_argparser.py b/tests/unit/commands/test_argparser.py index fc577fd15..54a584e71 100644 --- a/tests/unit/commands/test_argparser.py +++ b/tests/unit/commands/test_argparser.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/commands/test_cmdutils.py b/tests/unit/commands/test_cmdutils.py index 577a85540..06740031c 100644 --- a/tests/unit/commands/test_cmdutils.py +++ b/tests/unit/commands/test_cmdutils.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/commands/test_runners.py b/tests/unit/commands/test_runners.py index a7370081b..720fa5209 100644 --- a/tests/unit/commands/test_runners.py +++ b/tests/unit/commands/test_runners.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/commands/test_userscripts.py b/tests/unit/commands/test_userscripts.py index 5212b0d32..c6348d028 100644 --- a/tests/unit/commands/test_userscripts.py +++ b/tests/unit/commands/test_userscripts.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/completion/test_column_widths.py b/tests/unit/completion/test_column_widths.py index aae9c71aa..21456ed37 100644 --- a/tests/unit/completion/test_column_widths.py +++ b/tests/unit/completion/test_column_widths.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Alexander Cogneau +# Copyright 2015-2017 Alexander Cogneau # # This file is part of qutebrowser. # diff --git a/tests/unit/completion/test_completer.py b/tests/unit/completion/test_completer.py index 3f0f021bf..9feff4655 100644 --- a/tests/unit/completion/test_completer.py +++ b/tests/unit/completion/test_completer.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Ryan Roden-Corrent (rcorre) +# Copyright 2016-2017 Ryan Roden-Corrent (rcorre) # # This file is part of qutebrowser. # diff --git a/tests/unit/completion/test_completionwidget.py b/tests/unit/completion/test_completionwidget.py index 5a4ee87c0..9a8de3cad 100644 --- a/tests/unit/completion/test_completionwidget.py +++ b/tests/unit/completion/test_completionwidget.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Ryan Roden-Corrent (rcorre) +# Copyright 2016-2017 Ryan Roden-Corrent (rcorre) # # This file is part of qutebrowser. # diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py index a767476a4..ff00a11a9 100644 --- a/tests/unit/completion/test_models.py +++ b/tests/unit/completion/test_models.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Ryan Roden-Corrent (rcorre) +# Copyright 2016-2017 Ryan Roden-Corrent (rcorre) # # This file is part of qutebrowser. # diff --git a/tests/unit/completion/test_sortfilter.py b/tests/unit/completion/test_sortfilter.py index cef394226..2d4a4e25d 100644 --- a/tests/unit/completion/test_sortfilter.py +++ b/tests/unit/completion/test_sortfilter.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index e98302604..c897b9f16 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -1,5 +1,5 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/tests/unit/config/test_configdata.py b/tests/unit/config/test_configdata.py index 3d0d046b2..50509820e 100644 --- a/tests/unit/config/test_configdata.py +++ b/tests/unit/config/test_configdata.py @@ -1,5 +1,5 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/tests/unit/config/test_configexc.py b/tests/unit/config/test_configexc.py index 3cb8397d4..330ad7b07 100644 --- a/tests/unit/config/test_configexc.py +++ b/tests/unit/config/test_configexc.py @@ -1,5 +1,5 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index d44303278..4cfb9100c 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -1,5 +1,5 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/tests/unit/config/test_configtypes_hypothesis.py b/tests/unit/config/test_configtypes_hypothesis.py index ed45ccc0b..96f47622e 100644 --- a/tests/unit/config/test_configtypes_hypothesis.py +++ b/tests/unit/config/test_configtypes_hypothesis.py @@ -1,5 +1,5 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/tests/unit/config/test_style.py b/tests/unit/config/test_style.py index 6b55aebae..2e4d8c1ce 100644 --- a/tests/unit/config/test_style.py +++ b/tests/unit/config/test_style.py @@ -1,5 +1,5 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/tests/unit/config/test_textwrapper.py b/tests/unit/config/test_textwrapper.py index 9fb337091..a654f9367 100644 --- a/tests/unit/config/test_textwrapper.py +++ b/tests/unit/config/test_textwrapper.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/javascript/conftest.py b/tests/unit/javascript/conftest.py index 88cfecdc4..b9f013ecf 100644 --- a/tests/unit/javascript/conftest.py +++ b/tests/unit/javascript/conftest.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/javascript/position_caret/test_position_caret.py b/tests/unit/javascript/position_caret/test_position_caret.py index 2691c1afe..7be62e3cc 100644 --- a/tests/unit/javascript/position_caret/test_position_caret.py +++ b/tests/unit/javascript/position_caret/test_position_caret.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/keyinput/conftest.py b/tests/unit/keyinput/conftest.py index 75f318b53..28c24b2e2 100644 --- a/tests/unit/keyinput/conftest.py +++ b/tests/unit/keyinput/conftest.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) : +# Copyright 2015-2017 Florian Bruhin (The Compiler) : # # This file is part of qutebrowser. # diff --git a/tests/unit/keyinput/test_basekeyparser.py b/tests/unit/keyinput/test_basekeyparser.py index da1ecfbdf..0cb0763e7 100644 --- a/tests/unit/keyinput/test_basekeyparser.py +++ b/tests/unit/keyinput/test_basekeyparser.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) : +# Copyright 2014-2017 Florian Bruhin (The Compiler) : # # This file is part of qutebrowser. # diff --git a/tests/unit/keyinput/test_modeman.py b/tests/unit/keyinput/test_modeman.py index aa3a2753a..d00ce09f7 100644 --- a/tests/unit/keyinput/test_modeman.py +++ b/tests/unit/keyinput/test_modeman.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/keyinput/test_modeparsers.py b/tests/unit/keyinput/test_modeparsers.py index c01e0e5ba..d7da17129 100644 --- a/tests/unit/keyinput/test_modeparsers.py +++ b/tests/unit/keyinput/test_modeparsers.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) : +# Copyright 2015-2017 Florian Bruhin (The Compiler) : # # This file is part of qutebrowser. # diff --git a/tests/unit/mainwindow/statusbar/test_percentage.py b/tests/unit/mainwindow/statusbar/test_percentage.py index 0974e34ed..6622b06f4 100644 --- a/tests/unit/mainwindow/statusbar/test_percentage.py +++ b/tests/unit/mainwindow/statusbar/test_percentage.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/mainwindow/statusbar/test_progress.py b/tests/unit/mainwindow/statusbar/test_progress.py index 70865e104..f553f8c22 100644 --- a/tests/unit/mainwindow/statusbar/test_progress.py +++ b/tests/unit/mainwindow/statusbar/test_progress.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/mainwindow/statusbar/test_tabindex.py b/tests/unit/mainwindow/statusbar/test_tabindex.py index ca5673da2..99b5d9114 100644 --- a/tests/unit/mainwindow/statusbar/test_tabindex.py +++ b/tests/unit/mainwindow/statusbar/test_tabindex.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/mainwindow/statusbar/test_textbase.py b/tests/unit/mainwindow/statusbar/test_textbase.py index 1fb661296..b976f5c6f 100644 --- a/tests/unit/mainwindow/statusbar/test_textbase.py +++ b/tests/unit/mainwindow/statusbar/test_textbase.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/mainwindow/statusbar/test_url.py b/tests/unit/mainwindow/statusbar/test_url.py index 78f7ab646..fc8ffb705 100644 --- a/tests/unit/mainwindow/statusbar/test_url.py +++ b/tests/unit/mainwindow/statusbar/test_url.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Clayton Craft (craftyguy) +# Copyright 2016-2017 Clayton Craft (craftyguy) # # This file is part of qutebrowser. # diff --git a/tests/unit/mainwindow/test_messageview.py b/tests/unit/mainwindow/test_messageview.py index ba77f3a49..ebecc9398 100644 --- a/tests/unit/mainwindow/test_messageview.py +++ b/tests/unit/mainwindow/test_messageview.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/mainwindow/test_prompt.py b/tests/unit/mainwindow/test_prompt.py index dcc5461a5..6fa8592af 100644 --- a/tests/unit/mainwindow/test_prompt.py +++ b/tests/unit/mainwindow/test_prompt.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/mainwindow/test_tabwidget.py b/tests/unit/mainwindow/test_tabwidget.py index 7a3fd68af..b8c12eaee 100644 --- a/tests/unit/mainwindow/test_tabwidget.py +++ b/tests/unit/mainwindow/test_tabwidget.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Daniel Schadt +# Copyright 2015-2017 Daniel Schadt # # This file is part of qutebrowser. # diff --git a/tests/unit/misc/test_autoupdate.py b/tests/unit/misc/test_autoupdate.py index eb7ed6f91..d803e4877 100644 --- a/tests/unit/misc/test_autoupdate.py +++ b/tests/unit/misc/test_autoupdate.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Alexander Cogneau (acogneau) : +# Copyright 2015-2017 Alexander Cogneau (acogneau) : # # This file is part of qutebrowser. # diff --git a/tests/unit/misc/test_checkpyver.py b/tests/unit/misc/test_checkpyver.py index 4c2cf3b7d..aeab032e7 100644 --- a/tests/unit/misc/test_checkpyver.py +++ b/tests/unit/misc/test_checkpyver.py @@ -1,4 +1,4 @@ -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: # This file is part of qutebrowser. diff --git a/tests/unit/misc/test_cmdhistory.py b/tests/unit/misc/test_cmdhistory.py index 47284c075..7da721c2b 100644 --- a/tests/unit/misc/test_cmdhistory.py +++ b/tests/unit/misc/test_cmdhistory.py @@ -1,7 +1,7 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Alexander Cogneau (acogneau) -# Copyright 2015-2016 Florian Bruhin (The-Compiler) +# Copyright 2015-2017 Alexander Cogneau (acogneau) +# Copyright 2015-2017 Florian Bruhin (The-Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/misc/test_crashdialog.py b/tests/unit/misc/test_crashdialog.py index dd2ebd35e..89ca342ee 100644 --- a/tests/unit/misc/test_crashdialog.py +++ b/tests/unit/misc/test_crashdialog.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/misc/test_earlyinit.py b/tests/unit/misc/test_earlyinit.py index e61b06475..840936853 100644 --- a/tests/unit/misc/test_earlyinit.py +++ b/tests/unit/misc/test_earlyinit.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The-Compiler) +# Copyright 2016-2017 Florian Bruhin (The-Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/misc/test_editor.py b/tests/unit/misc/test_editor.py index 193f5fa30..de9125c8b 100644 --- a/tests/unit/misc/test_editor.py +++ b/tests/unit/misc/test_editor.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/misc/test_guiprocess.py b/tests/unit/misc/test_guiprocess.py index 2d804dbe5..3101b7427 100644 --- a/tests/unit/misc/test_guiprocess.py +++ b/tests/unit/misc/test_guiprocess.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/misc/test_ipc.py b/tests/unit/misc/test_ipc.py index eaa74b1ff..cf5cc9a3f 100644 --- a/tests/unit/misc/test_ipc.py +++ b/tests/unit/misc/test_ipc.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/misc/test_keyhints.py b/tests/unit/misc/test_keyhints.py index 34eea85b5..b1255eb2c 100644 --- a/tests/unit/misc/test_keyhints.py +++ b/tests/unit/misc/test_keyhints.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Ryan Roden-Corrent (rcorre) +# Copyright 2016-2017 Ryan Roden-Corrent (rcorre) # # This file is part of qutebrowser. # diff --git a/tests/unit/misc/test_lineparser.py b/tests/unit/misc/test_lineparser.py index 546d53cf9..0d2b3a52e 100644 --- a/tests/unit/misc/test_lineparser.py +++ b/tests/unit/misc/test_lineparser.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/misc/test_miscwidgets.py b/tests/unit/misc/test_miscwidgets.py index dd17ac254..03441d9f2 100644 --- a/tests/unit/misc/test_miscwidgets.py +++ b/tests/unit/misc/test_miscwidgets.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/misc/test_msgbox.py b/tests/unit/misc/test_msgbox.py index 311680266..2c1268bd8 100644 --- a/tests/unit/misc/test_msgbox.py +++ b/tests/unit/misc/test_msgbox.py @@ -1,4 +1,4 @@ -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: # This file is part of qutebrowser. diff --git a/tests/unit/misc/test_pastebin.py b/tests/unit/misc/test_pastebin.py index b60b319a6..5591bfd0f 100644 --- a/tests/unit/misc/test_pastebin.py +++ b/tests/unit/misc/test_pastebin.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Anna Kobak (avk) : +# Copyright 2016-2017 Anna Kobak (avk) : # # This file is part of qutebrowser. # diff --git a/tests/unit/misc/test_readline.py b/tests/unit/misc/test_readline.py index b0c1403a1..6c168774c 100644 --- a/tests/unit/misc/test_readline.py +++ b/tests/unit/misc/test_readline.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/misc/test_sessions.py b/tests/unit/misc/test_sessions.py index 4bef9b9a4..c30ac6bdc 100644 --- a/tests/unit/misc/test_sessions.py +++ b/tests/unit/misc/test_sessions.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/misc/test_split.py b/tests/unit/misc/test_split.py index 7b3c8015a..179084849 100644 --- a/tests/unit/misc/test_split.py +++ b/tests/unit/misc/test_split.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/misc/test_split_hypothesis.py b/tests/unit/misc/test_split_hypothesis.py index 52eb8cb11..c63ddff09 100644 --- a/tests/unit/misc/test_split_hypothesis.py +++ b/tests/unit/misc/test_split_hypothesis.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/misc/test_utilcmds.py b/tests/unit/misc/test_utilcmds.py index 41c2f1f15..e4b686e31 100644 --- a/tests/unit/misc/test_utilcmds.py +++ b/tests/unit/misc/test_utilcmds.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/scripts/test_check_coverage.py b/tests/unit/scripts/test_check_coverage.py index 16182bcf7..8b80d6e44 100644 --- a/tests/unit/scripts/test_check_coverage.py +++ b/tests/unit/scripts/test_check_coverage.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/tests/unit/scripts/test_run_vulture.py b/tests/unit/scripts/test_run_vulture.py index 91f7a0744..c19d70b29 100644 --- a/tests/unit/scripts/test_run_vulture.py +++ b/tests/unit/scripts/test_run_vulture.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # This file is part of qutebrowser. # diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index ecb3ec46e..080399ecc 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/utils/overflow_test_cases.py b/tests/unit/utils/overflow_test_cases.py index 308e5f9a3..450522d6b 100644 --- a/tests/unit/utils/overflow_test_cases.py +++ b/tests/unit/utils/overflow_test_cases.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/utils/test_debug.py b/tests/unit/utils/test_debug.py index 7bcad59ec..76c84fee1 100644 --- a/tests/unit/utils/test_debug.py +++ b/tests/unit/utils/test_debug.py @@ -1,4 +1,4 @@ -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: # diff --git a/tests/unit/utils/test_error.py b/tests/unit/utils/test_error.py index abcc72c44..4f4905365 100644 --- a/tests/unit/utils/test_error.py +++ b/tests/unit/utils/test_error.py @@ -1,4 +1,4 @@ -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: # This file is part of qutebrowser. diff --git a/tests/unit/utils/test_javascript.py b/tests/unit/utils/test_javascript.py index 298a75312..54c32ee92 100644 --- a/tests/unit/utils/test_javascript.py +++ b/tests/unit/utils/test_javascript.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/utils/test_jinja.py b/tests/unit/utils/test_jinja.py index 289b2c26b..5d8798b96 100644 --- a/tests/unit/utils/test_jinja.py +++ b/tests/unit/utils/test_jinja.py @@ -1,4 +1,4 @@ -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: # diff --git a/tests/unit/utils/test_log.py b/tests/unit/utils/test_log.py index 3c1e82fc0..147e760bb 100644 --- a/tests/unit/utils/test_log.py +++ b/tests/unit/utils/test_log.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/utils/test_qtutils.py b/tests/unit/utils/test_qtutils.py index e0b4bf934..45daa36d8 100644 --- a/tests/unit/utils/test_qtutils.py +++ b/tests/unit/utils/test_qtutils.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/utils/test_standarddir.py b/tests/unit/utils/test_standarddir.py index 69acd2884..bc7a58975 100644 --- a/tests/unit/utils/test_standarddir.py +++ b/tests/unit/utils/test_standarddir.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/utils/test_typing.py b/tests/unit/utils/test_typing.py index fc507299c..4a3359a45 100644 --- a/tests/unit/utils/test_typing.py +++ b/tests/unit/utils/test_typing.py @@ -1,4 +1,4 @@ -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: # diff --git a/tests/unit/utils/test_urlutils.py b/tests/unit/utils/test_urlutils.py index 9b3c5d1c3..74c244b7b 100644 --- a/tests/unit/utils/test_urlutils.py +++ b/tests/unit/utils/test_urlutils.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index d42f7a971..7eb4008a6 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py index 13c0a5d13..96f4bf88f 100644 --- a/tests/unit/utils/test_version.py +++ b/tests/unit/utils/test_version.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/utils/usertypes/test_enum.py b/tests/unit/utils/usertypes/test_enum.py index b78251171..a5d2f33e5 100644 --- a/tests/unit/utils/usertypes/test_enum.py +++ b/tests/unit/utils/usertypes/test_enum.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/utils/usertypes/test_misc.py b/tests/unit/utils/usertypes/test_misc.py index c2c0738e5..d21816ace 100644 --- a/tests/unit/utils/usertypes/test_misc.py +++ b/tests/unit/utils/usertypes/test_misc.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2016 Florian Bruhin (The Compiler) +# Copyright 2016-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/utils/usertypes/test_neighborlist.py b/tests/unit/utils/usertypes/test_neighborlist.py index 7bfbe3e2c..751f940a3 100644 --- a/tests/unit/utils/usertypes/test_neighborlist.py +++ b/tests/unit/utils/usertypes/test_neighborlist.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2014-2016 Florian Bruhin (The Compiler) +# Copyright 2014-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/utils/usertypes/test_question.py b/tests/unit/utils/usertypes/test_question.py index 119d4ee3f..031e304cc 100644 --- a/tests/unit/utils/usertypes/test_question.py +++ b/tests/unit/utils/usertypes/test_question.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # diff --git a/tests/unit/utils/usertypes/test_timer.py b/tests/unit/utils/usertypes/test_timer.py index e48bd332b..d120d82e6 100644 --- a/tests/unit/utils/usertypes/test_timer.py +++ b/tests/unit/utils/usertypes/test_timer.py @@ -1,6 +1,6 @@ # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2016 Florian Bruhin (The Compiler) +# Copyright 2015-2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # From b91d4ee9c2598caa40f1705355d3b58f22e5edea Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 9 May 2017 22:02:30 +0200 Subject: [PATCH 226/825] Clean up :debug-webaction --- doc/help/commands.asciidoc | 2 +- qutebrowser/browser/browsertab.py | 17 ++++++++- qutebrowser/browser/commands.py | 35 ++++--------------- qutebrowser/browser/webengine/webenginetab.py | 10 +++--- qutebrowser/browser/webkit/webkittab.py | 3 ++ 5 files changed, 32 insertions(+), 35 deletions(-) diff --git a/doc/help/commands.asciidoc b/doc/help/commands.asciidoc index 1a76e7fc8..c79d83d08 100644 --- a/doc/help/commands.asciidoc +++ b/doc/help/commands.asciidoc @@ -1655,7 +1655,7 @@ Syntax: +:debug-webaction 'action'+ Execute a webaction. -See http://doc.qt.io/qt-5/qwebpage.html#WebAction-enum for the available actions. +Available actions: http://doc.qt.io/archives/qt-5.5/qwebpage.html#WebAction-enum (WebKit) http://doc.qt.io/qt-5/qwebenginepage.html#WebAction-enum (WebEngine) ==== positional arguments * +'action'+: The action to execute, e.g. MoveToNextChar. diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 8e705d9df..62fb67453 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -105,7 +105,15 @@ class TabData: class AbstractAction: - """Attribute of AbstractTab for Qt WebActions.""" + """Attribute of AbstractTab for Qt WebActions. + + Class attributes (overridden by subclasses): + action_class: The class actions are defined on (QWeb{Engine,}Page) + action_base: The type of the actions (QWeb{Engine,}Page.WebAction) + """ + + action_class = None + action_base = None def __init__(self): self._widget = None @@ -118,6 +126,13 @@ class AbstractAction: """Save the current page.""" raise NotImplementedError + def run_string(self, name): + """Run a webaction based on its name.""" + member = getattr(self.action_class, name, None) + if not isinstance(member, self.action_base): + raise WebTabError("{} is not a valid web action!".format(name)) + self._widget.triggerPageAction(member) + class AbstractPrinting: diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 12de21b33..6b34e4d30 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -28,14 +28,6 @@ from PyQt5.QtWidgets import QApplication, QTabBar from PyQt5.QtCore import Qt, QUrl, QEvent, QUrlQuery from PyQt5.QtGui import QKeyEvent from PyQt5.QtPrintSupport import QPrintDialog, QPrintPreviewDialog -try: - from PyQt5.QtWebKitWidgets import QWebPage -except ImportError: - QWebPage = None -try: - from PyQt5.QtWebEngineWidgets import QWebEnginePage -except ImportError: - QWebEnginePage = None import pygments import pygments.lexers import pygments.formatters @@ -1905,33 +1897,20 @@ class CommandDispatcher: def debug_webaction(self, action, count=1): """Execute a webaction. - See http://doc.qt.io/qt-5/qwebpage.html#WebAction-enum for the - available actions. + Available actions: + http://doc.qt.io/archives/qt-5.5/qwebpage.html#WebAction-enum (WebKit) + http://doc.qt.io/qt-5/qwebenginepage.html#WebAction-enum (WebEngine) Args: action: The action to execute, e.g. MoveToNextChar. count: How many times to repeat the action. """ tab = self._current_widget() - - if tab.backend == usertypes.Backend.QtWebKit: - assert QWebPage is not None - member = getattr(QWebPage, action, None) - base = QWebPage.WebAction - elif tab.backend == usertypes.Backend.QtWebEngine: - assert QWebEnginePage is not None - member = getattr(QWebEnginePage, action, None) - base = QWebEnginePage.WebAction - - if not isinstance(member, base): - raise cmdexc.CommandError("{} is not a valid web action!".format( - action)) - for _ in range(count): - # This whole command is backend-specific anyways, so it makes no - # sense to introduce some API for this. - # pylint: disable=protected-access - tab._widget.triggerPageAction(member) + try: + tab.action.run_string(action) + except browsertab.WebTabError as e: + raise cmdexc.CommandError(str(e)) @cmdutils.register(instance='command-dispatcher', scope='window', maxsplit=0, no_cmd_split=True) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 5d2c171c3..af93786d6 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -79,17 +79,17 @@ _JS_WORLD_MAP = { class WebEngineAction(browsertab.AbstractAction): - """QtWebKit implementations related to web actions.""" + """QtWebEngine implementations related to web actions.""" - def _action(self, action): - self._widget.triggerPageAction(action) + action_class = QWebEnginePage + action_base = QWebEnginePage.WebAction def exit_fullscreen(self): - self._action(QWebEnginePage.ExitFullScreen) + self._widget.triggerPageAction(QWebEnginePage.ExitFullScreen) def save_page(self): """Save the current page.""" - self._action(QWebEnginePage.SavePage) + self._widget.triggerPageAction(QWebEnginePage.SavePage) class WebEnginePrinting(browsertab.AbstractPrinting): diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index 3de652887..daf46a503 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -57,6 +57,9 @@ class WebKitAction(browsertab.AbstractAction): """QtWebKit implementations related to web actions.""" + action_class = QWebPage + action_base = QWebPage.WebAction + def exit_fullscreen(self): raise browsertab.UnsupportedOperationError From 56d42b6c82145288e6d6e897889b6f3cf8d86c50 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 9 May 2017 22:16:24 +0200 Subject: [PATCH 227/825] Update requests from 2.13.0 to 2.14.1 --- misc/requirements/requirements-codecov.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-codecov.txt b/misc/requirements/requirements-codecov.txt index 6c3864d96..5fee39d9a 100644 --- a/misc/requirements/requirements-codecov.txt +++ b/misc/requirements/requirements-codecov.txt @@ -2,4 +2,4 @@ codecov==2.0.9 coverage==4.4 -requests==2.13.0 +requests==2.14.1 From a0a9c9d32e841bfef8dc26f4da96b84ac51447c0 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 9 May 2017 22:16:25 +0200 Subject: [PATCH 228/825] Update requests from 2.13.0 to 2.14.1 --- misc/requirements/requirements-pylint-master.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-pylint-master.txt b/misc/requirements/requirements-pylint-master.txt index 8c08b4e7b..22622f14c 100644 --- a/misc/requirements/requirements-pylint-master.txt +++ b/misc/requirements/requirements-pylint-master.txt @@ -8,7 +8,7 @@ lazy-object-proxy==1.3.1 mccabe==0.6.1 -e git+https://github.com/PyCQA/pylint.git#egg=pylint ./scripts/dev/pylint_checkers -requests==2.13.0 +requests==2.14.1 uritemplate==3.0.0 uritemplate.py==3.0.2 wrapt==1.10.10 From 3bd7e33c4a5ee609eb962a333068ee820a4b8b77 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Tue, 9 May 2017 22:16:27 +0200 Subject: [PATCH 229/825] Update requests from 2.13.0 to 2.14.1 --- misc/requirements/requirements-pylint.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index 647661e2f..2c6cf7729 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -7,7 +7,7 @@ lazy-object-proxy==1.3.1 mccabe==0.6.1 pylint==1.7.1 ./scripts/dev/pylint_checkers -requests==2.13.0 +requests==2.14.1 uritemplate==3.0.0 uritemplate.py==3.0.2 wrapt==1.10.10 From 1973e61424efb124f453ce0894d1c8b3e5ea99c3 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 10 May 2017 00:08:30 +0200 Subject: [PATCH 230/825] Update hypothesis from 3.8.2 to 3.8.3 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 7717e47d1..eca85541a 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -9,7 +9,7 @@ EasyProcess==0.2.3 Flask==0.12.1 glob2==0.5 httpbin==0.5.0 -hypothesis==3.8.2 +hypothesis==3.8.3 itsdangerous==0.24 # Jinja2==2.9.5 Mako==1.0.6 From 661c0f7b7c74c23db0a8ed2dfa111a711c5f8771 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 10 May 2017 00:08:34 +0200 Subject: [PATCH 231/825] Update pytest-cov from 2.4.0 to 2.5.0 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 7717e47d1..c1108e9b9 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -21,7 +21,7 @@ pytest==3.0.7 pytest-bdd==2.18.2 pytest-benchmark==3.0.0 pytest-catchlog==1.2.2 -pytest-cov==2.4.0 +pytest-cov==2.5.0 pytest-faulthandler==1.3.1 pytest-instafail==0.3.0 pytest-mock==1.6.0 From 1c50377c0a30cc56ef723125884c15de9523044b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 24 Apr 2017 21:08:00 +0200 Subject: [PATCH 232/825] Initial work on new private browsing --- qutebrowser/app.py | 2 +- qutebrowser/browser/browsertab.py | 10 +- qutebrowser/browser/commands.py | 35 ++++- qutebrowser/browser/history.py | 3 +- qutebrowser/browser/navigate.py | 10 +- qutebrowser/browser/qtnetworkdownloads.py | 2 +- qutebrowser/browser/shared.py | 4 +- qutebrowser/browser/webelem.py | 7 +- qutebrowser/browser/webengine/webenginetab.py | 6 +- qutebrowser/browser/webkit/cache.py | 143 +----------------- .../browser/webkit/network/networkmanager.py | 36 ++--- qutebrowser/browser/webkit/webkitsettings.py | 25 ++- qutebrowser/browser/webkit/webkittab.py | 13 +- qutebrowser/browser/webkit/webpage.py | 4 +- qutebrowser/browser/webkit/webview.py | 5 +- qutebrowser/config/configdata.py | 8 + qutebrowser/mainwindow/mainwindow.py | 24 ++- qutebrowser/mainwindow/statusbar/bar.py | 31 +++- qutebrowser/mainwindow/statusbar/command.py | 5 +- .../mainwindow/statusbar/percentage.py | 2 - qutebrowser/mainwindow/statusbar/progress.py | 2 - qutebrowser/mainwindow/statusbar/url.py | 2 - qutebrowser/mainwindow/tabbedbrowser.py | 13 +- qutebrowser/misc/cmdhistory.py | 9 +- qutebrowser/misc/consolewidget.py | 2 +- qutebrowser/misc/miscwidgets.py | 4 +- qutebrowser/misc/sessions.py | 3 +- tests/unit/misc/test_cmdhistory.py | 2 +- 28 files changed, 173 insertions(+), 239 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index cca9e1a76..225985e66 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -195,7 +195,7 @@ def _process_args(args): session_manager = objreg.get('session-manager') if not session_manager.did_load: log.init.debug("Initializing main window...") - window = mainwindow.MainWindow() + window = mainwindow.MainWindow(private=None) if not args.nowindow: window.show() qApp.setActiveWindow(window) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 62fb67453..d8a5a5132 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -35,11 +35,12 @@ from qutebrowser.browser import mouse, hints tab_id_gen = itertools.count(0) -def create(win_id, parent=None): +def create(win_id, private, parent=None): """Get a QtWebKit/QtWebEngine tab object. Args: win_id: The window ID where the tab will be shown. + private: Whether the tab is a private/off the record tab. parent: The Qt parent to set. """ # Importing modules here so we don't depend on QtWebEngine without the @@ -51,7 +52,8 @@ def create(win_id, parent=None): else: from qutebrowser.browser.webkit import webkittab tab_class = webkittab.WebKitTab - return tab_class(win_id=win_id, mode_manager=mode_manager, parent=parent) + return tab_class(win_id=win_id, mode_manager=mode_manager, private=private, + parent=parent) def init(): @@ -542,6 +544,7 @@ class AbstractTab(QWidget): Attributes: history: The AbstractHistory for the current tab. registry: The ObjectRegistry associated with this tab. + private: Whether private browsing is turned on for this tab. _load_status: loading status of this page Accessible via load_status() method. @@ -581,7 +584,8 @@ class AbstractTab(QWidget): fullscreen_requested = pyqtSignal(bool) renderer_process_terminated = pyqtSignal(TerminationStatus, int) - def __init__(self, win_id, mode_manager, parent=None): + def __init__(self, *, win_id, mode_manager, private, parent=None): + self.private = private self.win_id = win_id self.tab_id = next(tab_id_gen) super().__init__(parent) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 6b34e4d30..57ac0eba7 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -67,10 +67,10 @@ class CommandDispatcher: def __repr__(self): return utils.get_repr(self) - def _new_tabbed_browser(self): + def _new_tabbed_browser(self, private): """Get a tabbed-browser from a new window.""" from qutebrowser.mainwindow import mainwindow - new_window = mainwindow.MainWindow() + new_window = mainwindow.MainWindow(private=private) new_window.show() return new_window.tabbed_browser @@ -110,7 +110,7 @@ class CommandDispatcher: return widget def _open(self, url, tab=False, background=False, window=False, - explicit=True): + explicit=True, private=None): """Helper function to open a page. Args: @@ -118,12 +118,17 @@ class CommandDispatcher: tab: Whether to open in a new tab. background: Whether to open in the background. window: Whether to open in a new window + private: If opening a new window, open it in private browsing mode. + If not given, inherit the current window's mode. """ urlutils.raise_cmdexc_if_invalid(url) tabbed_browser = self._tabbed_browser cmdutils.check_exclusive((tab, background, window), 'tbw') + if private is None: + private = self._tabbed_browser.private + if window: - tabbed_browser = self._new_tabbed_browser() + tabbed_browser = self._new_tabbed_browser(private) tabbed_browser.tabopen(url) elif tab: tabbed_browser.tabopen(url, background=False, explicit=explicit) @@ -228,7 +233,8 @@ class CommandDispatcher: @cmdutils.argument('url', completion=usertypes.Completion.url) @cmdutils.argument('count', count=True) def openurl(self, url=None, implicit=False, - bg=False, tab=False, window=False, count=None, secure=False): + bg=False, tab=False, window=False, count=None, secure=False, + private=False): """Open a URL in the current/[count]th tab. If the URL contains newlines, each line gets opened in its own tab. @@ -242,12 +248,25 @@ class CommandDispatcher: clicking on a link). count: The tab index to open the URL in, or None. secure: Force HTTPS. + private: Open a new window in private browsing mode. """ if url is None: urls = [config.get('general', 'default-page')] else: urls = self._parse_url_input(url) + if private: + try: + from PyQt5.QtWebKit import qWebKitVersion + except ImportError: + pass + else: + # WORKAROUND for https://github.com/annulen/webkit/issues/54 + if qtutils.is_qtwebkit_ng(qWebKitVersion()): + message.warning("Private browsing is not fully " + "implemented by QtWebKit-NG!") + window = True + for i, cur_url in enumerate(urls): if secure: cur_url.setScheme('https') @@ -255,7 +274,8 @@ class CommandDispatcher: tab = False bg = True if tab or bg or window: - self._open(cur_url, tab, bg, window, not implicit) + self._open(cur_url, tab, bg, window, explicit=not implicit, + private=private) else: curtab = self._cntwidget(count) if curtab is None: @@ -430,7 +450,8 @@ class CommandDispatcher: # The new tab could be in a new tabbed_browser (e.g. because of # tabs-are-windows being set) if window: - new_tabbed_browser = self._new_tabbed_browser() + new_tabbed_browser = self._new_tabbed_browser( + private=self._tabbed_browser.private) else: new_tabbed_browser = self._tabbed_browser newtab = new_tabbed_browser.tabopen(background=bg, explicit=True) diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py index 294425549..db67d686b 100644 --- a/qutebrowser/browser/history.py +++ b/qutebrowser/browser/history.py @@ -274,8 +274,7 @@ class WebHistory(QObject): (hidden in completion) atime: Override the atime used to add the entry """ - if config.get('general', 'private-browsing'): - return + assert not config.get('general', 'private-browsing') if not url.isValid(): log.misc.warning("Ignoring invalid URL being added to history") return diff --git a/qutebrowser/browser/navigate.py b/qutebrowser/browser/navigate.py index b1ab6f9b1..113941edb 100644 --- a/qutebrowser/browser/navigate.py +++ b/qutebrowser/browser/navigate.py @@ -128,17 +128,19 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False, return qtutils.ensure_valid(url) + cur_tabbed_browser = objreg.get('tabbed-browser', scope='window', + window=win_id) + if window: from qutebrowser.mainwindow import mainwindow - new_window = mainwindow.MainWindow() + new_window = mainwindow.MainWindow( + private=cur_tabbed_browser.private) new_window.show() tabbed_browser = objreg.get('tabbed-browser', scope='window', window=new_window.win_id) tabbed_browser.tabopen(url, background=False) elif tab: - tabbed_browser = objreg.get('tabbed-browser', scope='window', - window=win_id) - tabbed_browser.tabopen(url, background=background) + cur_tabbed_browser.tabopen(url, background=background) else: browsertab.openurl(url) diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py index f3bc4dfe2..5fb0ed163 100644 --- a/qutebrowser/browser/qtnetworkdownloads.py +++ b/qutebrowser/browser/qtnetworkdownloads.py @@ -366,7 +366,7 @@ class DownloadManager(downloads.AbstractDownloadManager): def __init__(self, win_id, parent=None): super().__init__(parent) self._networkmanager = networkmanager.NetworkManager( - win_id, None, self) + win_id=win_id, tab_id=None, private=False, parent=self) @pyqtSlot('QUrl') def get(self, url, *, user_agent=None, **kwargs): diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py index 005d652a3..d400387a9 100644 --- a/qutebrowser/browser/shared.py +++ b/qutebrowser/browser/shared.py @@ -216,8 +216,10 @@ def get_tab(win_id, target): win_id = win_id bg_tab = True elif target == usertypes.ClickTarget.window: + tabbed_browser = objreg.get('tabbed-browser', scope='window', + window=win_id) from qutebrowser.mainwindow import mainwindow - window = mainwindow.MainWindow() + window = mainwindow.MainWindow(private=tabbed_browser.private) window.show() win_id = window.win_id bg_tab = False diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py index dce808871..7b7f1e3fe 100644 --- a/qutebrowser/browser/webelem.py +++ b/qutebrowser/browser/webelem.py @@ -379,15 +379,16 @@ class AbstractWebElement(collections.abc.MutableMapping): self._click_fake_event(click_target) return + tabbed_browser = objreg.get('tabbed-browser', scope='window', + window=self._tab.win_id) + if click_target in [usertypes.ClickTarget.tab, usertypes.ClickTarget.tab_bg]: background = click_target == usertypes.ClickTarget.tab_bg - tabbed_browser = objreg.get('tabbed-browser', scope='window', - window=self._tab.win_id) tabbed_browser.tabopen(url, background=background) elif click_target == usertypes.ClickTarget.window: from qutebrowser.mainwindow import mainwindow - window = mainwindow.MainWindow() + window = mainwindow.MainWindow(private=tabbed_browser.private) window.show() window.tabbed_browser.tabopen(url) else: diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index af93786d6..57bd49f6b 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -521,9 +521,11 @@ class WebEngineTab(browsertab.AbstractTab): """A QtWebEngine tab in the browser.""" - def __init__(self, win_id, mode_manager, parent=None): + def __init__(self, *, win_id, mode_manager, private, parent=None): + # FIXME + assert not private super().__init__(win_id=win_id, mode_manager=mode_manager, - parent=parent) + private=private, parent=parent) widget = webview.WebEngineView(tabdata=self.data, win_id=win_id) self.history = WebEngineHistory(self) self.scroller = WebEngineScroller(self, parent=self) diff --git a/qutebrowser/browser/webkit/cache.py b/qutebrowser/browser/webkit/cache.py index 8ae9aa0f2..d04a5cfac 100644 --- a/qutebrowser/browser/webkit/cache.py +++ b/qutebrowser/browser/webkit/cache.py @@ -21,8 +21,7 @@ import os.path -from PyQt5.QtCore import pyqtSlot -from PyQt5.QtNetwork import QNetworkDiskCache, QNetworkCacheMetaData +from PyQt5.QtNetwork import QNetworkDiskCache from qutebrowser.config import config from qutebrowser.utils import utils, objreg, qtutils @@ -30,24 +29,21 @@ from qutebrowser.utils import utils, objreg, qtutils class DiskCache(QNetworkDiskCache): - """Disk cache which sets correct cache dir and size. - - Attributes: - _activated: Whether the cache should be used. - _cache_dir: The base directory for cache files (standarddir.cache()) - """ + """Disk cache which sets correct cache dir and size.""" def __init__(self, cache_dir, parent=None): super().__init__(parent) - self._cache_dir = cache_dir - self._maybe_activate() - objreg.get('config').changed.connect(self.on_config_changed) + assert not config.get('general', 'private-browsing') + self.setCacheDirectory(os.path.join(cache_dir, 'http')) + self._set_cache_size() + objreg.get('config').changed.connect(self._set_cache_size) def __repr__(self): return utils.get_repr(self, size=self.cacheSize(), maxsize=self.maximumCacheSize(), path=self.cacheDirectory()) + @config.change_filter('storage', 'cache-size') def _set_cache_size(self): """Set the cache size based on the config.""" size = config.get('storage', 'cache-size') @@ -58,128 +54,3 @@ class DiskCache(QNetworkDiskCache): not qtutils.version_check('5.9')): # pragma: no cover size = 0 self.setMaximumCacheSize(size) - - def _maybe_activate(self): - """Activate/deactivate the cache based on the config.""" - if config.get('general', 'private-browsing'): - self._activated = False - else: - self._activated = True - self.setCacheDirectory(os.path.join(self._cache_dir, 'http')) - self._set_cache_size() - - @pyqtSlot(str, str) - def on_config_changed(self, section, option): - """Update cache size/activated if the config was changed.""" - if (section, option) == ('storage', 'cache-size'): - self._set_cache_size() - elif (section, option) == ('general', # pragma: no branch - 'private-browsing'): - self._maybe_activate() - - def cacheSize(self): - """Return the current size taken up by the cache. - - Return: - An int. - """ - if self._activated: - return super().cacheSize() - else: - return 0 - - def fileMetaData(self, filename): - """Return the QNetworkCacheMetaData for the cache file filename. - - Args: - filename: The file name as a string. - - Return: - A QNetworkCacheMetaData object. - """ - if self._activated: - return super().fileMetaData(filename) - else: - return QNetworkCacheMetaData() - - def data(self, url): - """Return the data associated with url. - - Args: - url: A QUrl. - - return: - A QIODevice or None. - """ - if self._activated: - return super().data(url) - else: - return None - - def insert(self, device): - """Insert the data in device and the prepared meta data into the cache. - - Args: - device: A QIODevice. - """ - if self._activated: - super().insert(device) - else: - return None - - def metaData(self, url): - """Return the meta data for the url url. - - Args: - url: A QUrl. - - Return: - A QNetworkCacheMetaData object. - """ - if self._activated: - return super().metaData(url) - else: - return QNetworkCacheMetaData() - - def prepare(self, meta_data): - """Return the device that should be populated with the data. - - Args: - meta_data: A QNetworkCacheMetaData object. - - Return: - A QIODevice or None. - """ - if self._activated: - return super().prepare(meta_data) - else: - return None - - def remove(self, url): - """Remove the cache entry for url. - - Return: - True on success, False otherwise. - """ - if self._activated: - return super().remove(url) - else: - return False - - def updateMetaData(self, meta_data): - """Update the cache meta date for the meta_data's url to meta_data. - - Args: - meta_data: A QNetworkCacheMetaData object. - """ - if self._activated: - super().updateMetaData(meta_data) - else: - return - - def clear(self): - """Remove all items from the cache.""" - if self._activated: - super().clear() - else: - return diff --git a/qutebrowser/browser/webkit/network/networkmanager.py b/qutebrowser/browser/webkit/network/networkmanager.py index 30adf3170..be14e2062 100644 --- a/qutebrowser/browser/webkit/network/networkmanager.py +++ b/qutebrowser/browser/webkit/network/networkmanager.py @@ -128,6 +128,7 @@ class NetworkManager(QNetworkAccessManager): _tab_id: The tab ID this NetworkManager is associated with. _rejected_ssl_errors: A {QUrl: [SslError]} dict of rejected errors. _accepted_ssl_errors: A {QUrl: [SslError]} dict of accepted errors. + _private: Whether we're in private browsing mode. Signals: shutting_down: Emitted when the QNAM is shutting down. @@ -135,7 +136,7 @@ class NetworkManager(QNetworkAccessManager): shutting_down = pyqtSignal() - def __init__(self, win_id, tab_id, parent=None): + def __init__(self, *, win_id, tab_id, private, parent=None): log.init.debug("Initializing NetworkManager") with log.disable_qt_msghandler(): # WORKAROUND for a hang when a message is printed - See: @@ -146,11 +147,12 @@ class NetworkManager(QNetworkAccessManager): self._win_id = win_id self._tab_id = tab_id self._requests = [] + self._private = private self._scheme_handlers = { 'qute': webkitqutescheme.QuteSchemeHandler(win_id), 'file': filescheme.FileSchemeHandler(win_id), } - self._set_cookiejar(private=config.get('general', 'private-browsing')) + self._set_cookiejar() self._set_cache() self.sslErrors.connect(self.on_ssl_errors) self._rejected_ssl_errors = collections.defaultdict(list) @@ -158,15 +160,10 @@ class NetworkManager(QNetworkAccessManager): self.authenticationRequired.connect(self.on_authentication_required) self.proxyAuthenticationRequired.connect( self.on_proxy_authentication_required) - objreg.get('config').changed.connect(self.on_config_changed) - def _set_cookiejar(self, private=False): - """Set the cookie jar of the NetworkManager correctly. - - Args: - private: Whether we're currently in private browsing mode. - """ - if private: + def _set_cookiejar(self): + """Set the cookie jar of the NetworkManager correctly.""" + if self._private: cookie_jar = objreg.get('ram-cookie-jar') else: cookie_jar = objreg.get('cookie-jar') @@ -178,11 +175,9 @@ class NetworkManager(QNetworkAccessManager): cookie_jar.setParent(app) def _set_cache(self): - """Set the cache of the NetworkManager correctly. - - We can't switch the whole cache in private mode because QNAM would - delete the old cache. - """ + """Set the cache of the NetworkManager correctly.""" + if self._private: + return # We have a shared cache - we restore its parent so we don't take # ownership of it. app = QCoreApplication.instance() @@ -324,17 +319,6 @@ class NetworkManager(QNetworkAccessManager): authenticator.setPassword(answer.password) _proxy_auth_cache[proxy_id] = answer - @config.change_filter('general', 'private-browsing') - def on_config_changed(self): - """Set cookie jar when entering/leaving private browsing mode.""" - private_browsing = config.get('general', 'private-browsing') - if private_browsing: - # switched from normal mode to private mode - self._set_cookiejar(private=True) - else: - # switched from private mode to normal mode - self._set_cookiejar() - @pyqtSlot() def on_adopted_download_destroyed(self): """Check if we can clean up if an adopted download was destroyed. diff --git a/qutebrowser/browser/webkit/webkitsettings.py b/qutebrowser/browser/webkit/webkitsettings.py index 0d4564e7e..d7cc91a4d 100644 --- a/qutebrowser/browser/webkit/webkitsettings.py +++ b/qutebrowser/browser/webkit/webkitsettings.py @@ -89,20 +89,19 @@ def _set_user_stylesheet(): def _init_private_browsing(): - if config.get('general', 'private-browsing'): - if qtutils.is_qtwebkit_ng(): - message.warning("Private browsing is not fully implemented by " - "QtWebKit-NG!") + if qtutils.is_qtwebkit_ng(): + # WORKAROUND for https://github.com/annulen/webkit/issues/54 + message.warning("Private browsing is not fully implemented by QtWebKit-NG!") + elif not qtutils.version_check('5.4.2'): + # WORKAROUND for https://codereview.qt-project.org/#/c/108936/ + # Won't work when private browsing is not enabled globally, but that's + # the best we can do... QWebSettings.setIconDatabasePath('') - else: - QWebSettings.setIconDatabasePath(standarddir.cache()) def update_settings(section, option): """Update global settings when qwebsettings changed.""" - if (section, option) == ('general', 'private-browsing'): - _init_private_browsing() - elif section == 'ui' and option in ['hide-scrollbar', 'user-stylesheet']: + if section == 'ui' and option in ['hide-scrollbar', 'user-stylesheet']: _set_user_stylesheet() websettings.update_mappings(MAPPINGS, section, option) @@ -113,8 +112,7 @@ def init(_args): cache_path = standarddir.cache() data_path = standarddir.data() - _init_private_browsing() - + QWebSettings.setIconDatabasePath(standarddir.cache()) QWebSettings.setOfflineWebApplicationCachePath( os.path.join(cache_path, 'application-cache')) QWebSettings.globalSettings().setLocalStoragePath( @@ -122,6 +120,9 @@ def init(_args): QWebSettings.setOfflineStoragePath( os.path.join(data_path, 'offline-storage')) + if config.get('general', 'private-browsing'): + _init_private_browsing() + websettings.init_mappings(MAPPINGS) _set_user_stylesheet() objreg.get('config').changed.connect(update_settings) @@ -254,8 +255,6 @@ MAPPINGS = { setter=QWebSettings.setOfflineWebApplicationCacheQuota), }, 'general': { - 'private-browsing': - Attribute(QWebSettings.PrivateBrowsingEnabled), 'developer-extras': Attribute(QWebSettings.DeveloperExtrasEnabled), 'print-element-backgrounds': diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py index daf46a503..22442817e 100644 --- a/qutebrowser/browser/webkit/webkittab.py +++ b/qutebrowser/browser/webkit/webkittab.py @@ -629,10 +629,13 @@ class WebKitTab(browsertab.AbstractTab): """A QtWebKit tab in the browser.""" - def __init__(self, win_id, mode_manager, parent=None): + def __init__(self, *, win_id, mode_manager, private, parent=None): super().__init__(win_id=win_id, mode_manager=mode_manager, - parent=parent) - widget = webview.WebView(win_id, self.tab_id, tab=self) + private=private, parent=parent) + widget = webview.WebView(win_id=win_id, tab_id=self.tab_id, + private=private, tab=self) + if private: + self._make_private(widget) self.history = WebKitHistory(self) self.scroller = WebKitScroller(self, parent=self) self.caret = WebKitCaret(win_id=win_id, mode_manager=mode_manager, @@ -649,6 +652,10 @@ class WebKitTab(browsertab.AbstractTab): def _install_event_filter(self): self._widget.installEventFilter(self._mouse_event_filter) + def _make_private(self, widget): + settings = widget.settings() + settings.setAttribute(QWebSettings.PrivateBrowsingEnabled, True) + def openurl(self, url): self._openurl_prepare(url) self._widget.openurl(url) diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index a687dd5e2..acebf5cd3 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -59,7 +59,7 @@ class BrowserPage(QWebPage): shutting_down = pyqtSignal() reloading = pyqtSignal(QUrl) - def __init__(self, win_id, tab_id, tabdata, parent=None): + def __init__(self, win_id, tab_id, tabdata, private, parent=None): super().__init__(parent) self._win_id = win_id self._tabdata = tabdata @@ -72,7 +72,7 @@ class BrowserPage(QWebPage): self.error_occurred = False self.open_target = usertypes.ClickTarget.normal self._networkmanager = networkmanager.NetworkManager( - win_id, tab_id, self) + win_id=win_id, tab_id=tab_id, private=private, parent=self) self.setNetworkAccessManager(self._networkmanager) self.setForwardUnsupportedContent(True) self.reloading.connect(self._networkmanager.clear_rejected_ssl_errors) diff --git a/qutebrowser/browser/webkit/webview.py b/qutebrowser/browser/webkit/webview.py index 01abca639..99980b4b1 100644 --- a/qutebrowser/browser/webkit/webview.py +++ b/qutebrowser/browser/webkit/webview.py @@ -55,7 +55,7 @@ class WebView(QWebView): scroll_pos_changed = pyqtSignal(int, int) shutting_down = pyqtSignal() - def __init__(self, win_id, tab_id, tab, parent=None): + def __init__(self, *, win_id, tab_id, tab, private, parent=None): super().__init__(parent) if sys.platform == 'darwin' and qtutils.version_check('5.4'): # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-42948 @@ -71,7 +71,8 @@ class WebView(QWebView): self._set_bg_color() self._tab_id = tab_id - page = webpage.BrowserPage(self.win_id, self._tab_id, tab.data, + page = webpage.BrowserPage(win_id=self.win_id, tab_id=self._tab_id, + tabdata=tab.data, private=private, parent=self) try: diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 6c3c61187..11f46094f 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1125,6 +1125,14 @@ def data(readonly=False): SettingValue(typ.QssColor(), 'black'), "Background color of the statusbar."), + ('statusbar.fg.private', + SettingValue(typ.QssColor(), '${statusbar.fg}'), + "Foreground color of the statusbar in private browsing mode."), + + ('statusbar.bg.private', + SettingValue(typ.QssColor(), 'grey'), # FIXME darker color + "Background color of the statusbar in private browsing mode."), + ('statusbar.fg.insert', SettingValue(typ.QssColor(), '${statusbar.fg}'), "Foreground color of the statusbar in insert mode."), diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 96489fbac..1f573c6d1 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -81,7 +81,7 @@ def get_window(via_ipc, force_window=False, force_tab=False, # Otherwise, or if no window was found, create a new one if window is None: - window = MainWindow() + window = MainWindow(private=None) window.show() raise_window = True @@ -127,13 +127,15 @@ class MainWindow(QWidget): _vbox: The main QVBoxLayout. _commandrunner: The main CommandRunner instance. _overlays: Widgets shown as overlay for the current webpage. + _private: Whether the window is in private browsing mode. """ - def __init__(self, geometry=None, parent=None): + def __init__(self, *, private, geometry=None, parent=None): """Create a new main window. Args: geometry: The geometry to load, as a bytes-object (or None). + private: Whether the window is in private browsing mode. parent: The parent the window should get. """ super().__init__(parent) @@ -161,7 +163,14 @@ class MainWindow(QWidget): self._init_downloadmanager() self._downloadview = downloadview.DownloadView(self.win_id) - self.tabbed_browser = tabbedbrowser.TabbedBrowser(self.win_id) + if config.get('general', 'private-browsing'): + # This setting always trumps what's passed in. + private = True + else: + private = bool(private) + self._private = private + self.tabbed_browser = tabbedbrowser.TabbedBrowser(win_id=self.win_id, + private=private) objreg.register('tabbed-browser', self.tabbed_browser, scope='window', window=self.win_id) self._init_command_dispatcher() @@ -169,7 +178,8 @@ class MainWindow(QWidget): # We need to set an explicit parent for StatusBar because it does some # show/hide magic immediately which would mean it'd show up as a # window. - self.status = bar.StatusBar(self.win_id, parent=self) + self.status = bar.StatusBar(win_id=self.win_id, private=private, + parent=self) self._add_widgets() self._downloadview.show() @@ -446,17 +456,15 @@ class MainWindow(QWidget): message_bridge.s_maybe_reset_text.connect(status.txt.maybe_reset_text) # statusbar - tabs.current_tab_changed.connect(status.prog.on_tab_changed) + tabs.current_tab_changed.connect(status.on_tab_changed) + tabs.cur_progress.connect(status.prog.setValue) tabs.cur_load_finished.connect(status.prog.hide) tabs.cur_load_started.connect(status.prog.on_load_started) - tabs.current_tab_changed.connect(status.percentage.on_tab_changed) tabs.cur_scroll_perc_changed.connect(status.percentage.set_perc) - tabs.tab_index_changed.connect(status.tabindex.on_tab_index_changed) - tabs.current_tab_changed.connect(status.url.on_tab_changed) tabs.cur_url_changed.connect(status.url.set_url) tabs.cur_link_hovered.connect(status.url.set_hover_url) tabs.cur_load_status_changed.connect(status.url.on_load_status_changed) diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index cddf4ec35..7a302f276 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -22,6 +22,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, pyqtProperty, Qt, QSize, QTimer from PyQt5.QtWidgets import QWidget, QHBoxLayout, QStackedLayout, QSizePolicy +from qutebrowser.browser import browsertab from qutebrowser.config import config, style from qutebrowser.utils import usertypes, log, objreg, utils from qutebrowser.mainwindow.statusbar import (command, progress, keystring, @@ -70,6 +71,11 @@ class StatusBar(QWidget): For some reason we need to have this as class attribute so pyqtProperty works correctly. + _private: Whether we're in private browsing 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. @@ -86,6 +92,7 @@ class StatusBar(QWidget): _insert_active = False _command_active = False _caret_mode = CaretMode.off + _private = False STYLESHEET = """ @@ -97,6 +104,13 @@ class StatusBar(QWidget): color: {{ color['statusbar.fg'] }}; } + QWidget#StatusBar[private="true"], + QWidget#StatusBar[private="true"] QLabel, + QWidget#StatusBar[private="true"] QLineEdit { + color: {{ color['statusbar.fg.private'] }}; + background-color: {{ color['statusbar.bg.private'] }}; + } + QWidget#StatusBar[caret_mode="on"], QWidget#StatusBar[caret_mode="on"] QLabel, QWidget#StatusBar[caret_mode="on"] QLineEdit { @@ -134,7 +148,7 @@ class StatusBar(QWidget): """ - def __init__(self, win_id, parent=None): + def __init__(self, *, win_id, private, parent=None): super().__init__(parent) objreg.register('statusbar', self, scope='window', window=win_id) self.setObjectName(self.__class__.__name__) @@ -146,6 +160,7 @@ class StatusBar(QWidget): self._win_id = win_id self._option = None self._page_fullscreen = False + self._private = private self._hbox = QHBoxLayout(self) self.set_hbox_padding() @@ -156,7 +171,7 @@ class StatusBar(QWidget): self._hbox.addLayout(self._stack) self._stack.setContentsMargins(0, 0, 0, 0) - self.cmd = command.Command(win_id) + self.cmd = command.Command(private=private, win_id=win_id) self._stack.addWidget(self.cmd) objreg.register('status-command', self.cmd, scope='window', window=win_id) @@ -226,6 +241,11 @@ class StatusBar(QWidget): """Getter for self._caret_mode, so it can be used as Qt property.""" return self._caret_mode.name + @pyqtProperty(bool) + def private(self): + """Getter for self.private so it can be used as Qt property.""" + return self._private + def set_mode_active(self, mode, val): """Setter for self.{insert,command,caret}_active. @@ -314,6 +334,13 @@ class StatusBar(QWidget): self._page_fullscreen = on self.maybe_hide() + @pyqtSlot(browsertab.AbstractTab) + def on_tab_changed(self, tab): + self.url.on_tab_changed(tab) + self.prog.on_tab_changed(tab) + self.percentage.on_tab_changed(tab) + assert tab.private == self._private + def resizeEvent(self, e): """Extend resizeEvent of QWidget to emit a resized signal afterwards. diff --git a/qutebrowser/mainwindow/statusbar/command.py b/qutebrowser/mainwindow/statusbar/command.py index d59c87dbc..410e578c9 100644 --- a/qutebrowser/mainwindow/statusbar/command.py +++ b/qutebrowser/mainwindow/statusbar/command.py @@ -54,12 +54,11 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit): show_cmd = pyqtSignal() hide_cmd = pyqtSignal() - def __init__(self, win_id, parent=None): - misc.CommandLineEdit.__init__(self, parent) + def __init__(self, *, win_id, private, parent=None): + misc.CommandLineEdit.__init__(self, private=private, parent=parent) misc.MinimalLineEditMixin.__init__(self) self._win_id = win_id command_history = objreg.get('command-history') - self.history.handle_private_mode = True self.history.history = command_history.data self.history.changed.connect(command_history.changed) self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Ignored) diff --git a/qutebrowser/mainwindow/statusbar/percentage.py b/qutebrowser/mainwindow/statusbar/percentage.py index 050b0747a..0d75e8163 100644 --- a/qutebrowser/mainwindow/statusbar/percentage.py +++ b/qutebrowser/mainwindow/statusbar/percentage.py @@ -21,7 +21,6 @@ from PyQt5.QtCore import pyqtSlot -from qutebrowser.browser import browsertab from qutebrowser.mainwindow.statusbar import textbase @@ -51,7 +50,6 @@ class Percentage(textbase.TextBase): else: self.setText('[{:2}%]'.format(y)) - @pyqtSlot(browsertab.AbstractTab) def on_tab_changed(self, tab): """Update scroll position when tab changed.""" self.set_perc(*tab.scroller.pos_perc()) diff --git a/qutebrowser/mainwindow/statusbar/progress.py b/qutebrowser/mainwindow/statusbar/progress.py index e78d5307c..a2f192732 100644 --- a/qutebrowser/mainwindow/statusbar/progress.py +++ b/qutebrowser/mainwindow/statusbar/progress.py @@ -22,7 +22,6 @@ from PyQt5.QtCore import pyqtSlot, QSize from PyQt5.QtWidgets import QProgressBar, QSizePolicy -from qutebrowser.browser import browsertab from qutebrowser.config import style from qutebrowser.utils import utils, usertypes @@ -60,7 +59,6 @@ class Progress(QProgressBar): self.setValue(0) self.show() - @pyqtSlot(browsertab.AbstractTab) def on_tab_changed(self, tab): """Set the correct value when the current tab changed.""" if self is None: # pragma: no branch diff --git a/qutebrowser/mainwindow/statusbar/url.py b/qutebrowser/mainwindow/statusbar/url.py index b54e020fa..f83fdef1f 100644 --- a/qutebrowser/mainwindow/statusbar/url.py +++ b/qutebrowser/mainwindow/statusbar/url.py @@ -21,7 +21,6 @@ from PyQt5.QtCore import pyqtSlot, pyqtProperty, Qt, QUrl -from qutebrowser.browser import browsertab from qutebrowser.mainwindow.statusbar import textbase from qutebrowser.config import style from qutebrowser.utils import usertypes, urlutils @@ -165,7 +164,6 @@ class UrlText(textbase.TextBase): self._hover_url = None self._update_url() - @pyqtSlot(browsertab.AbstractTab) def on_tab_changed(self, tab): """Update URL if the tab changed.""" self._hover_url = None diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 896ea5c9d..8e3ee5e39 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -68,6 +68,7 @@ class TabbedBrowser(tabwidget.TabWidget): _local_marks: Jump markers local to each page _global_marks: Jump markers used across all pages default_window_icon: The qutebrowser window icon + private: Whether private browsing is on for this window. Signals: cur_progress: Progress of the current tab changed (load_progress). @@ -100,7 +101,7 @@ class TabbedBrowser(tabwidget.TabWidget): new_tab = pyqtSignal(browsertab.AbstractTab, int) page_fullscreen_requested = pyqtSignal(bool) - def __init__(self, win_id, parent=None): + def __init__(self, *, win_id, private, parent=None): super().__init__(win_id, parent) self._win_id = win_id self._tab_insert_idx_left = 0 @@ -118,6 +119,7 @@ class TabbedBrowser(tabwidget.TabWidget): self._local_marks = {} self._global_marks = {} self.default_window_icon = self.window().windowIcon() + self.private = private objreg.get('config').changed.connect(self.update_favicons) objreg.get('config').changed.connect(self.update_window_title) objreg.get('config').changed.connect(self.update_tab_titles) @@ -205,7 +207,9 @@ class TabbedBrowser(tabwidget.TabWidget): tab.renderer_process_terminated.connect( functools.partial(self._on_renderer_process_terminated, tab)) tab.new_tab_requested.connect(self.tabopen) - tab.add_history_item.connect(objreg.get('web-history').add_from_tab) + if not self.private: + web_history = objreg.get('web-history') + tab.add_history_item.connect(web_history.add_from_tab) tab.fullscreen_requested.connect(self.page_fullscreen_requested) tab.fullscreen_requested.connect( self.tabBar().on_page_fullscreen_requested) @@ -399,13 +403,14 @@ class TabbedBrowser(tabwidget.TabWidget): if (config.get('tabs', 'tabs-are-windows') and self.count() > 0 and not ignore_tabs_are_windows): from qutebrowser.mainwindow import mainwindow - window = mainwindow.MainWindow() + window = mainwindow.MainWindow(private=self.private) window.show() tabbed_browser = objreg.get('tabbed-browser', scope='window', window=window.win_id) return tabbed_browser.tabopen(url, background, explicit) - tab = browsertab.create(win_id=self._win_id, parent=self) + tab = browsertab.create(win_id=self._win_id, private=self.private, + parent=self) self._connect_tab_signals(tab) if idx is None: diff --git a/qutebrowser/misc/cmdhistory.py b/qutebrowser/misc/cmdhistory.py index b0990a67e..9193f7bcc 100644 --- a/qutebrowser/misc/cmdhistory.py +++ b/qutebrowser/misc/cmdhistory.py @@ -44,9 +44,9 @@ class History(QObject): """Command history. Attributes: - handle_private_mode: Whether to ignore history in private mode. history: A list of executed commands, with newer commands at the end. _tmphist: Temporary history for history browsing (as NeighborList) + _private: Whether this history is in private browsing mode. Signals: changed: Emitted when an entry was added to the history. @@ -54,15 +54,15 @@ class History(QObject): changed = pyqtSignal() - def __init__(self, history=None, parent=None): + def __init__(self, *, private=False, history=None, parent=None): """Constructor. Args: history: The initial history to set. """ super().__init__(parent) - self.handle_private_mode = False self._tmphist = None + self._private = private if history is None: self.history = [] else: @@ -129,8 +129,7 @@ class History(QObject): Args: text: The text to append. """ - if (self.handle_private_mode and - config.get('general', 'private-browsing')): + if self._private: return if not self.history or text != self.history[-1]: self.history.append(text) diff --git a/qutebrowser/misc/consolewidget.py b/qutebrowser/misc/consolewidget.py index 537621b4d..963076c21 100644 --- a/qutebrowser/misc/consolewidget.py +++ b/qutebrowser/misc/consolewidget.py @@ -50,7 +50,7 @@ class ConsoleLineEdit(miscwidgets.CommandLineEdit): Args: _namespace: The local namespace of the interpreter. """ - super().__init__(parent) + super().__init__(parent=parent) self.update_font() objreg.get('config').changed.connect(self.update_font) self._history = cmdhistory.History(parent=self) diff --git a/qutebrowser/misc/miscwidgets.py b/qutebrowser/misc/miscwidgets.py index 48f775b85..75f941fd8 100644 --- a/qutebrowser/misc/miscwidgets.py +++ b/qutebrowser/misc/miscwidgets.py @@ -69,9 +69,9 @@ class CommandLineEdit(QLineEdit): _promptlen: The length of the current prompt. """ - def __init__(self, parent=None): + def __init__(self, *, private=False, parent=None): super().__init__(parent) - self.history = cmdhistory.History(parent=self) + self.history = cmdhistory.History(private=private, parent=self) self._validator = _CommandValidator(self) self.setValidator(self._validator) self.textEdited.connect(self.on_text_edited) diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index cba3629ef..6a096c06a 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -379,7 +379,8 @@ class SessionManager(QObject): raise SessionError(e) log.sessions.debug("Loading session {} from {}...".format(name, path)) for win in data['windows']: - window = mainwindow.MainWindow(geometry=win['geometry']) + window = mainwindow.MainWindow(geometry=win['geometry'], + private=win.get('private', False)) window.show() tabbed_browser = objreg.get('tabbed-browser', scope='window', window=window.win_id) diff --git a/tests/unit/misc/test_cmdhistory.py b/tests/unit/misc/test_cmdhistory.py index 7da721c2b..717fd4a96 100644 --- a/tests/unit/misc/test_cmdhistory.py +++ b/tests/unit/misc/test_cmdhistory.py @@ -146,7 +146,7 @@ def test_previtem_index_error(hist): def test_append_private_mode(hist, config_stub): """Test append in private mode.""" - hist.handle_private_mode = True + hist._private = True # We want general.private-browsing set to True config_stub.data = CONFIG_PRIVATE hist.append('new item') From f4d3f97cb75b657af5e0b10f380d948cd60656b0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 24 Apr 2017 22:24:04 +0200 Subject: [PATCH 233/825] Implement private browsing for QtWebEngine --- .../browser/webengine/webenginesettings.py | 46 ++++++++++++------- qutebrowser/browser/webengine/webenginetab.py | 18 ++++---- qutebrowser/browser/webengine/webview.py | 16 +++++-- qutebrowser/config/configdata.py | 3 +- 4 files changed, 51 insertions(+), 32 deletions(-) diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index e1f4a22c9..2eb31d005 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -38,6 +38,12 @@ from qutebrowser.utils import (objreg, utils, standarddir, javascript, log, qtutils) +# The default QWebEngineProfile +default_profile = None +# The QWebEngineProfile used for private (off-the-record) windows +private_profile = None + + class Attribute(websettings.Attribute): """A setting set via QWebEngineSettings::setAttribute.""" @@ -67,7 +73,7 @@ class StaticSetter(websettings.StaticSetter): GLOBAL_SETTINGS = QWebEngineSettings.globalSettings -class ProfileSetter(websettings.Base): +class DefaultProfileSetter(websettings.Base): """A setting set on the QWebEngineProfile.""" @@ -78,16 +84,16 @@ class ProfileSetter(websettings.Base): def get(self, settings=None): utils.unused(settings) - getter = getattr(QWebEngineProfile.defaultProfile(), self._getter) + getter = getattr(default_profile, self._getter) return getter() def _set(self, value, settings=None): utils.unused(settings) - setter = getattr(QWebEngineProfile.defaultProfile(), self._setter) + setter = getattr(default_profile, self._setter) setter(value) -class PersistentCookiePolicy(ProfileSetter): +class PersistentCookiePolicy(DefaultProfileSetter): """The cookies -> store setting is different from other settings.""" @@ -141,19 +147,27 @@ def _init_stylesheet(profile): profile.scripts().insert(script) -def _init_profile(profile): - """Initialize settings set on the QWebEngineProfile.""" - profile.setCachePath(os.path.join(standarddir.cache(), 'webengine')) - profile.setPersistentStoragePath( - os.path.join(standarddir.data(), 'webengine')) - - def update_settings(section, option): """Update global settings when qwebsettings changed.""" websettings.update_mappings(MAPPINGS, section, option) - profile = QWebEngineProfile.defaultProfile() if section == 'ui' and option in ['hide-scrollbar', 'user-stylesheet']: - _init_stylesheet(profile) + _init_stylesheet(default_profile) + _init_stylesheet(private_profile) + + +def _init_profiles(): + """Init the two used QWebEngineProfiles""" + global default_profile, private_profile + default_profile = QWebEngineProfile.defaultProfile() + default_profile.setCachePath( + os.path.join(standarddir.cache(), 'webengine')) + default_profile.setPersistentStoragePath( + os.path.join(standarddir.data(), 'webengine')) + _init_stylesheet(default_profile) + + private_profile = QWebEngineProfile() + assert private_profile.isOffTheRecord() + _init_stylesheet(private_profile) def init(args): @@ -173,9 +187,7 @@ def init(args): else: log.misc.debug("Imported PyOpenGL as workaround") - profile = QWebEngineProfile.defaultProfile() - _init_profile(profile) - _init_stylesheet(profile) + _init_profiles() # We need to do this here as a WORKAROUND for # https://bugreports.qt.io/browse/QTBUG-58650 if not qtutils.version_check('5.9'): @@ -282,7 +294,7 @@ MAPPINGS = { 'local-storage': Attribute(QWebEngineSettings.LocalStorageEnabled), 'cache-size': - ProfileSetter(getter='httpCacheMaximumSize', + DefaultProfileSetter(getter='httpCacheMaximumSize', setter='setHttpCacheMaximumSize') }, 'general': { diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 57bd49f6b..7c17fc5b8 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -34,7 +34,8 @@ from PyQt5.QtWebEngineWidgets import (QWebEnginePage, QWebEngineScript, from qutebrowser.browser import browsertab, mouse, shared from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory, interceptor, webenginequtescheme, - webenginedownloads) + webenginedownloads, + webenginesettings) from qutebrowser.misc import miscwidgets from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils, objreg, jinja, debug) @@ -50,21 +51,23 @@ def init(): # https://www.riverbankcomputing.com/pipermail/pyqt/2016-September/038075.html global _qute_scheme_handler app = QApplication.instance() - profile = QWebEngineProfile.defaultProfile() log.init.debug("Initializing qute://* handler...") _qute_scheme_handler = webenginequtescheme.QuteSchemeHandler(parent=app) - _qute_scheme_handler.install(profile) + _qute_scheme_handler.install(webenginesettings.default_profile) + _qute_scheme_handler.install(webenginesettings.private_profile) log.init.debug("Initializing request interceptor...") host_blocker = objreg.get('host-blocker') req_interceptor = interceptor.RequestInterceptor( host_blocker, parent=app) - req_interceptor.install(profile) + req_interceptor.install(webenginesettings.default_profile) + req_interceptor.install(webenginesettings.private_profile) log.init.debug("Initializing QtWebEngine downloads...") download_manager = webenginedownloads.DownloadManager(parent=app) - download_manager.install(profile) + download_manager.install(webenginesettings.default_profile) + download_manager.install(webenginesettings.private_profile) objreg.register('webengine-download-manager', download_manager) @@ -522,11 +525,10 @@ class WebEngineTab(browsertab.AbstractTab): """A QtWebEngine tab in the browser.""" def __init__(self, *, win_id, mode_manager, private, parent=None): - # FIXME - assert not private super().__init__(win_id=win_id, mode_manager=mode_manager, private=private, parent=parent) - widget = webview.WebEngineView(tabdata=self.data, win_id=win_id) + widget = webview.WebEngineView(tabdata=self.data, win_id=win_id, + private=private) self.history = WebEngineHistory(self) self.scroller = WebEngineScroller(self, parent=self) self.caret = WebEngineCaret(win_id=win_id, mode_manager=mode_manager, diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py index ee6e099bf..0cb9bb612 100644 --- a/qutebrowser/browser/webengine/webview.py +++ b/qutebrowser/browser/webengine/webview.py @@ -28,7 +28,7 @@ from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage # pylint: enable=no-name-in-module,import-error,useless-suppression from qutebrowser.browser import shared -from qutebrowser.browser.webengine import certificateerror +from qutebrowser.browser.webengine import certificateerror, webenginesettings from qutebrowser.config import config from qutebrowser.utils import (log, debug, usertypes, jinja, urlutils, message, objreg) @@ -38,13 +38,19 @@ class WebEngineView(QWebEngineView): """Custom QWebEngineView subclass with qutebrowser-specific features.""" - def __init__(self, tabdata, win_id, parent=None): + def __init__(self, *, tabdata, win_id, private, parent=None): super().__init__(parent) self._win_id = win_id self._tabdata = tabdata theme_color = self.style().standardPalette().color(QPalette.Base) - page = WebEnginePage(theme_color=theme_color, parent=self) + if private: + profile = webenginesettings.private_profile + assert profile.isOffTheRecord() + else: + profile = webenginesettings.default_profile + page = WebEnginePage(theme_color=theme_color, profile=profile, + parent=self) self.setPage(page) def shutdown(self): @@ -124,8 +130,8 @@ class WebEnginePage(QWebEnginePage): certificate_error = pyqtSignal() shutting_down = pyqtSignal() - def __init__(self, theme_color, parent=None): - super().__init__(parent) + def __init__(self, *, theme_color, profile, parent=None): + super().__init__(profile, parent) self._is_shutting_down = False self.featurePermissionRequested.connect( self._on_feature_permission_requested) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 11f46094f..0ac20a1e6 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -185,8 +185,7 @@ def data(readonly=False): "Encoding to use for editor."), ('private-browsing', - SettingValue(typ.Bool(), 'false', - backends=[usertypes.Backend.QtWebKit]), + SettingValue(typ.Bool(), 'false'), "Do not record visited pages in the history or store web page " "icons."), From c6e31391dee7ae7c5490719b66983d5ddbbd7402 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 24 Apr 2017 23:09:03 +0200 Subject: [PATCH 234/825] Fix most tests/lint --- qutebrowser/browser/commands.py | 16 +- qutebrowser/browser/history.py | 2 - .../browser/webengine/webenginesettings.py | 2 +- qutebrowser/browser/webengine/webenginetab.py | 3 +- qutebrowser/browser/webkit/webkitsettings.py | 3 +- qutebrowser/mainwindow/mainwindow.py | 21 ++- qutebrowser/mainwindow/statusbar/bar.py | 1 + qutebrowser/misc/cmdhistory.py | 1 - tests/helpers/stubs.py | 2 +- .../webkit/network/test_networkmanager.py | 19 +-- tests/unit/browser/webkit/test_cache.py | 159 +----------------- tests/unit/browser/webkit/test_history.py | 41 +---- tests/unit/misc/test_miscwidgets.py | 2 +- 13 files changed, 41 insertions(+), 231 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 57ac0eba7..99954ac78 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -40,7 +40,7 @@ from qutebrowser.keyinput import modeman from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils, objreg, utils, typing) from qutebrowser.utils.usertypes import KeyMode -from qutebrowser.misc import editor, guiprocess +from qutebrowser.misc import editor, guiprocess, objects from qutebrowser.completion.models import instances, sortfilter @@ -256,15 +256,11 @@ class CommandDispatcher: urls = self._parse_url_input(url) if private: - try: - from PyQt5.QtWebKit import qWebKitVersion - except ImportError: - pass - else: - # WORKAROUND for https://github.com/annulen/webkit/issues/54 - if qtutils.is_qtwebkit_ng(qWebKitVersion()): - message.warning("Private browsing is not fully " - "implemented by QtWebKit-NG!") + # WORKAROUND for https://github.com/annulen/webkit/issues/54 + if (objects.backend == usertypes.Backend.QtWebKit and + qtutils.is_qtwebkit_ng()): + message.warning("Private browsing is not fully " + "implemented by QtWebKit-NG!") window = True for i, cur_url in enumerate(urls): diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py index db67d686b..7cade0bad 100644 --- a/qutebrowser/browser/history.py +++ b/qutebrowser/browser/history.py @@ -27,7 +27,6 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject from qutebrowser.commands import cmdutils from qutebrowser.utils import (utils, objreg, standarddir, log, qtutils, usertypes, message) -from qutebrowser.config import config from qutebrowser.misc import lineparser, objects @@ -274,7 +273,6 @@ class WebHistory(QObject): (hidden in completion) atime: Override the atime used to add the entry """ - assert not config.get('general', 'private-browsing') if not url.isValid(): log.misc.warning("Ignoring invalid URL being added to history") return diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index 2eb31d005..8451aa4ce 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -156,7 +156,7 @@ def update_settings(section, option): def _init_profiles(): - """Init the two used QWebEngineProfiles""" + """Init the two used QWebEngineProfiles.""" global default_profile, private_profile default_profile = QWebEngineProfile.defaultProfile() default_profile.setCachePath( diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 7c17fc5b8..8c2478a18 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -27,8 +27,7 @@ from PyQt5.QtGui import QKeyEvent from PyQt5.QtNetwork import QAuthenticator # pylint: disable=no-name-in-module,import-error,useless-suppression from PyQt5.QtWidgets import QApplication -from PyQt5.QtWebEngineWidgets import (QWebEnginePage, QWebEngineScript, - QWebEngineProfile) +from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript # pylint: enable=no-name-in-module,import-error,useless-suppression from qutebrowser.browser import browsertab, mouse, shared diff --git a/qutebrowser/browser/webkit/webkitsettings.py b/qutebrowser/browser/webkit/webkitsettings.py index d7cc91a4d..f30f5ff5f 100644 --- a/qutebrowser/browser/webkit/webkitsettings.py +++ b/qutebrowser/browser/webkit/webkitsettings.py @@ -91,7 +91,8 @@ def _set_user_stylesheet(): def _init_private_browsing(): if qtutils.is_qtwebkit_ng(): # WORKAROUND for https://github.com/annulen/webkit/issues/54 - message.warning("Private browsing is not fully implemented by QtWebKit-NG!") + message.warning("Private browsing is not fully implemented by " + "QtWebKit-NG!") elif not qtutils.version_check('5.4.2'): # WORKAROUND for https://codereview.qt-project.org/#/c/108936/ # Won't work when private browsing is not enabled globally, but that's diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index 1f573c6d1..e3657d9db 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -206,15 +206,7 @@ class MainWindow(QWidget): self._messageview = messageview.MessageView(parent=self) self._add_overlay(self._messageview, self._messageview.update_geometry) - 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._init_geometry(geometry) self._connect_signals() # When we're here the statusbar might not even really exist yet, so @@ -225,6 +217,17 @@ class MainWindow(QWidget): objreg.get("app").new_window.emit(self) + def _init_geometry(self, geometry): + """Initialize the window geometry or load it from disk.""" + 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())) + def _add_overlay(self, widget, signal, *, centered=False, padding=0): self._overlays.append((widget, signal, centered, padding)) diff --git a/qutebrowser/mainwindow/statusbar/bar.py b/qutebrowser/mainwindow/statusbar/bar.py index 7a302f276..63cc093dd 100644 --- a/qutebrowser/mainwindow/statusbar/bar.py +++ b/qutebrowser/mainwindow/statusbar/bar.py @@ -336,6 +336,7 @@ class StatusBar(QWidget): @pyqtSlot(browsertab.AbstractTab) def on_tab_changed(self, tab): + """Notify sub-widgets when the tab has been changed.""" self.url.on_tab_changed(tab) self.prog.on_tab_changed(tab) self.percentage.on_tab_changed(tab) diff --git a/qutebrowser/misc/cmdhistory.py b/qutebrowser/misc/cmdhistory.py index 9193f7bcc..cb60c84f6 100644 --- a/qutebrowser/misc/cmdhistory.py +++ b/qutebrowser/misc/cmdhistory.py @@ -21,7 +21,6 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject -from qutebrowser.config import config from qutebrowser.utils import usertypes, log diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index df598e4ed..8b2a235d3 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -230,7 +230,7 @@ class FakeWebTab(browsertab.AbstractTab): scroll_pos_perc=(0, 0), load_status=usertypes.LoadStatus.success, progress=0): - super().__init__(win_id=0, mode_manager=None) + super().__init__(win_id=0, mode_manager=None, private=False) self._load_status = load_status self._title = title self._url = url diff --git a/tests/unit/browser/webkit/network/test_networkmanager.py b/tests/unit/browser/webkit/network/test_networkmanager.py index 3e8dd4d13..b66d71f4d 100644 --- a/tests/unit/browser/webkit/network/test_networkmanager.py +++ b/tests/unit/browser/webkit/network/test_networkmanager.py @@ -22,20 +22,11 @@ import pytest from qutebrowser.browser.webkit.network import networkmanager from qutebrowser.browser.webkit import cookies + pytestmark = pytest.mark.usefixtures('cookiejar_and_cache') -class TestPrivateMode: - - def test_init_with_private_mode(self, config_stub): - config_stub.data = {'general': {'private-browsing': True}} - nam = networkmanager.NetworkManager(0, 0) - assert isinstance(nam.cookieJar(), cookies.RAMCookieJar) - - def test_setting_private_mode_later(self, config_stub): - config_stub.data = {'general': {'private-browsing': False}} - nam = networkmanager.NetworkManager(0, 0) - assert not isinstance(nam.cookieJar(), cookies.RAMCookieJar) - config_stub.data = {'general': {'private-browsing': True}} - nam.on_config_changed() - assert isinstance(nam.cookieJar(), cookies.RAMCookieJar) +def test_init_with_private_mode(config_stub): + nam = networkmanager.NetworkManager(win_id=0, tab_id=0, private=True) + assert isinstance(nam.cookieJar(), cookies.RAMCookieJar) + assert nam.cache() is None diff --git a/tests/unit/browser/webkit/test_cache.py b/tests/unit/browser/webkit/test_cache.py index f084a9869..8eaa0112c 100644 --- a/tests/unit/browser/webkit/test_cache.py +++ b/tests/unit/browser/webkit/test_cache.py @@ -54,41 +54,6 @@ def test_cache_config_change_cache_size(config_stub, tmpdir): assert disk_cache.maximumCacheSize() == max_cache_size * 2 -def test_cache_config_enable_private_browsing(config_stub, tmpdir): - """Change private-browsing config to True and emit signal.""" - config_stub.data = { - 'storage': {'cache-size': 1024}, - 'general': {'private-browsing': False} - } - disk_cache = cache.DiskCache(str(tmpdir)) - assert disk_cache.cacheSize() == 0 - preload_cache(disk_cache) - assert disk_cache.cacheSize() > 0 - - config_stub.set('general', 'private-browsing', True) - assert disk_cache.cacheSize() == 0 - - -def test_cache_config_disable_private_browsing(config_stub, tmpdir): - """Change private-browsing config to False and emit signal.""" - config_stub.data = { - 'storage': {'cache-size': 1024}, - 'general': {'private-browsing': True} - } - url = 'http://qutebrowser.org' - metadata = QNetworkCacheMetaData() - metadata.setUrl(QUrl(url)) - assert metadata.isValid() - - disk_cache = cache.DiskCache(str(tmpdir)) - assert disk_cache.prepare(metadata) is None - - config_stub.set('general', 'private-browsing', False) - content = b'cute' - preload_cache(disk_cache, url, content) - assert disk_cache.data(QUrl(url)).readAll() == content - - def test_cache_size_leq_max_cache_size(config_stub, tmpdir): """Test cacheSize <= MaximumCacheSize when cache is activated.""" limit = 100 @@ -108,16 +73,6 @@ def test_cache_size_leq_max_cache_size(config_stub, tmpdir): assert disk_cache.cacheSize() < limit + 100 -def test_cache_size_deactivated(config_stub, tmpdir): - """Confirm that the cache size returns 0 when deactivated.""" - config_stub.data = { - 'storage': {'cache-size': 1024}, - 'general': {'private-browsing': True} - } - disk_cache = cache.DiskCache(str(tmpdir)) - assert disk_cache.cacheSize() == 0 - - def test_cache_existing_metadata_file(config_stub, tmpdir): """Test querying existing meta data file from activated cache.""" config_stub.data = { @@ -155,42 +110,6 @@ def test_cache_nonexistent_metadata_file(config_stub, tmpdir): assert not cache_file.isValid() -def test_cache_deactivated_metadata_file(config_stub, tmpdir): - """Test querying meta data file when cache is deactivated.""" - config_stub.data = { - 'storage': {'cache-size': 1024}, - 'general': {'private-browsing': True} - } - disk_cache = cache.DiskCache(str(tmpdir)) - assert disk_cache.fileMetaData("foo") == QNetworkCacheMetaData() - - -def test_cache_deactivated_private_browsing(config_stub, tmpdir): - """Test if cache is deactivated in private-browsing mode.""" - config_stub.data = { - 'storage': {'cache-size': 1024}, - 'general': {'private-browsing': True} - } - disk_cache = cache.DiskCache(str(tmpdir)) - - metadata = QNetworkCacheMetaData() - metadata.setUrl(QUrl('http://www.example.com/')) - assert metadata.isValid() - assert disk_cache.prepare(metadata) is None - - -def test_cache_deactivated_get_data(config_stub, tmpdir): - """Query some data from a deactivated cache.""" - config_stub.data = { - 'storage': {'cache-size': 1024}, - 'general': {'private-browsing': True} - } - disk_cache = cache.DiskCache(str(tmpdir)) - - url = QUrl('http://www.example.com/') - assert disk_cache.data(url) is None - - def test_cache_get_nonexistent_data(config_stub, tmpdir): """Test querying some data that was never inserted.""" config_stub.data = { @@ -203,18 +122,6 @@ def test_cache_get_nonexistent_data(config_stub, tmpdir): assert disk_cache.data(QUrl('http://qutebrowser.org')) is None -def test_cache_deactivated_remove_data(config_stub, tmpdir): - """Test removing some data from a deactivated cache.""" - config_stub.data = { - 'storage': {'cache-size': 1024}, - 'general': {'private-browsing': True} - } - disk_cache = cache.DiskCache(str(tmpdir)) - - url = QUrl('http://www.example.com/') - assert not disk_cache.remove(url) - - def test_cache_insert_data(config_stub, tmpdir): """Test if entries inserted into the cache are actually there.""" config_stub.data = { @@ -232,28 +139,6 @@ def test_cache_insert_data(config_stub, tmpdir): assert disk_cache.data(QUrl(url)).readAll() == content -def test_cache_deactivated_insert_data(config_stub, tmpdir): - """Insert data when cache is deactivated.""" - # First create QNetworkDiskCache just to get a valid QIODevice from it - url = 'http://qutebrowser.org' - disk_cache = QNetworkDiskCache() - disk_cache.setCacheDirectory(str(tmpdir)) - metadata = QNetworkCacheMetaData() - metadata.setUrl(QUrl(url)) - device = disk_cache.prepare(metadata) - assert device is not None - - # Now create a deactivated DiskCache and insert the valid device created - # above (there probably is a better way to get a valid QIODevice...) - config_stub.data = { - 'storage': {'cache-size': 1024}, - 'general': {'private-browsing': True} - } - - deactivated_cache = cache.DiskCache(str(tmpdir)) - assert deactivated_cache.insert(device) is None - - def test_cache_remove_data(config_stub, tmpdir): """Test if a previously inserted entry can be removed from the cache.""" config_stub.data = { @@ -285,16 +170,6 @@ def test_cache_clear_activated(config_stub, tmpdir): assert disk_cache.cacheSize() == 0 -def test_cache_clear_deactivated(config_stub, tmpdir): - """Test method clear() on deactivated cache.""" - config_stub.data = { - 'storage': {'cache-size': 1024}, - 'general': {'private-browsing': True} - } - disk_cache = cache.DiskCache(str(tmpdir)) - assert disk_cache.clear() is None - - def test_cache_metadata(config_stub, tmpdir): """Ensure that DiskCache.metaData() returns exactly what was inserted.""" config_stub.data = { @@ -313,18 +188,6 @@ def test_cache_metadata(config_stub, tmpdir): assert disk_cache.metaData(QUrl(url)) == metadata -def test_cache_deactivated_metadata(config_stub, tmpdir): - """Test querying metaData() on not activated cache.""" - config_stub.data = { - 'storage': {'cache-size': 1024}, - 'general': {'private-browsing': True} - } - url = 'http://qutebrowser.org' - - disk_cache = cache.DiskCache(str(tmpdir)) - assert disk_cache.metaData(QUrl(url)) == QNetworkCacheMetaData() - - def test_cache_update_metadata(config_stub, tmpdir): """Test updating the meta data for an existing cache entry.""" config_stub.data = { @@ -343,21 +206,6 @@ def test_cache_update_metadata(config_stub, tmpdir): assert disk_cache.metaData(QUrl(url)) == metadata -def test_cache_deactivated_update_metadata(config_stub, tmpdir): - """Test updating the meta data when cache is not activated.""" - config_stub.data = { - 'storage': {'cache-size': 1024}, - 'general': {'private-browsing': True} - } - url = 'http://qutebrowser.org' - disk_cache = cache.DiskCache(str(tmpdir)) - - metadata = QNetworkCacheMetaData() - metadata.setUrl(QUrl(url)) - assert metadata.isValid() - assert disk_cache.updateMetaData(metadata) is None - - def test_cache_full(config_stub, tmpdir): """Do a sanity test involving everything.""" config_stub.data = { @@ -385,3 +233,10 @@ def test_cache_full(config_stub, tmpdir): assert disk_cache.metaData(QUrl(url)).lastModified() == soon assert disk_cache.data(QUrl(url)).readAll() == content + + +def test_private_browsing(config_stub, tmpdir): + """Make sure the cache asserts with private browsing.""" + config_stub.data = {'general': {'private-browsing': True}} + with pytest.raises(AssertionError): + cache.DiskCache(str(tmpdir)) diff --git a/tests/unit/browser/webkit/test_history.py b/tests/unit/browser/webkit/test_history.py index 37176dffe..f40e41c2c 100644 --- a/tests/unit/browser/webkit/test_history.py +++ b/tests/unit/browser/webkit/test_history.py @@ -38,18 +38,13 @@ class FakeWebHistory: self.history_dict = history_dict -@pytest.fixture(autouse=True) -def prerequisites(config_stub, fake_save_manager): - """Make sure everything is ready to initialize a WebHistory.""" - config_stub.data = {'general': {'private-browsing': False}} - - @pytest.fixture() -def hist(tmpdir): +def hist(tmpdir, fake_save_manager): return history.WebHistory(hist_dir=str(tmpdir), hist_name='history') -def test_async_read_twice(monkeypatch, qtbot, tmpdir, caplog): +def test_async_read_twice(monkeypatch, qtbot, tmpdir, caplog, + fake_save_manager): (tmpdir / 'filled-history').write('\n'.join([ '12345 http://example.com/ title', '67890 http://example.com/', @@ -88,34 +83,6 @@ def test_adding_item_during_async_read(qtbot, hist, redirect): assert list(hist.history_dict.values()) == [expected] -def test_private_browsing(qtbot, tmpdir, fake_save_manager, config_stub): - """Make sure no data is saved at all with private browsing.""" - config_stub.data = {'general': {'private-browsing': True}} - private_hist = history.WebHistory(hist_dir=str(tmpdir), - hist_name='history') - - # Before initial read - with qtbot.assertNotEmitted(private_hist.add_completion_item), \ - qtbot.assertNotEmitted(private_hist.item_added): - private_hist.add_url(QUrl('http://www.example.com/')) - assert not private_hist._temp_history - - # read - with qtbot.assertNotEmitted(private_hist.add_completion_item), \ - qtbot.assertNotEmitted(private_hist.item_added): - with qtbot.waitSignals([private_hist.async_read_done], order='strict'): - list(private_hist.async_read()) - - # after read - with qtbot.assertNotEmitted(private_hist.add_completion_item), \ - qtbot.assertNotEmitted(private_hist.item_added): - private_hist.add_url(QUrl('http://www.example.com/')) - - assert not private_hist._temp_history - assert not private_hist._new_history - assert not private_hist.history_dict - - def test_iter(hist): list(hist.async_read()) @@ -257,7 +224,7 @@ def test_add_item_redirect(qtbot, hist): assert hist.history_dict[url] == entry -def test_add_item_redirect_update(qtbot, tmpdir): +def test_add_item_redirect_update(qtbot, tmpdir, fake_save_manager): """A redirect update added should override a non-redirect one.""" url = 'http://www.example.com/' diff --git a/tests/unit/misc/test_miscwidgets.py b/tests/unit/misc/test_miscwidgets.py index 03441d9f2..d6a1c2210 100644 --- a/tests/unit/misc/test_miscwidgets.py +++ b/tests/unit/misc/test_miscwidgets.py @@ -34,7 +34,7 @@ class TestCommandLineEdit: @pytest.fixture def cmd_edit(self, qtbot): """Fixture to initialize a CommandLineEdit.""" - cmd_edit = miscwidgets.CommandLineEdit(None) + cmd_edit = miscwidgets.CommandLineEdit() cmd_edit.set_prompt(':') qtbot.add_widget(cmd_edit) assert cmd_edit.text() == '' From b2abf7a3aaeacd774bde13dd12435797670e2552 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 10 May 2017 19:08:29 +0200 Subject: [PATCH 235/825] Update requests from 2.14.1 to 2.14.2 --- misc/requirements/requirements-codecov.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-codecov.txt b/misc/requirements/requirements-codecov.txt index 5fee39d9a..a26ad70b1 100644 --- a/misc/requirements/requirements-codecov.txt +++ b/misc/requirements/requirements-codecov.txt @@ -2,4 +2,4 @@ codecov==2.0.9 coverage==4.4 -requests==2.14.1 +requests==2.14.2 From 140eb13677519c0c7dacb5280183d82f69c09a12 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 10 May 2017 19:08:31 +0200 Subject: [PATCH 236/825] Update requests from 2.14.1 to 2.14.2 --- misc/requirements/requirements-pylint-master.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-pylint-master.txt b/misc/requirements/requirements-pylint-master.txt index 22622f14c..d8637a30b 100644 --- a/misc/requirements/requirements-pylint-master.txt +++ b/misc/requirements/requirements-pylint-master.txt @@ -8,7 +8,7 @@ lazy-object-proxy==1.3.1 mccabe==0.6.1 -e git+https://github.com/PyCQA/pylint.git#egg=pylint ./scripts/dev/pylint_checkers -requests==2.14.1 +requests==2.14.2 uritemplate==3.0.0 uritemplate.py==3.0.2 wrapt==1.10.10 From d50a08d159deff04791e4736343b4d42e5e0d4e2 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Wed, 10 May 2017 19:08:32 +0200 Subject: [PATCH 237/825] Update requests from 2.14.1 to 2.14.2 --- misc/requirements/requirements-pylint.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index 2c6cf7729..ae9caa0ae 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -7,7 +7,7 @@ lazy-object-proxy==1.3.1 mccabe==0.6.1 pylint==1.7.1 ./scripts/dev/pylint_checkers -requests==2.14.1 +requests==2.14.2 uritemplate==3.0.0 uritemplate.py==3.0.2 wrapt==1.10.10 From 3317834b3626c41cbe803c4c22c5b4485e8b92ce Mon Sep 17 00:00:00 2001 From: Jay Kamat Date: Thu, 11 May 2017 13:28:26 -0700 Subject: [PATCH 238/825] Fix a bug where pinned tabs were occasionally miscounted Example case: :tab-only. This should cover other cases, but currently those cases (such as :tab-only) do NOT have a warning message when popping up. --- qutebrowser/browser/commands.py | 12 +++++------- qutebrowser/mainwindow/tabbedbrowser.py | 4 ++++ tests/end2end/features/conftest.py | 1 + 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index f55e77c6f..2bc91c8b3 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -220,9 +220,6 @@ class CommandDispatcher: self._tabbed_browser.close_tab(tab) tabbar.setSelectionBehaviorOnRemove(old_selection_behavior) - if tab.data.pinned: - tabbar.pinned_count -= 1 - @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('count', count=True) def tab_close(self, prev=False, next_=False, opposite=False, @@ -241,12 +238,13 @@ class CommandDispatcher: if tab is None: return close = functools.partial(self._tab_close, tab, prev, - next_, opposite) + next_, opposite) if tab.data.pinned and not force: - message.confirm_async(title='Pinned Tab', - text="Are you sure you want to close a pinned tab?", - yes_action=close, default=False) + message.confirm_async( + title='Pinned Tab', + text="Are you sure you want to close a pinned tab?", + yes_action=close, default=False) else: close() diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py index 6bce2166a..41a7d00df 100644 --- a/qutebrowser/mainwindow/tabbedbrowser.py +++ b/qutebrowser/mainwindow/tabbedbrowser.py @@ -241,6 +241,10 @@ class TabbedBrowser(tabwidget.TabWidget): if last_close == 'ignore' and count == 1: return + # If we are removing a pinned tab, decrease count + if tab.data.pinned: + self.tabBar().pinned_count -= 1 + self._remove_tab(tab, add_undo=add_undo) if count == 1: # We just closed the last tab above. diff --git a/tests/end2end/features/conftest.py b/tests/end2end/features/conftest.py index 2aa897dfa..3b6d86c3d 100644 --- a/tests/end2end/features/conftest.py +++ b/tests/end2end/features/conftest.py @@ -163,6 +163,7 @@ def clean_open_tabs(quteproc): quteproc.send_cmd(':tab-close --force') quteproc.wait_for_load_finished_url('about:blank') + @bdd.given('pdfjs is available') def pdfjs_available(): if not pdfjs.is_available(): From cb654225fd1577e4605a08c644a056f37f368cbf Mon Sep 17 00:00:00 2001 From: Jay Kamat Date: Thu, 11 May 2017 14:05:25 -0700 Subject: [PATCH 239/825] Add a test case for loading/saving pinned tabs in sessions --- tests/end2end/features/sessions.feature | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/end2end/features/sessions.feature b/tests/end2end/features/sessions.feature index 226d3107d..d8c766632 100644 --- a/tests/end2end/features/sessions.feature +++ b/tests/end2end/features/sessions.feature @@ -342,7 +342,7 @@ Feature: Saving and loading sessions Scenario: Loading a directory When I run :session-load (tmpdir) Then the error "Error while loading session: *" should be shown - + Scenario: Loading internal session without --force When I run :session-save --force _internal And I run :session-load _internal @@ -367,3 +367,26 @@ Feature: Saving and loading sessions Scenario: Loading a session which doesn't exist When I run :session-load inexistent_session Then the error "Session inexistent_session not found!" should be shown + + + # Test load/save of pinned tabs + + Scenario: Saving/Loading a session with pinned tabs + When I open data/numbers/1.txt + And I open data/numbers/2.txt in a new tab + And I open data/numbers/3.txt in a new tab + And I run :tab-prev + And I run :tab-pin + And I run :tab-next + And I run :session-save pin_session + And I run :tab-only + And I run :tab-close --force + And I run :session-load -c pin_session + And I run :tab-prev + And I run :open data/numbers/4.txt + And I wait 10s + Then the message "Tab is pinned!" should be shown + And the following tabs should be open: + - data/numbers/2.txt (active) + - data/numbers/1.txt + - data/numbers/3.txt From 4c28487fd0339c51829b6765ab232b5f8702fee8 Mon Sep 17 00:00:00 2001 From: Jay Kamat Date: Thu, 11 May 2017 14:30:45 -0700 Subject: [PATCH 240/825] Warn user if pinned tab is closed via tab-only --- qutebrowser/browser/commands.py | 30 ++++++++++++++++++++----- tests/end2end/features/tabs.feature | 2 +- tests/end2end/features/test_tabs_bdd.py | 5 ----- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 2bc91c8b3..1780e0053 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -852,7 +852,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('count', count=True) - def zoom(self, zoom: int = None, count=None): + def zoom(self, zoom: int=None, count=None): """Set the zoom level for the current tab. The zoom can be given as argument or as [count]. If neither is @@ -875,22 +875,40 @@ class CommandDispatcher: message.info("Zoom level: {}%".format(level), replace=True) @cmdutils.register(instance='command-dispatcher', scope='window') - def tab_only(self, prev=False, next_=False): + def tab_only(self, prev=False, next_=False, force=False): """Close all tabs except for the current one. Args: prev: Keep tabs before the current. next_: Keep tabs after the current. + force: Avoid confirmation for pinned tabs. """ cmdutils.check_exclusive((prev, next_), 'pn') cur_idx = self._tabbed_browser.currentIndex() assert cur_idx != -1 + def _to_close(index): + """Helper method to check if a tab should be closed or not.""" + return not (i == cur_idx + or (prev and i < cur_idx) + or (next_ and i > cur_idx)) + + # Check to see if we are closing any pinned tabs + if not force: + for i, tab in enumerate(self._tabbed_browser.widgets()): + if _to_close(i) and tab.data.pinned: + # Show alert only once, then exit + message.confirm_async( + title='Closing Pinned Tab', + text="This action will close at least one " + + "pinned tab, continue?", + yes_action=lambda: self.tab_only( + prev=prev, next_=next_, force=True), default=False) + # We will get called again with force if user selects yes + return + for i, tab in enumerate(self._tabbed_browser.widgets()): - if (i == cur_idx or (prev and i < cur_idx) or - (next_ and i > cur_idx)): - continue - else: + if _to_close(i): self._tabbed_browser.close_tab(tab) @cmdutils.register(instance='command-dispatcher', scope='window') diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature index 9e14e6ca6..de1c02bcc 100644 --- a/tests/end2end/features/tabs.feature +++ b/tests/end2end/features/tabs.feature @@ -1077,7 +1077,7 @@ Feature: Tab management And I open data/numbers/2.txt in a new tab And I run :tab-pin And I run :tab-close - And I wait for a prompt + And I wait for "Asking question *" in the log And I run :prompt-accept yes Then the following tabs should be open: - data/numbers/1.txt (active) diff --git a/tests/end2end/features/test_tabs_bdd.py b/tests/end2end/features/test_tabs_bdd.py index 3b106d7b8..e86204ba9 100644 --- a/tests/end2end/features/test_tabs_bdd.py +++ b/tests/end2end/features/test_tabs_bdd.py @@ -19,8 +19,3 @@ import pytest_bdd as bdd bdd.scenarios('tabs.feature') - - -@bdd.when("I wait for a prompt") -def wait_for_prompt(quteproc): - quteproc.wait_for(message='Asking question *') From 21455cf0e74a2423b5188432f31f0d43c0df91ef Mon Sep 17 00:00:00 2001 From: Jay Kamat Date: Thu, 11 May 2017 15:37:52 -0700 Subject: [PATCH 241/825] Clean up pinned tab alert logic should be a lot more reusable now --- qutebrowser/browser/commands.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 1780e0053..af2da6869 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -220,6 +220,19 @@ class CommandDispatcher: self._tabbed_browser.close_tab(tab) tabbar.setSelectionBehaviorOnRemove(old_selection_behavior) + def _tab_close_prompt_if_pinned(self, tab, force, yes_action): + """Helper method for tab_close. + + If tab is pinned, prompt. If everything is good, run yes_action. + """ + if tab.data.pinned and not force: + message.confirm_async( + title='Pinned Tab', + text="Are you sure you want to close a pinned tab?", + yes_action=yes_action, default=False) + else: + yes_action() + @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('count', count=True) def tab_close(self, prev=False, next_=False, opposite=False, @@ -240,13 +253,7 @@ class CommandDispatcher: close = functools.partial(self._tab_close, tab, prev, next_, opposite) - if tab.data.pinned and not force: - message.confirm_async( - title='Pinned Tab', - text="Are you sure you want to close a pinned tab?", - yes_action=close, default=False) - else: - close() + self._tab_close_prompt_if_pinned(tab, force, close) @cmdutils.register(instance='command-dispatcher', scope='window', name='tab-pin') @@ -897,14 +904,9 @@ class CommandDispatcher: if not force: for i, tab in enumerate(self._tabbed_browser.widgets()): if _to_close(i) and tab.data.pinned: - # Show alert only once, then exit - message.confirm_async( - title='Closing Pinned Tab', - text="This action will close at least one " + - "pinned tab, continue?", - yes_action=lambda: self.tab_only( - prev=prev, next_=next_, force=True), default=False) - # We will get called again with force if user selects yes + self._tab_close_prompt_if_pinned( + tab, force, + lambda: self.tab_only(prev=prev, next_=next_, force=True)) return for i, tab in enumerate(self._tabbed_browser.widgets()): From e238f32e7b40f6da9aa893150cd35368814fc403 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Fri, 12 May 2017 00:59:36 +0200 Subject: [PATCH 242/825] Update flake8-deprecated from 1.1 to 1.2 --- misc/requirements/requirements-flake8.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt index 8cd1eb296..52a37907f 100644 --- a/misc/requirements/requirements-flake8.txt +++ b/misc/requirements/requirements-flake8.txt @@ -3,7 +3,7 @@ flake8==2.6.2 # rq.filter: < 3.0.0 flake8-copyright==0.2.0 flake8-debugger==1.4.0 # rq.filter: != 2.0.0 -flake8-deprecated==1.1 +flake8-deprecated==1.2 flake8-docstrings==1.0.3 # rq.filter: < 1.1.0 flake8-future-import==0.4.3 flake8-mock==0.3 From dfc44f05c57885e7c6890318dbfb7204f4e07618 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Fri, 12 May 2017 00:59:40 +0200 Subject: [PATCH 243/825] Update pytest-cov from 2.5.0 to 2.5.1 --- misc/requirements/requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index e78b98e5d..462ff8406 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -21,7 +21,7 @@ pytest==3.0.7 pytest-bdd==2.18.2 pytest-benchmark==3.0.0 pytest-catchlog==1.2.2 -pytest-cov==2.5.0 +pytest-cov==2.5.1 pytest-faulthandler==1.3.1 pytest-instafail==0.3.0 pytest-mock==1.6.0 From 203a5dff746917d0dc5d1366eb2fd9e408700483 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 12 May 2017 09:13:19 +0200 Subject: [PATCH 244/825] Get rid of webelem.FILTERS There's actually no good reason to filter javascript links as we might want to click them (or copy their URL) just like any other link - this fixes #2404. With that being gone, we don't need FILTERS at all anymore, as we can check for existence of the href attribute in the CSS selector instead. --- CHANGELOG.asciidoc | 1 + qutebrowser/browser/hints.py | 4 +--- qutebrowser/browser/navigate.py | 3 +-- qutebrowser/browser/webelem.py | 18 +++--------------- tests/end2end/data/hints/html/javascript.html | 13 +++++++++++++ tests/end2end/features/hints.feature | 6 ++++++ tests/unit/browser/webkit/test_webkitelem.py | 10 ++-------- 7 files changed, 27 insertions(+), 28 deletions(-) create mode 100644 tests/end2end/data/hints/html/javascript.html diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index bf01a2a17..1dca28d33 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -54,6 +54,7 @@ Changed a restart. - The adblocker now also blocks non-GET requests (e.g. POST) - `:follow-selected` now also works with QtWebEngine +- javascript: links can now be hinted Fixed ~~~~~ diff --git a/qutebrowser/browser/hints.py b/qutebrowser/browser/hints.py index a7a5941f9..3cc70f434 100644 --- a/qutebrowser/browser/hints.py +++ b/qutebrowser/browser/hints.py @@ -579,12 +579,10 @@ class HintManager(QObject): if elems is None: message.error("There was an error while getting hint elements") return - - filterfunc = webelem.FILTERS.get(self._context.group, lambda e: True) - elems = [e for e in elems if filterfunc(e)] if not elems: message.error("No elements found.") return + strings = self._hint_strings(elems) log.hints.debug("hints: {}".format(', '.join(strings))) diff --git a/qutebrowser/browser/navigate.py b/qutebrowser/browser/navigate.py index b1ab6f9b1..a3ec15458 100644 --- a/qutebrowser/browser/navigate.py +++ b/qutebrowser/browser/navigate.py @@ -79,8 +79,7 @@ def _find_prevnext(prev, elems): return e # Then check for regular links/buttons. - filterfunc = webelem.FILTERS[webelem.Group.prevnext] - elems = [e for e in elems if e.tag_name() != 'link' and filterfunc(e)] + elems = [e for e in elems if e.tag_name() != 'link'] option = 'prev-regexes' if prev else 'next-regexes' if not elems: return None diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py index dce808871..d2e499b83 100644 --- a/qutebrowser/browser/webelem.py +++ b/qutebrowser/browser/webelem.py @@ -22,9 +22,6 @@ Module attributes: Group: Enum for different kinds of groups. SELECTORS: CSS selectors for different groups of elements. - FILTERS: A dictionary of filter functions for the modes. - The filter for "links" filters javascript:-links and a-tags - without "href". """ import collections.abc @@ -45,10 +42,11 @@ SELECTORS = { Group.all: ('a, area, textarea, select, input:not([type=hidden]), button, ' 'frame, iframe, link, [onclick], [onmousedown], [role=link], ' '[role=option], [role=button], img'), - Group.links: 'a, area, link, [role=link]', + Group.links: 'a[href], area[href], link[href], [role=link][href]', Group.images: 'img', Group.url: '[src], [href]', - Group.prevnext: 'a, area, button, link, [role=button]', + Group.prevnext: 'a[href], area[href], button[href], link[href], ' + '[role=button][href]', Group.inputs: ('input[type=text], input[type=email], input[type=url], ' 'input[type=tel], input[type=number], ' 'input[type=password], input[type=search], ' @@ -56,16 +54,6 @@ SELECTORS = { } -def filter_links(elem): - return 'href' in elem and QUrl(elem['href']).scheme() != 'javascript' - - -FILTERS = { - Group.links: filter_links, - Group.prevnext: filter_links, -} - - class Error(Exception): """Base class for WebElement errors.""" diff --git a/tests/end2end/data/hints/html/javascript.html b/tests/end2end/data/hints/html/javascript.html new file mode 100644 index 000000000..89395ff92 --- /dev/null +++ b/tests/end2end/data/hints/html/javascript.html @@ -0,0 +1,13 @@ + + + + + + + + Javascript link + + + Follow me via JS! + + diff --git a/tests/end2end/features/hints.feature b/tests/end2end/features/hints.feature index bdcf9b9d8..a5e073d8a 100644 --- a/tests/end2end/features/hints.feature +++ b/tests/end2end/features/hints.feature @@ -114,6 +114,12 @@ Feature: Using hints And I hint with args "links yank" and follow a Then the clipboard should contain "nobody" + Scenario: Yanking javascript link to clipboard + When I run :debug-set-fake-clipboard + And I open data/hints/html/javascript.html + And I hint with args "links yank" and follow a + Then the clipboard should contain "javascript:window.location.href='/data/hello.txt'" + Scenario: Rapid hinting When I open data/hints/rapid.html in a new tab And I run :tab-only diff --git a/tests/unit/browser/webkit/test_webkitelem.py b/tests/unit/browser/webkit/test_webkitelem.py index 6258508ff..4915c1fba 100644 --- a/tests/unit/browser/webkit/test_webkitelem.py +++ b/tests/unit/browser/webkit/test_webkitelem.py @@ -149,19 +149,17 @@ class SelectionAndFilterTests: ('', [webelem.Group.all, webelem.Group.links, webelem.Group.prevnext, webelem.Group.url]), ('', [webelem.Group.all, + webelem.Group.links, + webelem.Group.prevnext, webelem.Group.url]), ('', [webelem.Group.all]), ('', [webelem.Group.all, webelem.Group.links, webelem.Group.prevnext, webelem.Group.url]), - ('', [webelem.Group.all, - webelem.Group.url]), ('', [webelem.Group.all]), ('', [webelem.Group.all, webelem.Group.links, webelem.Group.prevnext, webelem.Group.url]), - ('', [webelem.Group.all, - webelem.Group.url]), ('
{{curr_date.strftime("%a, %d %B %Y")}}
{{title}} + {{title}} + {{host}} + {{time.strftime("%X")}}

Q1#7vcZi1F2Rf;pi-lwv4UR3r>6GYF^h@82r-lkxk2A$^4{wf8E#a-2U!a> zcsKyP{A-lJFIZVy-mZ27VU!?!_ZJUsY=B4^tHa8rYRj1)&^eN{rskMXk+))pPkm+Z zBEA5^jg?g?Uwwp!6))nFxr4wQ5JUhI0ft!|7uX))z_8;i$%6onJ65t^5}rMBkTo!v zjU;>ytliGe4gfO%(m2l5bMA-q>kWKX)`t7OZ7{q&=Ws@%xg{rTEnJ)O!9}7;?JKOF z1Pk=SsPPZj`@6e4z^id`zV(rL{|X|E^quw0y81Jw(zkxn5rzXuw2T5`HbZ`=g=SecpS|e{Vq>pm zJCmD?sofgg7fqgrR;9$xwLSKKcj>SOJ&6NrWn!`j?8s!PfsV29ft#Sye5010o7%0jkyO6)E;W3|s`V{7rO zqN3uLH$K1#2i&JEEEp%!y9*kTOyJ3pC&FNXTf;d=Oy2S1Q&XuQKYmP2bqzxE6BHE0 zz{I>Vdf(sQk2Y0s>U&H}!^Wlzm@NiaM#e8KgVesR3%2i7z6YE7nM8oA_TJ3$Z0|35 z^a^qbp8?weVP(ySL}Jf+10=M&yL8rLVewZej}mYS;Hr+cwvTW&=ol9{?dO)yKLJ~) zT&R(w+MmwNBp~n??{nXJzXeVMXjx5dZRMjh;7v0_!TbAu{rXl`RsgZ}@|u>-f3om@ z)dJYoY^<$;y%eB`1!{+kNz}h~Bj~dB^K?k;;iQipyb$1&f2{7Xqy%Wo{0T&P;Iu@q z3J?yoSbUr$SfVg?Hn!MY2y6)0PF>>nJ*y2a>%Rb4CBz8nwP=59SU3iFEVyjykpmj} zFgpRh5#H$$RQ>Aj3}6-Ta&-3$q;{&RPJz=62?-ez zKJ8!cOZ68u0oK>dY-KP@h)$qtF^V_`zC2?MEB*TQb)g{{7E0L$AR&o~h-#~=r}xi+ z@_=m2O*4^#rY2}s2Du2X^yq>&g<}GzcdgU}1u%esyoIxYFaccYCO9K7eBn%4IChl^ zzqRa&B;1`UCywfR5|-D};=i#Nur}k-U|6UEGX{v<&8>QDTiA7@zdw^)PgV6bS307# zu9l7tyYBaQMyfdt4Obs-`242QS()#Ba!Vgfygr7pG$VPyt&Y|y%u(R>9HuVv)(!lBJ3k9{(O zG-7sRw+2Jt!>!$40UtgbGSpE`gBbL=r6mhKe7Ur=)arjz;Q1^Ha7|f4o*w}g`1lck zi}EQUfL1)#MJR{B((9D!*Sr~H^mrBmzdtL#ZENGWZ1GBc-v*Eih+gYSN_n_sZfk2RolLC#Li1HXS(jPetPvaWgXaGJzNvN!LIFW^Dfyn8 z5F|D%EB%z_UD0b0^a6tyc(-eE_#+8Pc-+Fu3jAROpT$;x;M)q`;dTiIIuatFi*NHhsX#<7Y(f*TF$$K=d_WT{wLN z8AdRSU$K@PSgX$bz-BS7ueFK9XJqVnGyyJ2@8OZs7(l4Y&TaVVY@nqV2OF!y?|Qos z{FMm6u*t6`w3}6Y$TYcR~t%)H&J8Uh(JRVv(u=OAkBTW4Wo15W_>lZ7SH zNVvYSkxV{7A7pkwV1B82_#sdJyd}}x87n7Kf)oogb*BYET8#MdKvbTRl8G z>$}tzBo##j#tJCS!m*k8`MV#eR7h-n*uo^#(*G(j5CZ?t5AP!Wf6>4EA43M%wEwYB z`hQJqy(@af?^>BVVzV&>gC!*7s27ohR?OHxd-g1q-NZvM3oWPIZ~Zw41j~zx(1lgY zr{Dm*JHhYHmzJmlA8H+D$;imqjGOtF^MS2>ju#<%_h*ds%sxBddOI_dA{bX`%&~6P zf-6fbuoVsuY=8WJV|Eh!OB-^{NxFB>o?v57OiXBLYmf58;IcpprD>Nh~6 zF(43K@2=0Zi_d@&1(^`Dis{!JfXM|k?#rMV``Q`%y}#Uprsg;xz2HZ2g&=+hGzMbO zt9*4hY3~&jbXIH{L6q=#E0qpFdBGJu?b2oyPJLcC2dx>799y?R)6v`#)HF z4{)si_iel#O)Jrm6)GWFnI$qKBOxUtBeFM9xho?H$tGC|naSQ|W@cuFjO?BHKkv`y z_j{h_cz(aeI-}VP+@(iXB}3Q>B;INiXQ&tb1!`z82DMgnu5@1?m8bP&7q{MjNH3b zFEnl8{O?X(p6aRHjAeT;Q1%RTnQUD1gU^vd_Mbj~wvG4Pli%}6Eh$$7rNCKscGW~x ztPaQ&yzQ042LkKvak1m3{NoomeE4t_ek(UOq*b0s<YtNKIMg8q{xAy2_+QzIZGC`Sag+s%J&OOUU>mK?6;7r|Z4YZY<{!I~xsY@72qfX<|Vf zck(O-$^uvwk%5t_Zlj(}x+;s*BvXTglPqGAJUl#6(a4JvEAme@-F&bIrXAGCf94h}}5pxA}G zsjQ{7zPUENmztYM0zXTXgyVs?D7+eulQa_#viHWJJ^c>Dfwy)vbb~k0p`&@;S?nhE zQw%8j-x^Ohr9aDECm7sAk;|nkWGGTi6~CYwU;jJ*#k}tt2?)W2*jOsSVj?O(!Z3?r{gW0&c1aJa=T@SI`wx%T)2+ za20`NVj{#U8;gsV1t^LpswLq-Z}_M4e{KKInz4YqJIcU-YClda69ZMa0;}6-VS2is zProzxVn#8~)(4A!#%B+15mx@Pl01D~JAARSOXTlR0MB)6K6RD=1_I*Yw>T=2T@%8} zq5}aSm#B{%`C3sy@=QE5zO%wKb+y55bN;=gcck0qU*vw31O*s{+J608X3ld)tDYqn zCr(bHp<(0T;5c)}*wj?mcAV^wr5dwgal4(2>Sv6kHT>>m63W95_rgi_j__4j7n zYKG@7=L`9?O<~_?|E;+NDgiTwIyOk6@8sKAwn%}Byh5kr50dL4a0+6~i^Hc39*!&* zEsGS53)+k}f;dE#0zzJmPpf=z_6H)ls_KfTuw4xB!spL7Q`=L4Qoy847tAXVuCN~7 zlhk^aj0CcI)D757FaSfu34*5?cJaisqjy)j+KYZKI-RGkB@RQ`1>af8(MZ~K?m@72 zpEV}|;h5wVm45U*ubs{JBw;vEuB)f_m-N@KUxcy?oo`TzT>lhC3S}`gG!)Xslgp3E z#wRCF%P1$b9Gva{1^Sj5YNbNLJm$(I54`m9nhTIkCRqXEAbINH?X9;q3?djwW^I|1ARp1IXz45y8RIEyQZM&t?95q4RS6 z>ltjI$5Qn%M3OgB1cdL$m6a8&Dfs#WR{R+g93pG#{mps7|P-KTeZ9qNb#DGK*pu z-@|JE)7910DJdz?7GtjYc=`C;=MKT5K%rTi8Q6)EHzUJJPj67HXn~%M4FuS;lYqwh zTr{cL#lKal5Kay1f^`fGs85~hYiYR%8p)4I>N$bXpI`p`;GeNEzreuBsVNA}I5W&Y z4Y2VqE3K?aOG|(F_!0015s{Y1=!T-AqKb-&jt)cmaHKqpLg36HGxanzS^lmIc~Y@8 zJ6QSii@6EZg$EDp?Cg+bP+RE!_@E^580-^*JrKrWT5kBPi1DTG=b4=N_;|bmIKKOa zhWvbdMGmv?k)RPl@!7yh;-D6~Cg-Kz15Smg0)HGGF5}_DNkm{2y|YXa5fKOWT$Pu9 z`BfJfs3*sAr->>gRT$%7Tf>X**s%k`uEf~P%nZ%3V~6nGnVFdd1?zbGt(=@8#Dd{r za&bs2r^H-e`uZx$%MTqui9}98!3}0TPA=%zG#laBGxBe-h?WSQHT{gGx7e*gpZo913fy7T=QVAnx-Y4_!<_nJ z1n;eWwST9^w#%vV;R5L&K6nVwN-Kw=_1FNk{N@?pOzWT_%~GnYp)D z{oj#7qm(!PW44gwzd-;0kH214`sbbg`Q*fR{BQr-zxMR71eXP$7?zCXoIUHfrvToR4^JsS>+_+-w zqutRmI>di}`cDXZo5h**3mI1r(yCC`M_#a7x$&#?ciXF$c0F?uIk)*wd2uOMWyRGc zjl)|R58fH}9GXzn98%J}$;rW;Gg10viY|WoD{Covs_fZ6$<)T{zq4qL9l_GG?Q;_e zVhZ4Q*aceOB_#CLgTY4^GT#ZO=8QK;IM3D0Ka@&cK3=yrv9a;wut)8usmO)(IsEUP z$I8#5Fjk)com*6U_9u5(PHlRdx@g?#vzc}M6_XQQnGjATby#oz`q?CDorSa`LiKib zi}y}h4wQDxvTZJEeh~}R6!Iu}HvAd?J33X=b>Fx^uaH69=;cJCr{4WJ*?G%L zr2F>{=Bqn>_-K+PM5ACt`gDg2Q`}d0NXlbwcL{FE*HKnhs|PpVkCT{<8-0C+I^JfT zU+)*&1nm-+XCp=YnO7DXNySa_chsfH7*y}}r0G_6M$)m|&XOFqUDnn=Ov}!tq3|}! z_o#ClUslH|!7_e8M!ff1;Zc(FS0tow+<1(J61B+#%vrlG43x&`i_$Wyv%hT*amGe; zJvL3NB1(HlxtGk-Q=z6Z`(PQJ`$?r#E6-cUKU~KUki8Q3&Oh-D%8<7IJ)V2^LCdh^ zv26C1Zwx~DPH)Y{yg%=97479^`S_Flz-Y_ROpjhr|J~f2@_`@mC-N7l7=Cm#NBZCXn9P%%9ep}K zje;S|tf(u|Qd^nb{L7P`Y>0a2TW3MN-7C;aV zOdKTfKBV)4M9j$ZIyp)7=llk3=Ws7FK?;h`-H&TncELTy|E^vsf=6Clzl_G%h!`*L zDXsi*+;mhsThicMWt_fE42{HQv9Vt!pPFxsPn6aC^2$?mf1h}^jUvr>Ab~VW{t#!& z{!6D8(n4Ms&X>nk*qQM#u6u@vgv{#2s&Gu%XeF8?e~L{Nq&>&BteZJ=2Ny-$>!BaH z%5>s5>~6L5OC}UtAxYed<dHKOm)d)g=fdGWZUZ)* zN)lts2?wezrvO8?HZF-52e`vFDE-PjMna-?9HwN-+C^ymn0vOvPvgdwyNnD1MdiYc zdXs1Mj_h0=vSeb8CIPf#3Yi6BRbJP5+*rwhd(6H#0xp z$kOXmEkPn8SJPKi->+dMD(>;(g_P~Mz&FLY#N0RpI`4Di<6)X6$2Dg%E1o_p_v)l} zewrq<;3_`4v8&bCD!cac(fY{J^7*lqYf~eI^hbD4d2U3S{`kS~NwQIVha^Uyx$V1( zyBe__hWeSg+@BF268|kXzuV#xqu#$5mbM=V|MoW9wcN%wQ%XqvbyP>_W3ppsmx}ej zWG!<~Mhd*`Vd;|(jqu*uGWFL>N;Lm=D5fEEJvQ7wlFmTO3GvA*JUMvg-eMHhOiIqn zGD5QM1RV>5{DzB}u4iwu++My{ej9yb&u@iDnsnFy*>Xvq8e~m*pZ?cms3e3F*R+it z0ax68_YD!Wrty5<6x;p2Fe>7uNZr<#?`hf5f915#v-5aZOm`UxKU^oN=VE%{nfW)% z7EH-9S)TayjV<{@WrdULtR_cj&XDSqZ4AsCeRn*=J)|;s;0FsOceT@P&o5>lHkcLH zrAY1{QFSp|Uq}{x#I1AwGRgP$NVE4B7Jp~jY|R)FxXFW*M(5T(i!FWnYUQvMP0GNd zle|zAHL`g2oWdoG->4t9ZNnZgoS2Drh$#uP=lz~?vMg%yr^fChg{-Ud3Qy<8_hh)G zhLMtqS3H(EkxU=Vqwv;Z|IECVndXFdRspwe_2UB0?NLe-M@h*UkRTmQUp25Z_Q_Up zuyj`xRVi^(s0_Urp;BWcm`NpE48c%_E*nR}>nwr?+V#w9AKX29^QN1c#!FH28}$9d zaqN*7t!KkSnyStDrVf5e-tZvv^7xy@tK%QezRA+Mmd7H}TJ663Z}darg^vnpI5h?* zO3Md{G<27jJK~^kZ~k%+o_trqsTIbJhq`v8c2RlMltE4T@41FDe=98&Z9nC9IqEQz z{3$VUv!R6Z4lmqen&)e)54Z7uel8+E&RaiOY1ZGf?duU+Q8!`cbzVcCQ};dvjW(3M z`qACo(yp5(^krqE$y<^1zPR4=9pt@_H>*F)%-la@$0hocLN&n$U8CodsyQ8fZJ%B` z8{o0`>eT6eJt~@q;|`SH6FYi(=8buEp477*9MiM@$tm^8@2Dj}r?{Hs1G6(()c8yY z(HE0>l|)<^J{D%~XBaxQIWa!?MyZ(s$7^=v=%pr=h?fm>7ebOR@fCT+Ja7HM|H#O6 z^4#W_7t`#g&87NJQFVnwQnELH@YE@VP>rU)eeuGuIHl9;>?j4f9MYxMp#~%Whl<<9jmyNB+@3!L_Zq82y6%rDHQU z8!j$aNbCt%7N6=il5k^&$`*j2>s~9K)jdcG?QhOCAp=>&7Z+*p{`Fm%>=eiqywR=-Me6L`QPn(ct*V^*A zZ62ez@FC#vNwVMQMc^KLw`+C%$~C@>UNwS#Z#K=zC9!dPevLSuHlQ z{K{{NySdmN6?^xU{HyEEa;C(_l5HjLXMEF3UtUnYw!YlYCY%@%FVJim_~}i~Okx9{ zV>SaHOQYjf?%uT%RAh8ejj3ZlH9B6;qr| zAZu6XAF@?yacMQ#g)!rVbAHvgVw9Dwhb@TipH9uh|FHy-oScrAb&GtZ2E>hv0i$-MMXvSZ{hUc*Jv~R zCCehL3b!0}Yina}NE2UXrtc;djD^vdn1jmh)(6#bw>wHJm6F82Zo8~qLWg>h_^1E2 zYWly|RR4#cZu>tKbpxCOV5-3z{QWXg*52M;Tl-w95M*j2lZ{A&cIUO=Bb$RLj-u`n3!T9xlX5%9xfPrqWedfR(oCGB(D zf~)Z>ei70-G`O|NFMY6TkcopM;W;)51vLD6FADj5bVwTUXu?~x%wusl8EW-K@;zMTLv!;NUTa4joSor00#m}n&wq}M z?eXQJs{{feULgXy!As8NiB3IT-S;0qu1-I2ai@dz( z(ql1mbkKaZ{`o(>01n|I&U_3EI&k#i&Ov)6iN>vV-+LG@(>%5Se`-&m^U=LUHe>!p zMWVA4VE>GGy72Rv(*f1^-6gOnWa8xHd`3p| zw{Oo&@9o;XJDs$qyxcu)8g_@Sj*d}~Gsea#cMC(Mw}m`?`t+ob9i)X1NlCalV$Ms+ zeHkAcpva-p8 z2OvhB#+pyVM}6{SL}Vl#;fAJWC`<+TetP;Wyof6+PPbfQ-oM9vZ}07$8XuR9X@W_+ zHqOFRi)f93!{q8!Js5O5x?vyAciVEcN`p^_GIy;xgU(^q?eWJyU?%ZvTKL4xMp>bu z$6P|f!sKF_dJCOF23?71f*T3q0IXVY0cM+aipMnZJ?tV}gt7cY8yc(7q3+1Y(N?BwW3 zPRq<>c(y1pH`mSezqkW9W=zXUrVKDPDMkq1N6+0ouC`Bi!a%|W2j|#LYVL8r z0D3focKw{DiMo1lTG|X}@%o(otNie|fWScX*7{z*vU4oxAG=|1Z!Z=WuL`0NEGCl~ z%*sD;=RiT4nBWfukJjEgRP^=r{rEw}A3{`xzQX*RZ#692E!d$(4Y$*iy+PPR*e5UL z&Ut%#V{4yuFAWxP&JGK^4Tdat%W-`r9W8jE>#*+v1CKN@B0KSuA3y$Qc=-Oqhbb{J zDoRS0a5x=4B`O~!TG+R@Axg|#Pw#3>6Z`q|>57jYZNPmB*8*5X(1|WCE)d8dBD|XB zI#d7k$Q7+7Ku* zoSdmJ5<*hAe}5P@0T{Ff$Hriyz!^g@L7NDfIPn!8JWy6stFNh^n*QQA5?LjnVzF&V}b72SnmQ(G&i zr6tV6V*_~vPTX-`=J?#)PPh;>G&*4ql~X>-u1-clf!uNNeht<7J$?Nl$Q=j@^>^wa zLSUS3`+7S& zX{o5-`QS5YW3QLTeTCH=DjR5I?A`6#w{tx6ro>7me)xdfeo4RTHAL@lP7W7Ijl;ql zVwYRvnv4*FI=<_sL#3E?hHsoeCMzT3=jR7TR9;eYw@zWP)vy=DQLxz`RL?$}GnQ@X z>KY#$oQL74wz?WiKhan0){&-TDtInFHnyU>TT4bJ5nMUqE+Zo&WTNoWjLb};#4=Rn zr&Ip?8G!(u%^bJ&cDCv4(h?r_kd~Rb|InfAjEr9{Gtf`4YvJ2@`}(zqhX)@1@CB~x zPv62B;1;2wrv6r5J_sHi9w-);-&0efI%NbLzK8IhG{?&1bQfoRW+cF)8;!Q<`!j3Q z7HZct_U_u%(ba_(j+2J5Sy^ush%WgI^<+fHyxiP7 zc{ZdBR0yNAw7HUuF;W^JiC7(LXf#~My z>Pm2`x#r?h0RR5Sn@O&JhlB7hAn+kHy&w>X!oyw*d;7kO``!@|Cl4QPz>UA7rS(a> zn1h~vCaRHYVR3Qxfcs)Yy6eU&WG?a`yp!r#Sdgi-5!m-0rX#aQr2L=?5rD5KD=kgH zbzQ)6844LXJh3}gxM6+M(8$fm@c*jYQ}ifIHV$eK#S`C?LfQ99cTB+Khd2x&q@pJ$ zN8oIrrn5M6L1WXoZ{!_H#8h^ zHQ7wptAyX7C+Hpk!|eTAw}QO9YM?b&9mXSL(s@ND@X=1Ue}fZ`%+YW^deEh$akYN3 zaY!aX4vl4HjP&&P=oiI+W-ijWm(s6Y45|CnfL=Cg@7CsK@i=AJ!%91cPM$cCw)Q=Y zUt-MS$%&VlHdPze#T));gu@20>}+f-xyMzd%iu8kYj>dRLt|r|bah1C!lfe!yb1~m zLy@I*bqDg>Q*KMqX<1Nvls>D2;AuC}aYYM}6cK8PDNXzlM_lZ>`_65Z6v9U1+rx*My@btk6 z19PxpL!_;}{b4RYl4z{lLYNB7cL-U|&TH6rOApkB^&8*8Ij9S#c403hq#iiZBh$$# zsW@cd)!D=;wJ1U}B5vB^!oq5xH6Q}gxZ{r4loZ&D@ansKw~WHdT-0HF@jhC5!i$ zlG=+w88R|xjG*l=TSCnCl!Mtd<53PLTAt7m>7?xvcf%th1-YxNtZYa`5zyk4sLSX% zsi7`73OjGfQc+QP>?2w(^K)FaOA9~yA6yxdd;u?GlbAJ(g~i1ePdOAzWYc5NCYr5s@<6~=W&6IkJ=*iXBH!?Ay6on)Y_3fdF$>0essv}1vB_ss;np;}~ zBO-u5WO_NS#wl0dq0HTm8_7tR$)!+tS`mU)h$Hs;^=o=(FE20GJEoB0>Z+<7=7vZ+ z1B^c!^mI6EO*2kJ$xBQZrr)k6Dt!?C5kkYqv!MTW%p;3qf1%Xq1oBLlWn#3eMwIz z*FF@U%4WhbHZrvLLmr|JwizNJPzYZ9FFwf`D%|DwxaoJNX*|NQoK>0z+WY4rX}Yb? zN@;0nVKT`xabt7y>7>iVpFWi&Nk2(lGXL?eT=@5d)YsvKv>d$kd-wKgl0H9p{91B5 z#`n~PxL01(4cJwG(y$9*hQF(g>$NMXZ-wbx#UO|G*F5y{0F}Uv^v@I)=CZor3rx(+ zL$J-Lsi`43*;rY@ei#)R7KS1YfXHFAkwG`|@y{U4m@zbDy?uN1rV_xn4tGd+N<31R zoM%AG92!2-FwP^JSMvSJ&#o?r?@gRAO;hGEjCiAq z$zt1_lbD*TW$G8VtxfC<3{?>l5+b~Yc#O8Mmp(qnk01BEs|angt*x!5W*kKiG}4iQ zfe?Okx@Cz&1&edDv*$H3^id|Fms0?~#5;E)UcA_Q=`xNZ%0y&81VPJ;>a!OOeXZw) zWbpEUyfIR~zw|9+KBlFmefl(o?Kf)UWMi{{f9knSYp6R()l#v`{6Q!4MLc#ZTPH9_ zRYe6=+YUd@`Z_C4Efi^YlV}Z_Ub{as*07clV+D+XHBZ!e$rNBvM+YSkHRRd|1vgw) z@C7|c(7CA<%Sp6p>HoHo67lRQvB;nxek?l7Reg@y>yjyf_i*2kZw?(gq|S{5kR#xc z?uZx!WD4Ee^l*{e7M)_-*6NkGVpq=_zdJsTVpGF=0xi0<6rmVh6DWgV2Dl547P8jaBCsD19R>n+5g>l}m>2}Aurp7C;9IvC`y7bJe4(t!2(k0{M7lB(MCah( z@XhyB)a%!mR}eYK_-=Mb`8$hb7seB-tdl2)42T|U;00fnD<3{V!PgIKg+N^SlD4J& z1j%iC_%KMLa3@hZ7VI}&7;VCN0CEP#?8ri=;7xV)6&S_fBgix+z9K3=X#U^!+@12o z%mx79WbIdcgT7XGcY#VgvXZCcHJNzeZF-L*xP3;-@6K|m7XpaKE`#zRh7%9|>wtdxqr zQI+D{k4xml<^$|=xIePCUep$4oU#i`;RatKJU=;v5MC4NpH1#qT|oY#WC5(fMp0g0 z^FT8vxfi*kbp7}5U-x8V@&KG`$UVt_7Bua^h6veDQBhI?+me%!1q<3ffU9D3>+q>l zr*QY4J{5;WZ}uJ{T~tN-3T!3p&JJE7j7|32HJq7sG_E)6&F?z zV9^KR?c;Nvg9AZp4jo`Q;e3%7^nhi4qZ9#Ep@EqSbab0A7^~mDP5D;wn2+7!IQoOU zgle!;z^Zf?I0lR^0)ro+ql-lKhHf%SO3bYCI2xVHYW4UBI(x-jg@O9e3XZyok@4w# zxCcHgLM-~a(DR90i|*qd^C(fXZUI)-1n@V7j`Q(-FeR(v@(Yw+A@Z1NOv=^c4oHza z>?;z%>u_>T!@+{&ztUI}6yMg}UGVW^4csv}pF%>;s1QtBj5sVfwa{aTQwz9-+l;DL zE7vLx&mm-p1?_|05JYEc@0)NLVgAz4>baU#@8+$Ysy#|E*AAW%UB?Nou*%5Gb3$6y zK2GJ}=m@xwdM6*T6kkh{DOayt8EA@;LiL0%6c!4yva6eGzfz4-NTL%}61sl=j1qm+ z)!Vzdxv_>zMX`ydMEayOA{T;G7!h$tRaI41wgxdVIVFWvG13&{9cXoM$Ix#HXn%BS zdNRpoPcx?u&c-V`kx;Az+DH*>$eFJ7UsnK?0A~>}7|sz+Vnyt320{=s;w{n{YhPmPGq*+1jbC*20^?aMIbW3l72}@2xa-; z5&r&s0lG_u^(T%V4MJ<@%G#PQ$~Qpp3I+U2<_PU2xcf<(12uSViNG?4dO0SUEs>ZXE>uREz?n^7Z!>vH{`-ChFZbF z$*HZq9dGolwbiz_z;Vd&wuHnkM@k>;N}PWr4hacnj9j9pe~0lwE0`SJ{^{%O!W?Wj zl70JNJOnnN-%0>By(4NfEV5s0$g0bro? zKv9yB_5c2T?bQ=+V#40Bb0C_N2b~t!Hs56w6bMH;9;c-RJ&B?&9~Hf(W>;C+(@~qE_sPX<8)Dap z83vf2Z}u*%ucuGPp^QEo*k$5?A^~Irt`aUTIuR$E{uEgA$}1}|#fBG&jgN*wb3@fIPoeTRGc~0mBctInMOA=#UjmQ){gu&?i7h!}$N+E&L<5q< zSwywEyqpP^yYuIJrk$P7M?6`O3q}gXlA{vO%ghY87L%KwKe(W-p@9y40YO2)MeHD~ zCCBaT17h3`GyQ1mgXa;0iO_cfRHnlef`*k%^{=QmENe9j94@#@XloaPBV=X@Xe z1v{iO3ar$z5D;HL`mcZ)(c_NovBy2#ysyZ7!?xCKzwnBwKEAYtEH}yP>gr&7%?5qM z7Cjw~))WK+q`>1vnV9G~b9HR5a1_t3l|||PKd4#H+noQy z5OwtbM3(2P*CFcN|B!a` zi;KgXnNzz^D`PYTs%sm;o~o*<@88kGCAs zL5G`)V)E5Vp>Io>(Wc$#eJ?0*K`;=zn`jE22D=`~4wiAGZj71`c3xs%PCa!^N(xsV zaV-Wh1<17h$B*u!N6y&A@K~P~jcs7Lu%3AVmf#BWhIMysiJ7G(oZy3`q{e1uxY`xh z8b%NyTwF8#rS6D=RQuwRlk33LV4?f0H;AnRetu7SCmZH{1Vb>#4RH!72b?U}Vg%b9 zWF1jaVp~Iljpw!*_jCJDQ6S1yJPOD65a7Wc)fC>urJABuTTDL@mTW!aMx!jG0IbA$ zF|ku?3$iLI0r~m)>FF2#2AN}s4LJEsgdDUz96HA1p`xp~|D4?DUp56PT$NpC&m6j@xqM(;|kTT-A^0`0-;w(GHj9vqHDbc7yHB z8I^c^0GIU@CG^R2Rgz~;VqgGvKRc|_=>NYx;t?PC=1rEt_haiS&PW*(V`B_-F8_t_ zrq1<5`0x1n^EMbeRK+zlXZiWRv}YZmqC(A74&a0Y+}*v7rmUSWWApO5zJ8U&>5!7D zEH5Xvxjk+MKshV^AwJ#!y(hQ>L$~302iJU+M8st!t+22MY|Es^-n}m%{@^m`2?D2= zW(TE!p`RF$VlEM)EjVykLGcIN|F4_qv;vaFZvR(|zz@-gwh9C6?7jbug?D-cF!KX_ zZAhRXCyjZIMJA&jM&XZv2Gw{xCralaJ;*|?en9y!23K(pS$V(9&Tjku{kpbxROvl- zt(=*a*};kws5clda)IV0K4(5g1bLnesLuNQSp;cONr~L&8c1Y#QxU9kstep%>qm7;^HEZvF9+eX%t|XdY(;UXsG73YkR~$fRshk z6wUxx4}3)JRi@gZ`y`;XC@3;42E+^Khet*#@94t7+?ivkhiVt}orRE~<*!{$uX3tp z(2#(W6w@*WN3+Y~_>qCW*4D+f>0%gs(OG%?co^L2MgA#3sxt#utsI+-vM`fEOuHpV zzwLifQ_?mx^-?1anBfVQW?yUdO{;07MF1+$UHlCpbdT93raTFnbSQtDQW$*ULCyp- zEY_nsDFvVyG7rMJ2;et%A;f_-)QvdGgZU`9umcdd_mh%hG8O5eLq(Q@dz!{fyL0@i z_31=ih+`6vI3T#Nr1IwF=cB6xI1{eed^nkHEG@?-CWhqIA3Z9B{)V1jY3VA&F8j$Y z$!uyXPkp>FfE9RnQWBEa{{D>1X&YWU7q<#&pSj&wfo#Zs5aX>*#EOP5>(kwT({D#v>@`OHq+) zOZ;`5Zjj)R)36H!(7mI?_Na$36C3^8w`)KKIP(aWzQ=g?tyNZ5fm_!wqiVZ8EhKp7AQ4zCr?g}j^@BL54uB#7V!6MV5q~A}P$2;UvVBjAi?<-{%uH87$-hFZx~l037=V)c z{=L=ax+4(H#KeTQ+s2~zxDI$Nd;2e_a&Osqa_Mg0BI(0?d{~aK6Yrqy=i)`my+bk0S>wxJ)fni}u z(b1(M{&cZ^d081|gkUlXXw~%Hf!}ky21bZqQY57N_W8@9?!h$RKMqa1 z7*MX&mzP`jmu#QN#Ox`|cz~{hbi?tbOV+=UAt+f+pkl)! zVuqBKv^1LXeL=KJ)1WSWCeA=q9l-RYuOSw|%5G$4_R7+S1AM%nR`v-YfV0d^p z8qYv&c)ej?b|{aGLRF_eSe=L<14a{7+4$m`-=M>Q0=^Yd`rhskWWGH zOM83y1C^GpsKd-V)C$Nq094pUvjr&a^?@x2kA_iHf@anG_Hyi2wgU}D1y)pGo7dK! z1L2S0gIW?X^UpG2_tYZD>D3xr(3jaUL%)AFkJ-E*92^;m0=6zmB1A_sM4#^O%moS> zh=GKr&z*kVqxL*En$|?o-vh1&aK8GarJY@Nd_2nl6Fa*+SYBKA{UUGufs*OB$w*vL z#a0Ht;jnb#LgdZ4HNl{p#N_nC4t@3%ndF?5g(#VE=$b-9vZ;;W|$vCLpk zLaD5u=j}dyE&7JXmqG5+UE-T#h_$QKEyo@h0N_y?%j z*xRcReFN-%mHnDmY<9L9(7=S^37*kFm>Hmx0#2%t~*f28%YNO(692i z)@A)dFCkMtf8SGLm^|FSLA^_z9FXQa2L^Y!p^Fn6A8;+@_7_<>xi48+ka)72X+fd~ z#o2Hp*dT}@hFpDqpt`2U%HH1m!2>V@56sLIWMpU||6+qic5cm9o&s}+Hw1((EIfI; zYPH4~6tqsfIEk%|4IWMHDw$cvd*V;>PAV1-mTbrK>28^~Xn%cq%{mOkBjB!HPVaq7 zLSiV%*)uiCxcefA1VI#loU(f^3FQQoR@2-Qsh~j3P0OuMvIKJm{0gspeOZ4VSRJR> z-d~)=HZwQ3)OrM?<;1wS(!lT|k>mJkImvVEB{|K^j1{gH1-#PkS_jktVUL52D#iQO za`i81BDL+#p? zD|ozE18fB7mSs767V{nDdVQDTY~8QLfH72Ndjov`@;Lu1OjY&pze+>l_5dyZh64`H%*F=@D6GZ#1;H5 z8&wUiy6Sc-TpC2C^W*VwZlq%cMMb3YBiV&G&Zs<*im{7<48)&fcvlFIF$JAyZmk8v zGwvm@*wDniB=iMf*G#sywVp$#^zXqzrjbwDI)}(I4xnHHt%Mp}>ea#Ng5yYD$Zd{7 z3I+x&L9>y`2}`zfqjDx)QPmSiZMk>4G=av3Adb)5TkN*gZWjpryTK8>qr^9n?Ose7 zXiMm|)oZyR)3*S8ff2wZ&t0gxOYBw_HzLk=3kkuUe}UCfRK$5*f3J_mKD3AS6uT9_ ze~*IJ6+3(9gc-gCn*%1zV*(nEEck*KLFvT`R2>B}Z6QbpDCCcmlMnRw6RR0e%~)OJ z0gO(kSmVER+78JAMJR?hLv`ufB;H~_0AVDke-hE(Mc z31arYqX{257QT`}_d3cxpaqAnkMG|rYii1}?dzytK&=c&tb5`1f8|Pl`K#Q#*$O@p z*`B!4py`k*@m>yzLTKQZlQZepj1La(02%7?=n>FKI27yS$DP61J&}7EaQ8kNJ9`0U z>YXBL8n02Xp%WPEA^K>Qih-Gf!_2^d2+DwT(I^fsB&uVr8h$BkN+)b?gaJ&~0jSyX z4_#^v*=SZU=;y@9BT6bNEwr5!m^1=wA^XJYH;ru15SWnMtRGb3* zUV!ocmCZAym%*)D#NJq3ge?Rd)F@ilvGH@|*>aiAoT0@$`H_(?&dXET*?houh`FZx zzpcFG0pY)Vu}5}#&2SZ=62^cFCk(o>j4_fB){zx3G1wTi2fl&h^jOrZ7fds1V#Htu zmmhDh#eigY!5zZ8RO}HHjDWq3I6JV6Y8s8fgM%2e7+UJS7uCh;%F1gopRdOJbBl{8 z-D9Jov>^w2cvJz`V^P!3h-2V@_YwgV8`1%W{)1$?SE$oZF&N89ixH;A80&2M%XgTWGn!9uDE3TA`fe0H+8p+?AvxQ5#UNQsgG`Nwok zZd-ptxx4Qn9A4LkzOX;z((Mg0wWcC&Jms;>NEu(hl0|Obf{`lZb z1aZ;^$cNVeb8f7Ci_JSXJESpV3~oZ~g(9=T<-=D|8h{_denSa(2?@=$CeU}fUE8e? z>^O#&PDE_#teCk`QlMRxYLgaOx;)W6Ho>xNz1jzl=k&VF)IVJ4(s|d0F3|o+*kX5+_LGo6 zHC|~>KpeqXmySOv%hP==Mp;NM#SF7U;vKm5JL(!4t!&gzratJ$_JBkraT8|Kii)8q*~c(}5(nN){sP;+*7tfpkb(Kp zzc8C=u($Q8oap0W+9E-!LkdQK6T5sD(^*#vPmei&;KDfrh(i(3gwdrW4FLIhd4i^b z8;6FM&L3NWegW+qXUCTlG7F1BxCa^0mH^&U{`&P(B2HPD8kYJ>3}PT0L)zyV)7V@N zLSaMUhbRnhUv!fZ`sx|`3T?(Ha-9q9r|EpIInVTOk50n^u0e4ki6(-d|HU{=S$q>9 zc5-?e*!UwjO;ghXP+b@)p3U?6;21u@f2AfF*}YKqKtsi6JFrI&=N%VZxrxdT8RY;8 z3D-ZR2+}8JComm<<_DYh<%=m?exgcM~%d|aG{x_W;F z2GhLqO;r?u#~W$Z3`53pt`MJAvIS(ehH!V4PR#X0*ws#l|H|wxE&mV3l8+g6s?HW* z6lw!V<#5>r#1Y5<{lP#+UNn3K@@5lN09CRywt}u*}^Z=+YMeiw?so76ADLh2Vc{04FDkp;&Ef>J@GQ0SB zOVRv#j|-a!gH@kMR>sLLOKO{3Ma{EP*nMu>hzdNTsPAxZa2F-MpK>U#eAo?&9J3s8 zi|XsaV%ZlKC8cugXivh;26Y1V6=w$_($70JRU#ugH8mf-0n^^twiJDp;DCW08jb_7$YkQ+V<7G# zM_u6N7AGMp^>Bw_gE7F0&&j|rFg$#kgM$vu23QD?(0~fhoiFq0d& z7O5O2f#H{8;B$+LdNFkc_N4#8%z`)pGmD)J${L&5#@@a%A_ouyy%or&@Cm{7jMxNl z(Q1+bSpm=zLz%e2He)>int<5QUUFVNtsmGPL(x$NNNhdZEgheoZHM#@s(~ngJbqjP zT#T2+tD&4ht%@-h7rD7-*cjn!+}v;`G-98_bU=j6IE6U>>#KiK)tMksVi#gd!bFcM z3a$YRDlB&S%T2cgBQQ9Y5pb=jTp>VN8ZbkdOH8b8X|aa<1qwsmKWB%9tnXJ{h-`QX z%=%Kfc8vv7VQXta%huM_VFdf`J$rs7zSHr7sv4+svfAw-@n%d=tl0W7Dv9#LH9^u;Rd7t zcnvUx3PUj^*}X6hG)lx7yx<)L1r9u_3D2|w3~6|jt2*|irMue{Uisu?Q}h{_@21ZL zvxhzoty}2XAmP2=fAAE84a!aMcaZ{?@8Kz+rzaLgV4=dN$CJEE?2!a;vvmy(A#+); z%;LB3Q7)t zV3iM^0j-1Rs}RcLX*8y_`W8exbmjb<9QjD$zaaA9)IyA;1^<}p6PKD-*;%rdq9o7K zf^7>n1wbnh_DR)raM-`%s1-DC-v-_n!)|mnk=7PTgH#BZ>Fr&M^U6lGZ>kY46x?cD z09uqH-n<#Yvsj=P^ZQwtb0a)LLvCvF`>K0V*rD`IQ5a6G}zVP zQ-1qa?cTk2uU;wG+w*&W-FNUH%7I~*k0~i=IKtE?47h@aMcei7GiP0}CW)%!cmopq z8#l--0TS~Oe>)yxo((MbwC>;uA|vm@d^kXye=;`a*#n`oLIwYUwhfg=q6)?QF&Y{& zQD9t92B=clscRb=8Y(Jw8NU6hYinnRpPGWUzqlQ1h$nLakq{byxG?O$L^mDACuAcc zC9;947d~tm8PeMa;5HzJ*zs{j7my$%O~4AKn5JV&A>=s6JYbD1Brc2eqIz}_Md?aL zdO9ZYZ{R>n2y))Z`*BMnCqMts=qOQQI-Lu}r6?9LQ7TUPAB)`M$J@|-f^>%N0vO;` zRWYmzxdGT-GKD>k5bW=l-_l*d z5q2+yA9IYi7_E1=3!QlFQV~-aGhf3L|gONTF~QWUzZ_ z$#;U*t{hpt5X{^qw@0tu2nx(X*~}yR7#R7!Bqf>PaX?s0clVusk2sDzKERO=s8WU} zC`kendLJvor$@C-4wiGLdufjIGRN`>zkHGto;XyNUp{{Hl8wUA6yqF!KhS~`1Jv9ZzIB%x9Sw}=rP{OBA3f;bdze|Fa%r3MLQE29xaw^i0gYf2MoCEJAU6% zQNU_Q+RAbP{h@tLAn1|YVTSMb|xIj~NHR>VP@#VOju?*gw1-7Y3Bq3(m zs&cJzG%xqtlTfL_<@t7WdH_QS&4+b%KD(2j4o_pc_Zbla2F`(|{TF4x(T1)7m}sFf zYy67X?-$e7(gN<$-%qaYHPglDm%oYQukj=S7@OuF!d!!jWCKhz%ndAhnE>eAR?5uF z@HKT*zpIWne%I6tSc2H8tfGSUMWntQtto_M7?y(5O*~H%r5Zd5qazIzKsc>QoFPGN zgR#1Q)&Wz%Iziq*V28X0SOxhKtQoXOuzq8Y&KbNBMNI(*aBM80b{Kzb-qrH)#*lij zxa9HkOJ64DuALL`BQ_>;C%{4DP0HUntYPQ(7AO`E)l5PY2Pn4|mTHb|6XG2H21^gG zT;g=`({8dZC0sFW{zzgRBrGzRL3u_Ob95Id!nkzGKt4d?WO#TTY0d96V>GjKZ+KST zvq){H1UyCE*wWu2UHY6eg~`s&(_Y3}ohKtRGPv=KiY0M-`(-FV5Q~|ZnCz;W%}D*! zc6!5O@$<6@aTXt{vY=WtX+#Gw>uYo4#gVeH_H_*nNB2Ej(7I?G@bbpU`|$RbPLIl=Ls_bpw~m9nZG!+(UT&9L_5|t3n60E*>N5zL%Amd1ZAK3BM&z z6mN|lDFJ~Mcv2F`&k0eZsSZO!uR}KjtOl8J?&0|3a5QW17&Q_N6PUdA9izw@7@nw= zGNxa`NN|=^&%*&8KZJ-ZCOSPGjwj>(Q&3BLQ;;yAuE#^ZJc(s^aFHJX4r~E%TAP}h zLh(A?D}nqGC#KWaawN5WC>oTDnHe1|?U{5sQ~Os=c#7y+1A?)gg4ep1ZiaD#KXoLG z3HU5Xr?8+(AbS6cg@8C<+^yR%o~6LKE*r2we=QKA{0| zLJ$NSQMb(kkVVc+sD_i0leG$+K4YIgeOd+#i{a(Q@gQ_PB@F7u;)n69D=(Rg&evBT zqUkGN=RWN^u-MqOo>`$0EGcHu47Cu6JFe5Ob$h;1HomW@dFtKO(|ACvG5QF2 z%sX#Bad^z;M(Ss1RR=KzxReNTO-6Y-OM>Qnu%p{78-@l94 zj=xDsIe-2jlxR2?kt5>2X1#m&n3mDNS35;LU0h0P1S^ypX#nFA>{RH$;d&GVG1frP z291icviqEkH1~kt!M7+WDXpnBcmLZl89^F{pJ!%d#6C>;{5ep>!S(OleW0o?`ve~~ z)F(JXkXs}K_tiBrPM+kJLr4f2Q^l@nR(PEQe0KE6kuz*;X};2$9n7yH044K;8PQUF|SV zsQv&*hX^bv9r+L7F5kR~=f*&hab6skJZEE&6Iea~TL2#tBStl-vABBuig_ zNDcmbLFtrO>ZpY^j8hs3c2~btFqACJA}$%1){g#x|5C*&>X6$-ZYV#uAD$ zw(Pqw)+`uj*L5-6$jQorZRO>z0vJ+z z0Iwq%P$E;|w4`o92fh;5jb}->jsQ#%hyEGv&9CKD>_P5QjeI~J* zw=;6+Tr|V(%mL?i>GI{-I8%B*5cmF&!Jpd+gWQ14-@lVIS(J+EzI=iFumiaI@ye4~ z)GY+U_U2MzT%6kmi8?t8YIU(9DOvzst2US4WH(9GGs-Lj$w^7ZW@Z3i;8h82gg+4w z5&}t==`9)HJeHQO!(h=+2wzVR2UV8&_`qZ@!5Ixt*20$-HV=qdfgnsyO?80f4%inK zjFVIr#E<}VL-Ghj>LDixw5jPW40$t90$B=_KXAqX#SMGI0pu$yw&?K&e;{h0eFQxn zU6Moy0B}g%&&bR?g+M$5$P9~~29#hWC6E`4O5GG>3W134g&Q~e2m}&TK?R&^SZd8G zy)_KofB=1fh~@nL(CFv^$bl&$C2&1vH5mX;favYsOIUlYKxBgR_h#tKyBkyh0B%?I zLY5@Zg#G>f;PAgt9?}=u%qggb3KEgPdO(>?Fm_6uOHa@a4rd`k2gGZnpf2pV7eGvd zS_s0?1LCpp1yNBDKNhPm1_u=iYG>tBmSZW2NffkrP=h@yI~(W`FbAcukyBF@2YAPT zS!ii)2JFfe(g<}3_Tk||YAOVMMVuB z6TmZIiu)7_DgO$TO zxkUjbyP#r#4gz@KqbO2;Jw3@|o={Q?&>a*{hf1KPYHBU;tiWF{dY$cf)ol(Y7kC3e zvygTPd;@sX;|7@1THdw`7an*6FatyhdN4Rl=Cirey03tb4m2~vv11dfWbnn9=?+6Y z75qp;Lqn!=-V~C*{uoG0IWB3yx?rL^{DS@`{w}M@pLFz7Y;2&5Y9zh@`~l1iw3mi7 zUI>Ijz!ThRo;JS6c6RRDYqOAZO-=n8UN3x0qqc=rWmxp&4xm~vb`bc7s0N%u&L=xu zK)M4v^8nm2CRCpMxhP>11qi}LHTp6N;KRVSQCKcO13>VwoDTU=1-$_kH{#;q^jsP! z4VYztiE{v90+2UA*g&=$pklsN&+3?%0T7_+12`nU098$0eSI|Tf|QqBp@Ft`dUCRf zy**TNHaCA}51}hqV(>MSt7JI5V4QJg8or;99|4;TYCx1)Yk?329&BZ$^l&a*Jj|k^ zVgt-QEK86$NaQmY}+R+DTaaE z0SJ^(vP}}(pj|4IFMqUZjj*Z4D{V(NNF;GLqaNdl8IxsH6z+*pQH@ znSKw5EOrr7MdYRe%@X9rK+!=Q7DYRm z1;z=+|MvAOD0PAO1r23uti%#)fP)`o_3Z}5aHQ=QAFq1-og^m7g>c!<*| z2gg(eV#uIx2ipgB5N4UD;6rFJWsw6ohD8n}DlB}_3+mPRmlU8=~$P4jwSDLJ(31uZ5cCIEC93HaUc*0lo}^gm|a{ z0Aw93gY6!-WkDWqsF(xNKR6Rrg70kJYGDGumUiwDk4kA|o#> z`vg*XRveM~1$V8G{CjEF&kf726gRwi7v%6T8_v$3-oIzAl7;k*0?~K(A^HGw3FSFJ ze5o!|;3faph!A9dv^6)QpFcNgvpY=X*$O2nqCj$oYX;JKlG3CtL`IqLr%Y1e78RP3 zzma4>_a}|Ri;~xI?oV@w!<3$+p7N9sB!W_jlIjqpzLI6{hB6YLV8tYhzYY zeU5wWrM-O&9BRiMo@K-BjEu^g^e&eqVY#eX>7w~iWG(pN?_Bh;MY)B4Qr9#%Vllt| z1Ec?9%R!3!{7kV)lOYW5O6^nm8O8SWh5$=h2U7B^I&{=sUHHThKeB2bSdZb24eE0; zcN%9^Xd<9wDEC=A$Sy$ISvbSYw<~Q)A{8chzvKT(%#v7Ysd;a3ZOZW{t z?cVF0>ec>~Qhy^}^1n2=6Fr2-tOS1_czTqHNR)_3?Cwt7Q&+e2ZPAC)=r1)g3)84Ad6b^PlMwP(Rt?%VKBMogSZ!{^-<4-peCcNj+edR$ zBzVo^KQZe}?|n=}th59&s-`<1j3$TkT0E6)y2?uDGpx4Wh}k@NQB?kRrFrNioww`z zd=9AykLjBxN@~<7&6ckFjfELx{`#|iCuS}(ozBd;R9^D8_<}TgY?D3Y?bfBcZ#T*e zrm<2_NW%P~sh}(1Kvi)#jnB1^_{}0-%3Qf_BUX=c-$ro|)AO?lxsfib`)@Oi=GkLs z6MIs0Iv5gyd-%%*=PXy?>3P+~rsb^T?WS~khJUm=&6x?=wN&^{-MMV4Zq#BL8Lrcd z)9D>;aUZyZXqWfm`1`CZuF$0S;&#TFoU~mNiwBm3B|_qryjO&{I`|p(*9lf~>u4@I zwyO0rt+G>lDci=D(+Z=J!N=$jWE>=r+BCTk1f@ z>K(?3p|4s@O|?#3ji0N}?dcm5H6!Ma3XPaVhHH04YIhla-kU+H2yqSx4}KeZhgfOc zQy8taJe`q}@|~K>6Z^4zT=s8X*&WeD_bHY9{^uX?6^7$60azJAFq2^5`r6BDGZxE* zesA_Hq6EAUrnQ}^T=a4HFN#Mu#M4zXwh3#l9`CC^roI@LkUnyC>fs3Xf#}QbRe>+J zbuPDKE!!>X1=ZG%a;vrEiS;%Rt4|#>*(GoF!_uOW30fZ6bK;j!YJ}o0e-dmBS_j_z((!lxqQMp*wr`#AadKPu`UU==V zXYdW(55uF3`Hab}zUe0w=uoP50|F8m30G4JkIJ=nDt}gECvJrL8SsZeOXX7<&c(_n z3b|w(emT!^dB5{bk33PSuEcQz>6$wo_I-p#=7f2v-;uN(Y>hfn@8$I^Vg{xpAkIkt z`Np?IQe=qB!$-<=r!7a_!e~_myoRYdTnI2Aq&pW{eBOHqQB3NnX6xIRZw1 zWiU-T#qS0}!D9#=6Wlg$nVq&^Xl|RNMn`r`FPIPii2qQOKGl=wr&<5H@hz!$=+P!= z<7>LFlsDplvDG7>yo2~%r+AjGp?>#)IdS1K|L3(?W#s6OR-Ff7GTEcWdN?DA772+S zJ1bVVG9F)=!>|&&VmJA>Zgf5mHfStpkKk~P;Y}|pLgOWEVfVl9UzNY)4yQ1 zddYe&BX4B5M_F!t7DY!ETRL65KejKvV(ZvV3guBP;$js`WXnZ(OP2Ex(j%Z=U{r&1OD!hKkBS)K(tl%HBE1@OI`|6I=4Zp80CW z#e*}SgpD(G*$n(?MKcq-n;DPwND;*Qqs40T*`*)vtP4w{7f=efTPG}5p8V^d<+>%k zRw_PR`BZs(-@dQ-WVOH4-Pro<0h$W$`d@F5vFFrv?$UH8aE=N2coqUr=u`pqtW`$*uk&})bf z=d15~4elQc1M}jT8&hIBr2rU4$}bKKa_H!svdYrxxrR`%o{yC(m+lAaA#vEf2reO~oO6`i%%||?e&5Cp zu`=G$zrXP%F+`=B({T-r4mc;4;pwsX*sSi%w6W50JA29&`5o+X`Kj#jeYc6mfbvCk z{K}SZksooy>nOUQu&$tb%H;+s8#W*f) z%Z&A%!o@{vHtmbUUv}1hF0q;A6?_4W zGa&~uHa^%-Li0}fEsmZ{{Mq=Uq`L5PX35H8ea3oFi^M@?-F=wZ-9e0H$GP@+9e34!|zUdZq!ndLA!J>6NyYB8JF#fG6=e?uY=Zg|A zn~b8#q`@e;;h-FAcD*C7c53o5%#<~I`0oN0)tn@sZi~Ize(~{~qlCoD>cou#rGw3n zW8~NSK70}JeP5#(&hkWhR5)lVfev#Z--&g>U*2tyK9*%)>} zKOR=Ctln-54QPBIisr;U?7g0yb4mB*b(m3;pSN|$n-E`~7Vb?LWEP66Nm%{#37?;= zYoPU=aC?&)$GKl0-)~bP{b*R`BhSNKTmYl#86GiqCC1F8v(O zRk5*aosC*4B@G?&P;*Z?gtMvbbRb!YfA7`Wjc;Gkyg#OVm0{~ilqRXLSz=LEXi6t1 zOJIGb$E*vn^Lc`%IOVVc9j=*teLP^PjPpQt;i;8U9=Mo3g(EWzXGip5Z;lx>0Fln^ z9Vh1=vzHhTP+_bpnBhhn?_r@83Xn*>nFO!@i5C9&?!S@iHP{dZ&>gD7gnB9hr?upo z^c&em`Q$sAa!$%e|KJgap=Lb>5;lwShP*JyJpXOTS|qOIVEJsiLDdcM&h9j&Zr<&LATI8&9W# z)gN6e_h>Z+m>jFxp0f8LLksV_H51Ymt4){G#|u)gBwr4=%3`E4UOliE;e6=HbT(3vWUDlw1{T82|^ILY+bhd%8jTyd@R&@D4-w@@0rfiN8_{wFHOnPLU}U7uv=;=kj1 zw(qKO3GC<)LZ$+N_Jp1+iV- z{)A8@R?2-*s*v~3>|J_yc#39PZcSPxz#qC#R3E!Of9*aaX!fmGdT?Oq?69rJd iqvNFj&2!xc)J%L2Er(8&LCFFtsyj;Riunr0ul@)8knN8E literal 27002 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 From a4c3aaaf1d2b06a05d5c7bb4cba2a14bd26db775 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 10 Apr 2017 18:34:47 +0200 Subject: [PATCH 097/825] Update setuptools from 34.4.0 to 34.4.1 --- misc/requirements/requirements-pip.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-pip.txt b/misc/requirements/requirements-pip.txt index 90c042c8e..5bd2aeac8 100644 --- a/misc/requirements/requirements-pip.txt +++ b/misc/requirements/requirements-pip.txt @@ -3,6 +3,6 @@ appdirs==1.4.3 packaging==16.8 pyparsing==2.2.0 -setuptools==34.4.0 +setuptools==34.4.1 six==1.10.0 wheel==0.29.0 From 13c5150e58fa42b5957da551cf830a868d10c49e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 11 Apr 2017 21:19:36 +0200 Subject: [PATCH 098/825] Update docs --- CHANGELOG.asciidoc | 2 ++ README.asciidoc | 1 + doc/qutebrowser.1.asciidoc | 9 +++------ 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 81ad97343..00170f731 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -26,6 +26,8 @@ Added - New `-s` option for `:open` to force a HTTPS scheme. - `:debug-log-filter` now accepts `none` as an argument to clear any log filters. +- New `--debug-flag` argument which replaces `--debug-exit` and + `--pdb-postmortem`. Changed ~~~~~~~ diff --git a/README.asciidoc b/README.asciidoc index 96b795463..64e633067 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -186,6 +186,7 @@ Contributors, sorted by the number of commits in descending order: * John ShaggyTwoDope Jenkins * Clayton Craft * Peter Vilim +* Jacob Sword * knaggita * Oliver Caldwell * Julian Weigt diff --git a/doc/qutebrowser.1.asciidoc b/doc/qutebrowser.1.asciidoc index 36530bffe..cd95bf8e5 100644 --- a/doc/qutebrowser.1.asciidoc +++ b/doc/qutebrowser.1.asciidoc @@ -93,12 +93,6 @@ show it. *--nowindow*:: Don't show the main window. -*--debug-exit*:: - Turn on debugging of late exit. - -*--pdb-postmortem*:: - Drop into pdb on exceptions. - *--temp-basedir*:: Use a temporary basedir. @@ -110,6 +104,9 @@ show it. *--qt-flag* 'QT_FLAG':: Pass an argument to Qt as flag. + +*--debug-flag* 'DEBUG_FLAGS':: + Pass name of debugging feature to be turned on. // QUTE_OPTIONS_END == FILES From 20b17f3fb1cb9b82a874923b3193be44d5a4f8ee Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 11 Apr 2017 21:19:49 +0200 Subject: [PATCH 099/825] Improve --debug-flag error message --- qutebrowser/qutebrowser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index 3ce11b07b..4a73c1162 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -156,8 +156,8 @@ def debug_flag_error(flag): if flag in valid_flags: return flag else: - raise argparse.ArgumentTypeError("Invalid flag - valid flags include: " + - str(valid_flags)) + raise argparse.ArgumentTypeError("Invalid debug flag - valid flags: {}" + .format(', '.join(valid_flags))) def main(): From c47da15bb143f889ad703172194cb21cf6ae9de0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 11 Apr 2017 21:26:23 +0200 Subject: [PATCH 100/825] Remove nargs=1 for --debug-flag Otherwisse we get [['foo'], ['bar']] from argparse... --- qutebrowser/qutebrowser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py index 4a73c1162..cd9ec5938 100644 --- a/qutebrowser/qutebrowser.py +++ b/qutebrowser/qutebrowser.py @@ -116,7 +116,7 @@ def get_argparser(): nargs=1, action='append') debug.add_argument('--debug-flag', type=debug_flag_error, default=[], help="Pass name of debugging feature to be turned on.", - nargs=1, action='append', dest='debug_flags') + action='append', dest='debug_flags') parser.add_argument('command', nargs='*', help="Commands to execute on " "startup.", metavar=':command') # URLs will actually be in command From 75f8d2a1d1bf7375b11f29981b98617906618f1a Mon Sep 17 00:00:00 2001 From: Fritz Reichwald Date: Wed, 12 Apr 2017 06:52:26 +0200 Subject: [PATCH 101/825] Test if stdin gets closed when starting QProcess --- tests/end2end/data/userscripts/test_stdinclose.py | 7 +++++++ tests/end2end/features/spawn.feature | 5 +++++ 2 files changed, 12 insertions(+) create mode 100755 tests/end2end/data/userscripts/test_stdinclose.py diff --git a/tests/end2end/data/userscripts/test_stdinclose.py b/tests/end2end/data/userscripts/test_stdinclose.py new file mode 100755 index 000000000..f3f4e4545 --- /dev/null +++ b/tests/end2end/data/userscripts/test_stdinclose.py @@ -0,0 +1,7 @@ +#!/usr/bin/python3 + +import sys +import os +sys.stdin.read() +with open(os.environ['QUTE_FIFO'], 'wb') as fifo: + fifo.write(b':message-info "stdin closed"\n') diff --git a/tests/end2end/features/spawn.feature b/tests/end2end/features/spawn.feature index 8e3f88bd1..f5108b9e7 100644 --- a/tests/end2end/features/spawn.feature +++ b/tests/end2end/features/spawn.feature @@ -55,3 +55,8 @@ Feature: :spawn Then the following tabs should be open: - about:blank - about:blank (active) + + Scenario: Running :spawn with userscript that expects the stdin getting closed + When I open about:blank + And I run :spawn -u (testdata)/userscripts/test_stdinclose.py + Then "stdin closed" should be logged From bc7f8018c0bfada45f53cc885e6e1a21f4d677f7 Mon Sep 17 00:00:00 2001 From: Fritz Reichwald Date: Wed, 12 Apr 2017 06:56:38 +0200 Subject: [PATCH 102/825] Close stdin after starting QProcess Fixes 2491 --- qutebrowser/misc/guiprocess.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qutebrowser/misc/guiprocess.py b/qutebrowser/misc/guiprocess.py index eb0a036e5..16bbbb3ba 100644 --- a/qutebrowser/misc/guiprocess.py +++ b/qutebrowser/misc/guiprocess.py @@ -142,12 +142,14 @@ class GUIProcess(QObject): self._proc.start(cmd, args) else: self._proc.start(cmd, args, mode) + self._proc.closeWriteChannel(); def start_detached(self, cmd, args, cwd=None): """Convenience wrapper around QProcess::startDetached.""" log.procs.debug("Starting detached.") self._pre_start(cmd, args) ok, _pid = self._proc.startDetached(cmd, args, cwd) + self._proc.closeWriteChannel(); if ok: log.procs.debug("Process started.") From 424d0aec5a403cbf23557ea878ef0b1695c21148 Mon Sep 17 00:00:00 2001 From: Fritz Reichwald Date: Wed, 12 Apr 2017 07:31:24 +0200 Subject: [PATCH 103/825] change test_stdinclose.py to stdinclose.py --- .../data/userscripts/{test_stdinclose.py => stdinclose.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/end2end/data/userscripts/{test_stdinclose.py => stdinclose.py} (100%) diff --git a/tests/end2end/data/userscripts/test_stdinclose.py b/tests/end2end/data/userscripts/stdinclose.py similarity index 100% rename from tests/end2end/data/userscripts/test_stdinclose.py rename to tests/end2end/data/userscripts/stdinclose.py From 3d549bf60709a539bf1ddb4cc3fa0c53c811c631 Mon Sep 17 00:00:00 2001 From: Fritz Reichwald Date: Wed, 12 Apr 2017 07:32:12 +0200 Subject: [PATCH 104/825] Remove closeWriteChannel from detached start --- qutebrowser/misc/guiprocess.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qutebrowser/misc/guiprocess.py b/qutebrowser/misc/guiprocess.py index 16bbbb3ba..8f986d1b1 100644 --- a/qutebrowser/misc/guiprocess.py +++ b/qutebrowser/misc/guiprocess.py @@ -142,14 +142,13 @@ class GUIProcess(QObject): self._proc.start(cmd, args) else: self._proc.start(cmd, args, mode) - self._proc.closeWriteChannel(); + self._proc.closeWriteChannel() def start_detached(self, cmd, args, cwd=None): """Convenience wrapper around QProcess::startDetached.""" log.procs.debug("Starting detached.") self._pre_start(cmd, args) ok, _pid = self._proc.startDetached(cmd, args, cwd) - self._proc.closeWriteChannel(); if ok: log.procs.debug("Process started.") From 590a9b4f7833f414ba397c5482c770e3fbab3642 Mon Sep 17 00:00:00 2001 From: Fritz Reichwald Date: Wed, 12 Apr 2017 07:32:40 +0200 Subject: [PATCH 105/825] Indent with spaces and minor changes --- tests/end2end/features/spawn.feature | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/end2end/features/spawn.feature b/tests/end2end/features/spawn.feature index f5108b9e7..3d69cdd9a 100644 --- a/tests/end2end/features/spawn.feature +++ b/tests/end2end/features/spawn.feature @@ -57,6 +57,5 @@ Feature: :spawn - about:blank (active) Scenario: Running :spawn with userscript that expects the stdin getting closed - When I open about:blank - And I run :spawn -u (testdata)/userscripts/test_stdinclose.py - Then "stdin closed" should be logged + When I run :spawn -u (testdata)/userscripts/stdinclose.py + Then the message "stdin closed" should be shown From b784ddedddc6503bd79f5e4d376d322386bbb839 Mon Sep 17 00:00:00 2001 From: Fritz Reichwald Date: Wed, 12 Apr 2017 07:40:11 +0200 Subject: [PATCH 106/825] Also test stdin close for detached start --- tests/end2end/features/spawn.feature | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/end2end/features/spawn.feature b/tests/end2end/features/spawn.feature index 3d69cdd9a..bea671f32 100644 --- a/tests/end2end/features/spawn.feature +++ b/tests/end2end/features/spawn.feature @@ -59,3 +59,7 @@ Feature: :spawn Scenario: Running :spawn with userscript that expects the stdin getting closed When I run :spawn -u (testdata)/userscripts/stdinclose.py Then the message "stdin closed" should be shown + + Scenario: Running :spawn -d with userscript that expects the stdin getting closed + When I run :spawn -d -u (testdata)/userscripts/stdinclose.py + Then the message "stdin closed" should be shown From c38dc95c23235a464051a06287bcf95d9f17971d Mon Sep 17 00:00:00 2001 From: Fritz Reichwald Date: Wed, 12 Apr 2017 07:59:40 +0200 Subject: [PATCH 107/825] Add posix to stdin test beacause the py script fails on windows --- tests/end2end/features/spawn.feature | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/end2end/features/spawn.feature b/tests/end2end/features/spawn.feature index bea671f32..dc0485391 100644 --- a/tests/end2end/features/spawn.feature +++ b/tests/end2end/features/spawn.feature @@ -56,10 +56,12 @@ Feature: :spawn - about:blank - about:blank (active) + @posix Scenario: Running :spawn with userscript that expects the stdin getting closed When I run :spawn -u (testdata)/userscripts/stdinclose.py Then the message "stdin closed" should be shown - + + @posix Scenario: Running :spawn -d with userscript that expects the stdin getting closed When I run :spawn -d -u (testdata)/userscripts/stdinclose.py Then the message "stdin closed" should be shown From ff767dd9659812c83032f1b1d062b98b75119887 Mon Sep 17 00:00:00 2001 From: Fritz Reichwald Date: Wed, 12 Apr 2017 08:47:39 +0200 Subject: [PATCH 108/825] Add neccessary metadata to py script --- tests/end2end/data/userscripts/stdinclose.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/end2end/data/userscripts/stdinclose.py b/tests/end2end/data/userscripts/stdinclose.py index f3f4e4545..d1397d18c 100755 --- a/tests/end2end/data/userscripts/stdinclose.py +++ b/tests/end2end/data/userscripts/stdinclose.py @@ -1,4 +1,22 @@ #!/usr/bin/python3 +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2015-2017 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 . import sys import os From b00c1dc9060c27e23c7d42ca2d8b1300a510d708 Mon Sep 17 00:00:00 2001 From: Fritz Reichwald Date: Wed, 12 Apr 2017 09:23:29 +0200 Subject: [PATCH 109/825] Add docstring --- tests/end2end/data/userscripts/stdinclose.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/end2end/data/userscripts/stdinclose.py b/tests/end2end/data/userscripts/stdinclose.py index d1397d18c..9b2ade1c0 100755 --- a/tests/end2end/data/userscripts/stdinclose.py +++ b/tests/end2end/data/userscripts/stdinclose.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: -# Copyright 2015-2017 Florian Bruhin (The Compiler) +# Copyright 2017 Florian Bruhin (The Compiler) # # This file is part of qutebrowser. # @@ -18,6 +18,8 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . +"""A userscript to check if the stdin gets closed""" + import sys import os sys.stdin.read() From 68c655bd9c62d2a1da4d0294c3cdf6130ff561e8 Mon Sep 17 00:00:00 2001 From: Fritz Reichwald Date: Wed, 12 Apr 2017 10:21:03 +0200 Subject: [PATCH 110/825] Add period at end of docstring to make flake happy --- tests/end2end/data/userscripts/stdinclose.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/end2end/data/userscripts/stdinclose.py b/tests/end2end/data/userscripts/stdinclose.py index 9b2ade1c0..fa0676f73 100755 --- a/tests/end2end/data/userscripts/stdinclose.py +++ b/tests/end2end/data/userscripts/stdinclose.py @@ -18,7 +18,7 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -"""A userscript to check if the stdin gets closed""" +"""A userscript to check if the stdin gets closed.""" import sys import os From fdaff02a582285c991bfaeb5b1d8671d9956015e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 12 Apr 2017 12:43:38 +0200 Subject: [PATCH 111/825] Update docs --- CHANGELOG.asciidoc | 1 + README.asciidoc | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 00170f731..9b7b809c9 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -43,6 +43,7 @@ Changed options instead of being before sections. - The HTTP cache is disabled with QtWebKit on Qt 5.8 now as it leads to frequent crashes due to a Qt bug. +- stdin is now closed immediately for processes spawned from qutebrowser Fixed ~~~~~ diff --git a/README.asciidoc b/README.asciidoc index 64e633067..f79812850 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -164,6 +164,7 @@ Contributors, sorted by the number of commits in descending order: * Patric Schmitz * Tarcisio Fedrizzi * Claude +* Fritz Reichwald * Corentin Julé * meles5 * Philipp Hansch @@ -172,7 +173,6 @@ Contributors, sorted by the number of commits in descending order: * Nathan Isom * Thorsten Wißmann * Austin Anderson -* Fritz Reichwald * Jimmy * Niklas Haas * Maciej Wołczyk From 4a480e6f5fd565f7186b3f083c4ff1737341f378 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 12 Apr 2017 13:24:10 +0200 Subject: [PATCH 112/825] Ignore Chromium NETLINK message --- tests/end2end/fixtures/quteprocess.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py index 15157cc78..80643b131 100644 --- a/tests/end2end/fixtures/quteprocess.py +++ b/tests/end2end/fixtures/quteprocess.py @@ -79,6 +79,8 @@ def is_ignored_lowlevel_message(message): return True elif message == 'getrlimit(RLIMIT_NOFILE) failed': return True + elif message == 'Could not bind NETLINK socket: Address already in use': + return True return False From 4511d042a1b0dc2ec3174716da8696dd6a87202c Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 13 Apr 2017 17:30:03 +0200 Subject: [PATCH 113/825] Update astroid from 1.4.9 to 1.5.1 --- misc/requirements/requirements-pylint.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index 12d94c8eb..6e311ca44 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -1,6 +1,6 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -astroid==1.4.9 +astroid==1.5.1 github3.py==0.9.6 isort==4.2.5 lazy-object-proxy==1.2.2 From 10b1c954b2137e6a5b7581b50795b023942ff142 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Thu, 13 Apr 2017 17:30:12 +0200 Subject: [PATCH 114/825] Update pylint from 1.6.5 to 1.7.0 --- misc/requirements/requirements-pylint.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index 12d94c8eb..615eeb18e 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -5,7 +5,7 @@ github3.py==0.9.6 isort==4.2.5 lazy-object-proxy==1.2.2 mccabe==0.6.1 -pylint==1.6.5 +pylint==1.7.0 ./scripts/dev/pylint_checkers requests==2.13.0 uritemplate==3.0.0 From 1d0f187fab71a8817d2a68e94788b1c9e80462a7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 13 Apr 2017 18:00:36 +0200 Subject: [PATCH 115/825] Adjustments for new pylint version --- qutebrowser/browser/commands.py | 6 +++--- qutebrowser/browser/downloads.py | 2 +- qutebrowser/browser/webengine/webenginedownloads.py | 3 ++- qutebrowser/browser/webengine/webengineelem.py | 2 +- qutebrowser/browser/webengine/webenginetab.py | 2 +- qutebrowser/browser/webkit/rfc6266.py | 3 --- qutebrowser/commands/argparser.py | 8 ++++---- qutebrowser/completion/models/base.py | 2 +- qutebrowser/config/config.py | 6 ++---- qutebrowser/mainwindow/prompt.py | 2 +- qutebrowser/misc/sessions.py | 6 ++---- qutebrowser/utils/message.py | 2 +- scripts/dev/run_pylint_on_tests.py | 6 ++++-- tests/conftest.py | 4 ++-- tests/end2end/fixtures/quteprocess.py | 3 ++- tests/helpers/stubs.py | 9 --------- tests/helpers/utils.py | 2 -- tests/unit/completion/test_completer.py | 1 - 18 files changed, 27 insertions(+), 42 deletions(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 12f58d610..254c21126 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -634,7 +634,7 @@ class CommandDispatcher: scope='window') @cmdutils.argument('count', count=True) @cmdutils.argument('horizontal', flag='x') - def scroll_perc(self, perc: float=None, horizontal=False, count=None): + def scroll_perc(self, perc: float = None, horizontal=False, count=None): """Scroll to a specific percentage of the page. The percentage can be given either as argument or as count. @@ -670,7 +670,7 @@ class CommandDispatcher: @cmdutils.argument('bottom_navigate', metavar='ACTION', choices=('next', 'increment')) def scroll_page(self, x: float, y: float, *, - top_navigate: str=None, bottom_navigate: str=None, + top_navigate: str = None, bottom_navigate: str = None, count=1): """Scroll the frame page-wise. @@ -807,7 +807,7 @@ class CommandDispatcher: @cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.argument('count', count=True) - def zoom(self, zoom: int=None, count=None): + def zoom(self, zoom: int = None, count=None): """Set the zoom level for the current tab. The zoom can be given as argument or as [count]. If neither is diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index f9b246d3a..8182aabfd 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -952,7 +952,7 @@ class DownloadModel(QAbstractListModel): @cmdutils.register(instance='download-model', scope='window', maxsplit=0) @cmdutils.argument('count', count=True) - def download_open(self, cmdline: str=None, count=0): + def download_open(self, cmdline: str = None, count=0): """Open the last/[count]th download. If no specific command is given, this will use the system's default diff --git a/qutebrowser/browser/webengine/webenginedownloads.py b/qutebrowser/browser/webengine/webenginedownloads.py index 53cbb82c1..ca6e04c04 100644 --- a/qutebrowser/browser/webengine/webenginedownloads.py +++ b/qutebrowser/browser/webengine/webenginedownloads.py @@ -100,7 +100,8 @@ class DownloadItem(downloads.AbstractDownloadItem): def _get_open_filename(self): return self._filename - def _set_fileobj(self, fileobj): + def _set_fileobj(self, fileobj, *, + autoclose=True): # pylint: disable=unused-argument raise downloads.UnsupportedOperationError def _set_tempfile(self, fileobj): diff --git a/qutebrowser/browser/webengine/webengineelem.py b/qutebrowser/browser/webengine/webengineelem.py index 3e145468b..1147b8475 100644 --- a/qutebrowser/browser/webengine/webengineelem.py +++ b/qutebrowser/browser/webengine/webengineelem.py @@ -18,7 +18,7 @@ # along with qutebrowser. If not, see . # FIXME:qtwebengine remove this once the stubs are gone -# pylint: disable=unused-variable +# pylint: disable=unused-argument """QtWebEngine specific part of the web element API.""" diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 7eef0b03a..821b73a1a 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -18,7 +18,7 @@ # along with qutebrowser. If not, see . # FIXME:qtwebengine remove this once the stubs are gone -# pylint: disable=unused-variable +# pylint: disable=unused-argument """Wrapper over a QWebEngineView.""" diff --git a/qutebrowser/browser/webkit/rfc6266.py b/qutebrowser/browser/webkit/rfc6266.py index 89f4c78c6..11a1d76a1 100644 --- a/qutebrowser/browser/webkit/rfc6266.py +++ b/qutebrowser/browser/webkit/rfc6266.py @@ -286,9 +286,6 @@ def normalize_ws(text): def parse_headers(content_disposition): """Build a _ContentDisposition from header values.""" - # WORKAROUND for https://bitbucket.org/logilab/pylint/issue/492/ - # pylint: disable=no-member - # We allow non-ascii here (it will only be parsed inside of qdtext, and # rejected by the grammar if it appears in other places), although parsing # it can be ambiguous. Parsing it ensures that a non-ambiguous filename* diff --git a/qutebrowser/commands/argparser.py b/qutebrowser/commands/argparser.py index e4c6378bd..c0e08ccb6 100644 --- a/qutebrowser/commands/argparser.py +++ b/qutebrowser/commands/argparser.py @@ -76,11 +76,11 @@ class ArgumentParser(argparse.ArgumentParser): self.name = name super().__init__(*args, add_help=False, prog=name, **kwargs) - def exit(self, status=0, msg=None): - raise ArgumentParserExit(status, msg) + def exit(self, status=0, message=None): + raise ArgumentParserExit(status, message) - def error(self, msg): - raise ArgumentParserError(msg.capitalize()) + def error(self, message): + raise ArgumentParserError(message.capitalize()) def arg_name(name): diff --git a/qutebrowser/completion/models/base.py b/qutebrowser/completion/models/base.py index 88b06a4e0..1ee45af71 100644 --- a/qutebrowser/completion/models/base.py +++ b/qutebrowser/completion/models/base.py @@ -103,7 +103,7 @@ class BaseCompletionModel(QStandardItemModel): nameitem.setData(userdata, Role.userdata) return nameitem, descitem, miscitem - def delete_cur_item(self, win_id): + def delete_cur_item(self, completion): """Delete the selected item.""" raise NotImplementedError diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 82c6aca00..73aa2ae22 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -643,8 +643,7 @@ class ConfigManager(QObject): def _after_set(self, changed_sect, changed_opt): """Clean up caches and emit signals after an option has been set.""" - # WORKAROUND for https://bitbucket.org/logilab/pylint/issues/659/ - self.get.cache_clear() # pylint: disable=no-member + self.get.cache_clear() self._changed(changed_sect, changed_opt) # Options in the same section and ${optname} interpolation. for optname, option in self.sections[changed_sect].items(): @@ -715,8 +714,7 @@ class ConfigManager(QObject): existed = optname in sectdict if existed: sectdict.delete(optname) - # WORKAROUND for https://bitbucket.org/logilab/pylint/issues/659/ - self.get.cache_clear() # pylint: disable=no-member + self.get.cache_clear() return existed @functools.lru_cache() diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 69feab920..358bcc80b 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -387,7 +387,7 @@ class PromptContainer(QWidget): @cmdutils.register(instance='prompt-container', hide=True, scope='window', modes=[usertypes.KeyMode.prompt], maxsplit=0) - def prompt_open_download(self, cmdline: str=None): + def prompt_open_download(self, cmdline: str = None): """Immediately open a download. If no specific command is given, this will use the system's default diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index 6ad8358a6..8bf55016f 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -443,7 +443,7 @@ class SessionManager(QObject): @cmdutils.register(name=['session-save', 'w'], instance='session-manager') @cmdutils.argument('name', completion=usertypes.Completion.sessions) @cmdutils.argument('win_id', win_id=True) - def session_save(self, name: str=default, current=False, quiet=False, + def session_save(self, name: str = default, current=False, quiet=False, force=False, only_active_window=False, win_id=None): """Save a session. @@ -455,9 +455,7 @@ class SessionManager(QObject): force: Force saving internal sessions (starting with an underline). only_active_window: Saves only tabs of the currently active window. """ - if (name is not default and - name.startswith('_') and # pylint: disable=no-member - not force): + if name is not default and name.startswith('_') and not force: raise cmdexc.CommandError("{} is an internal session, use --force " "to save anyways.".format(name)) if current: diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py index bb758d78a..8f39e8174 100644 --- a/qutebrowser/utils/message.py +++ b/qutebrowser/utils/message.py @@ -18,7 +18,7 @@ # along with qutebrowser. If not, see . # Because every method needs to have a log_stack argument -# pylint: disable=unused-variable +# pylint: disable=unused-argument """Message singleton so we don't have to define unneeded signals.""" diff --git a/scripts/dev/run_pylint_on_tests.py b/scripts/dev/run_pylint_on_tests.py index 01dd14ad7..e4412708d 100644 --- a/scripts/dev/run_pylint_on_tests.py +++ b/scripts/dev/run_pylint_on_tests.py @@ -54,7 +54,8 @@ def main(): 'missing-docstring', 'protected-access', # https://bitbucket.org/logilab/pylint/issue/511/ - 'undefined-variable', + #'undefined-variable', + 'len-as-condition', # directories without __init__.py... 'import-error', ] @@ -66,7 +67,8 @@ def main(): no_docstring_rgx = ['^__.*__$', '^setup$'] args = (['--disable={}'.format(','.join(disabled)), - '--no-docstring-rgx=({})'.format('|'.join(no_docstring_rgx))] + + '--no-docstring-rgx=({})'.format('|'.join(no_docstring_rgx)), + '--ignored-modules=helpers,pytest'] + sys.argv[2:] + files) env = os.environ.copy() env['PYTHONPATH'] = os.pathsep.join(pythonpath) diff --git a/tests/conftest.py b/tests/conftest.py index fe2ca9042..53dbbb752 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -# pylint: disable=unused-import +# pylint: disable=unused-import,wildcard-import,unused-wildcard-import """The qutebrowser test suite conftest file.""" @@ -34,7 +34,7 @@ pytest.register_assert_rewrite('helpers') from helpers import logfail from helpers.logfail import fail_on_logging from helpers.messagemock import message_mock -from helpers.fixtures import * # pylint: disable=wildcard-import +from helpers.fixtures import * from qutebrowser.utils import qtutils diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py index 80643b131..cc835693c 100644 --- a/tests/end2end/fixtures/quteprocess.py +++ b/tests/end2end/fixtures/quteprocess.py @@ -356,7 +356,8 @@ class QuteProc(testprocess.Process): self.wait_for(category='webview', message='Scroll position changed to ' + point) - def wait_for(self, timeout=None, **kwargs): + def wait_for(self, timeout=None, # pylint: disable=arguments-differ + **kwargs): """Extend wait_for to add divisor if a test is xfailing.""" __tracebackhide__ = (lambda e: e.errisinstance(testprocess.WaitForTimeout)) diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index dfbcc550d..3e028b0c9 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -376,9 +376,6 @@ class InstaTimer(QObject): timeout = pyqtSignal() - def __init__(self, parent=None): - super().__init__(parent) - def start(self): self.timeout.emit() @@ -410,9 +407,6 @@ class StatusBarCommandStub(QLineEdit): show_cmd = pyqtSignal() hide_cmd = pyqtSignal() - def __init__(self, parent=None): - super().__init__(parent) - def prefix(self): return self.text()[0] @@ -594,6 +588,3 @@ class ApplicationStub(QObject): """Stub to insert as the app object in objreg.""" new_window = pyqtSignal(mainwindow.MainWindow) - - def __init__(self): - super().__init__() diff --git a/tests/helpers/utils.py b/tests/helpers/utils.py index faafefa82..52a843dbc 100644 --- a/tests/helpers/utils.py +++ b/tests/helpers/utils.py @@ -17,8 +17,6 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -# pylint: disable=unused-variable - """Partial comparison of dicts/lists.""" diff --git a/tests/unit/completion/test_completer.py b/tests/unit/completion/test_completer.py index a2b3bc7f0..3f0f021bf 100644 --- a/tests/unit/completion/test_completer.py +++ b/tests/unit/completion/test_completer.py @@ -113,7 +113,6 @@ def cmdutils_patch(monkeypatch, stubs): @cmdutils.argument('command', completion=usertypes.Completion.command) def bind(key, win_id, command=None, *, mode='normal', force=False): """docstring.""" - # pylint: disable=unused-variable pass def tab_detach(): From 7c4e4a58183017b898f228fc4b3d765925fbed89 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 13 Apr 2017 21:10:52 +0200 Subject: [PATCH 116/825] Adjust flake8 config Since we now ignore this on a per-file level for pylint, we need to do the same for flake8 too. --- .flake8 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index 7bfc34c0a..d967a505b 100644 --- a/.flake8 +++ b/.flake8 @@ -35,9 +35,9 @@ max-complexity = 12 putty-auto-ignore = True putty-ignore = /# pylint: disable=invalid-name/ : +N801,N806 - /# pylint: disable=wildcard-import/ : +F403 /# pragma: no mccabe/ : +C901 tests/*/test_*.py : +D100,D101,D401 + tests/conftest.py : +F403 tests/unit/browser/webkit/test_history.py : +N806 tests/helpers/fixtures.py : +N806 tests/unit/browser/webkit/http/test_content_disposition.py : +D400 From dd24039d64e72d4a79cda9ee3b7d0d0b19f146a4 Mon Sep 17 00:00:00 2001 From: Daniel Jakots Date: Fri, 14 Apr 2017 12:37:41 -0400 Subject: [PATCH 117/825] OpenBSD 6.1 is now released. Also prefer the package than the port. --- INSTALL.asciidoc | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/INSTALL.asciidoc b/INSTALL.asciidoc index 933859346..651082bee 100644 --- a/INSTALL.asciidoc +++ b/INSTALL.asciidoc @@ -222,19 +222,19 @@ On OpenBSD qutebrowser is in http://cvsweb.openbsd.org/cgi-bin/cvsweb/ports/www/qutebrowser/[OpenBSD ports]. -Manual install: +Install the package: + +---- +# pkg_add qutebrowser +---- + +Or alternatively, use the ports system : ---- # cd /usr/ports/www/qutebrowser # make install ---- -Or alternatively if you're using `-current` (or OpenBSD 6.1 once it's been released): - ----- -# pkg_add qutebrowser ----- - On Windows ---------- From 9050aac71e433dc82d087d47eb31f1f4e764d809 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Sat, 15 Apr 2017 17:37:14 +0200 Subject: [PATCH 118/825] Update setuptools from 34.4.1 to 35.0.0 --- misc/requirements/requirements-pip.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-pip.txt b/misc/requirements/requirements-pip.txt index 5bd2aeac8..9979e87f2 100644 --- a/misc/requirements/requirements-pip.txt +++ b/misc/requirements/requirements-pip.txt @@ -3,6 +3,6 @@ appdirs==1.4.3 packaging==16.8 pyparsing==2.2.0 -setuptools==34.4.1 +setuptools==35.0.0 six==1.10.0 wheel==0.29.0 From 842c2d297e4aaa547113de213ab41e838b6cfdb1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 16 Apr 2017 13:07:33 +0200 Subject: [PATCH 119/825] Allow to set message clear timer to 0 Fixes #2527 --- CHANGELOG.asciidoc | 1 + doc/help/settings.asciidoc | 1 + qutebrowser/config/configdata.py | 5 +++-- qutebrowser/mainwindow/messageview.py | 7 +++++-- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 9b7b809c9..983f8a6d3 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -44,6 +44,7 @@ Changed - The HTTP cache is disabled with QtWebKit on Qt 5.8 now as it leads to frequent crashes due to a Qt bug. - stdin is now closed immediately for processes spawned from qutebrowser +- When ui -> message-timeout is set to 0, messages are now never cleared. Fixed ~~~~~ diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index a2df65ca4..9092ea052 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -581,6 +581,7 @@ Default: +pass:[bottom]+ [[ui-message-timeout]] === message-timeout Time (in ms) to show messages in the statusbar for. +Set to 0 to never clear messages. Default: +pass:[2000]+ diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 2c5b27808..abf704801 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -317,8 +317,9 @@ def data(readonly=False): "The position of the status bar."), ('message-timeout', - SettingValue(typ.Int(), '2000'), - "Time (in ms) to show messages in the statusbar for."), + SettingValue(typ.Int(minval=0), '2000'), + "Time (in ms) to show messages in the statusbar for.\n" + "Set to 0 to never clear messages."), ('message-unfocused', SettingValue(typ.Bool(), 'false'), diff --git a/qutebrowser/mainwindow/messageview.py b/qutebrowser/mainwindow/messageview.py index 5c407b78b..7b2d64e07 100644 --- a/qutebrowser/mainwindow/messageview.py +++ b/qutebrowser/mainwindow/messageview.py @@ -98,7 +98,9 @@ class MessageView(QWidget): @config.change_filter('ui', 'message-timeout') def _set_clear_timer_interval(self): """Configure self._clear_timer according to the config.""" - self._clear_timer.setInterval(config.get('ui', 'message-timeout')) + interval = config.get('ui', 'message-timeout') + if interval != 0: + self._clear_timer.setInterval(interval) @pyqtSlot() def clear_messages(self): @@ -125,7 +127,8 @@ class MessageView(QWidget): widget = Message(level, text, replace=replace, parent=self) self._vbox.addWidget(widget) widget.show() - self._clear_timer.start() + if config.get('ui', 'message-timeout') != 0: + self._clear_timer.start() self._messages.append(widget) self._last_text = text self.show() From 2d45257dcc4a24602baa6689a9adffe25513ff20 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 16 Apr 2017 13:08:15 +0200 Subject: [PATCH 120/825] Remove exclamation mark for bookmark messages --- qutebrowser/browser/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 254c21126..8d49ddef8 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -1233,7 +1233,7 @@ class CommandDispatcher: except urlmarks.Error as e: raise cmdexc.CommandError(str(e)) else: - msg = "Bookmarked {}!" if was_added else "Removed bookmark {}!" + msg = "Bookmarked {}" if was_added else "Removed bookmark {}" message.info(msg.format(url.toDisplayString())) @cmdutils.register(instance='command-dispatcher', scope='window', From c82bd837151eb2a2c0bcd0b10ab73955cb40a642 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 16 Apr 2017 13:14:19 +0200 Subject: [PATCH 121/825] Implement RedirectNetworkReply.abort --- qutebrowser/browser/webkit/network/networkreply.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/qutebrowser/browser/webkit/network/networkreply.py b/qutebrowser/browser/webkit/network/networkreply.py index ec37e1e84..dc1f09f0c 100644 --- a/qutebrowser/browser/webkit/network/networkreply.py +++ b/qutebrowser/browser/webkit/network/networkreply.py @@ -149,5 +149,9 @@ class RedirectNetworkReply(QNetworkReply): self.setAttribute(QNetworkRequest.RedirectionTargetAttribute, new_url) QTimer.singleShot(0, lambda: self.finished.emit()) + def abort(self): + """Called when there's e.g. a redirection limit.""" + pass + def readData(self, _maxlen): return bytes() From 9d2734ff6208e7ebef4f73ac1361b07e69f159a9 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 16 Apr 2017 13:15:15 +0200 Subject: [PATCH 122/825] Make sure host is valid for qute:// redirects --- qutebrowser/browser/qutescheme.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index ccd976d0a..d71b6c135 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -146,7 +146,8 @@ def data_for_url(url): new_url.setScheme('qute') new_url.setHost(path) new_url.setPath('/') - raise Redirect(new_url) + if new_url.host(): # path was a valid host + raise Redirect(new_url) try: handler = _HANDLERS[host] From ad9e82b91ec0636778ff2b29df102ebafc39be7c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sun, 16 Apr 2017 21:13:01 +0200 Subject: [PATCH 123/825] Adjust bookmark tests --- tests/end2end/features/urlmarks.feature | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/end2end/features/urlmarks.feature b/tests/end2end/features/urlmarks.feature index f233215b8..b41359885 100644 --- a/tests/end2end/features/urlmarks.feature +++ b/tests/end2end/features/urlmarks.feature @@ -7,12 +7,12 @@ Feature: quickmarks and bookmarks Scenario: Saving a bookmark When I open data/title.html And I run :bookmark-add - Then the message "Bookmarked http://localhost:*/data/title.html!" should be shown + Then the message "Bookmarked http://localhost:*/data/title.html" should be shown And the bookmark file should contain "http://localhost:*/data/title.html Test title" Scenario: Saving a bookmark with a provided url and title When I run :bookmark-add http://example.com "some example title" - Then the message "Bookmarked http://example.com!" should be shown + Then the message "Bookmarked http://example.com" should be shown And the bookmark file should contain "http://example.com some example title" Scenario: Saving a bookmark with a url but no title From 6fb48a5514f2f5dd6759ccfae0ea66ec8ad8297a Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 17 Apr 2017 14:00:24 +0200 Subject: [PATCH 124/825] Update astroid from 1.5.1 to 1.5.2 --- misc/requirements/requirements-pylint.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index 381c13fd6..de6288832 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -1,6 +1,6 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -astroid==1.5.1 +astroid==1.5.2 github3.py==0.9.6 isort==4.2.5 lazy-object-proxy==1.2.2 From 00e4bf7640692d89e5acedc0521ac6ff66591943 Mon Sep 17 00:00:00 2001 From: pyup-bot Date: Mon, 17 Apr 2017 15:20:23 +0200 Subject: [PATCH 125/825] Update pylint from 1.7.0 to 1.7.1 --- misc/requirements/requirements-pylint.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index 381c13fd6..744299808 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -5,7 +5,7 @@ github3.py==0.9.6 isort==4.2.5 lazy-object-proxy==1.2.2 mccabe==0.6.1 -pylint==1.7.0 +pylint==1.7.1 ./scripts/dev/pylint_checkers requests==2.13.0 uritemplate==3.0.0 From db8b6d3e68db1b584c3b46393c5226d6a92bccac Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 17 Apr 2017 16:02:57 +0200 Subject: [PATCH 126/825] Add test for QNetworkReply.abort --- tests/unit/browser/webkit/network/test_networkreply.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/browser/webkit/network/test_networkreply.py b/tests/unit/browser/webkit/network/test_networkreply.py index 7505dbc8c..13e060c8a 100644 --- a/tests/unit/browser/webkit/network/test_networkreply.py +++ b/tests/unit/browser/webkit/network/test_networkreply.py @@ -98,3 +98,4 @@ def test_redirect_network_reply(): reply = networkreply.RedirectNetworkReply(url) assert reply.readData(1) == b'' assert reply.attribute(QNetworkRequest.RedirectionTargetAttribute) == url + reply.abort() # shouldn't do anything From 6151b43c47f1ed0b8a6f0118037ba8bb93447f42 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sun, 16 Apr 2017 09:01:46 -0400 Subject: [PATCH 127/825] Fix qute_history benchmark. This benchmark was running very quickly due to an improper setup. The current history implementation expects that a newly inserted entry must be more recent than any existing entries and sorts according to this assumption. The benchmark test inserts increasingly older entries, breaking this invariant. When run in the benchmark, the qute://history/data implementation would see an entry older than the oldest time in the time window and would immediately return with a single "next" entry. This patch inserts data in an order that mantains history's invariant and adds a sanity-check at the end of the test. It does not check for the exact length as not all entries will be within the time window. The length will be some values <= 100000, the check just ensures that there is at least something more than a "next" entry. Before: ---------------------------------------------- benchmark: 1 tests ---------------------------------------------- Name (time in us) Min Max Mean StdDev Median IQR Outliers(*) Rounds Iterations ---------------------------------------------------------------------------------------------------------------- test_qute_history_benchmark 9.3050 21.9250 9.6143 0.2454 9.5880 0.1070 230;360 9930 1 ---------------------------------------------------------------------------------------------------------------- After: -------------------------------------------------- benchmark: 1 tests ------------------------------------------------- Name (time in ms) Min Max Mean StdDev Median IQR Outliers(*) Rounds Iterations ----------------------------------------------------------------------------------------------------------------------- test_qute_history_benchmark 220.7040 223.1900 221.7536 1.1070 221.1939 1.8803 1;0 5 1 ----------------------------------------------------------------------------------------------------------------------- --- tests/unit/browser/test_qutescheme.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/unit/browser/test_qutescheme.py b/tests/unit/browser/test_qutescheme.py index e46038c8d..92ad30574 100644 --- a/tests/unit/browser/test_qutescheme.py +++ b/tests/unit/browser/test_qutescheme.py @@ -154,7 +154,8 @@ class TestHistoryHandler: assert items[0]["next"] == now - next_time def test_qute_history_benchmark(self, fake_web_history, benchmark, now): - for t in range(100000): # one history per second + # items must be earliest-first to ensure history is sorted properly + for t in range(100000, 0, -1): # one history per second entry = history.Entry( atime=str(now - t), url=QUrl('www.x.com/{}'.format(t)), @@ -162,4 +163,5 @@ class TestHistoryHandler: fake_web_history._add_entry(entry) url = QUrl("qute://history/data?start_time={}".format(now)) - _mimetype, _data = benchmark(qutescheme.qute_history, url) + _mimetype, data = benchmark(qutescheme.qute_history, url) + assert len(json.loads(data)) > 1 From 59a01b860f9e4f636c7911adc0ac370c09e0bd09 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 18 Apr 2017 16:36:14 +0200 Subject: [PATCH 128/825] Add crowdfunding note to README/website --- README.asciidoc | 7 +++++++ www/header.asciidoc | 4 ++++ www/qute.css | 6 ++++++ 3 files changed, 17 insertions(+) diff --git a/README.asciidoc b/README.asciidoc index f79812850..d9dbd8c0c 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -24,6 +24,13 @@ on Python and PyQt5 and free software, licensed under the GPL. It was inspired by other browsers/addons like dwb and Vimperator/Pentadactyl. +// QUTE_WEB_HIDE +**qutebrowser is currently running a crowdfunding campaign for its new +configuration system, allowing for per-domain settings and much more. + +See the link:https://www.kickstarter.com/projects/the-compiler/qutebrowser-v10-with-per-domain-settings?ref=6zw7qz[Kickstarter campaign] for more information!** +// QUTE_WEB_HIDE_END + Screenshots ----------- diff --git a/www/header.asciidoc b/www/header.asciidoc index dc80a5d00..ff5b26d9e 100644 --- a/www/header.asciidoc +++ b/www/header.asciidoc @@ -17,4 +17,8 @@ Releases Blog +