Merge pull request #2 from qutebrowser/master

More Updates
This commit is contained in:
Penaz 2017-07-31 20:45:12 +02:00 committed by GitHub
commit 0611dc0cb4
27 changed files with 178 additions and 79 deletions

View File

@ -28,6 +28,8 @@ Breaking changes
- New dependency on ruamel.yaml; dropped PyYAML dependency. - New dependency on ruamel.yaml; dropped PyYAML dependency.
- The QtWebEngine backend is now used by default if available. - The QtWebEngine backend is now used by default if available.
- New config system which ignores the old config file. - New config system which ignores the old config file.
- The depedency on PyOpenGL (when using QtWebEngine) got removed. Note
that PyQt5.QtOpenGL is still a dependency.
Major changes Major changes
~~~~~~~~~~~~~ ~~~~~~~~~~~~~

View File

@ -45,8 +45,8 @@ pointers:
* https://github.com/qutebrowser/qutebrowser/labels/easy[Issues which should * https://github.com/qutebrowser/qutebrowser/labels/easy[Issues which should
be easy to solve] be easy to solve]
* https://github.com/qutebrowser/qutebrowser/labels/not%20code[Issues which * https://github.com/qutebrowser/qutebrowser/labels/component%3A%20docs[Documentation
require little/no coding] * issues which require little/no coding]
If you prefer C++ or Javascript to Python, see the relevant issues which involve If you prefer C++ or Javascript to Python, see the relevant issues which involve
work in those languages: work in those languages:

View File

@ -118,7 +118,6 @@ The following software and libraries are required to run qutebrowser:
* http://jinja.pocoo.org/[jinja2] * http://jinja.pocoo.org/[jinja2]
* http://pygments.org/[pygments] * http://pygments.org/[pygments]
* http://pyyaml.org/wiki/PyYAML[PyYAML] * http://pyyaml.org/wiki/PyYAML[PyYAML]
* http://pyopengl.sourceforge.net/[PyOpenGL] when using QtWebEngine
The following libraries are optional: The following libraries are optional:

View File

@ -1,9 +1,9 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
certifi==2017.4.17 certifi==2017.7.27.1
chardet==3.0.4 chardet==3.0.4
codecov==2.0.9 codecov==2.0.9
coverage==4.4.1 coverage==4.4.1
idna==2.5 idna==2.5
requests==2.18.1 requests==2.18.2
urllib3==1.21.1 urllib3==1.22

View File

@ -3,7 +3,7 @@
flake8==2.6.2 # rq.filter: < 3.0.0 flake8==2.6.2 # rq.filter: < 3.0.0
flake8-copyright==0.2.0 flake8-copyright==0.2.0
flake8-debugger==1.4.0 # rq.filter: != 2.0.0 flake8-debugger==1.4.0 # rq.filter: != 2.0.0
flake8-deprecated==1.2 flake8-deprecated==1.2.1
flake8-docstrings==1.0.3 # rq.filter: < 1.1.0 flake8-docstrings==1.0.3 # rq.filter: < 1.1.0
flake8-future-import==0.4.3 flake8-future-import==0.4.3
flake8-mock==0.3 flake8-mock==0.3

View File

@ -3,6 +3,6 @@
appdirs==1.4.3 appdirs==1.4.3
packaging==16.8 packaging==16.8
pyparsing==2.2.0 pyparsing==2.2.0
setuptools==36.2.0 setuptools==36.2.5
six==1.10.0 six==1.10.0
wheel==0.29.0 wheel==0.29.0

View File

@ -1,7 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
-e git+https://github.com/PyCQA/astroid.git#egg=astroid -e git+https://github.com/PyCQA/astroid.git#egg=astroid
certifi==2017.4.17 certifi==2017.7.27.1
chardet==3.0.4 chardet==3.0.4
github3.py==0.9.6 github3.py==0.9.6
idna==2.5 idna==2.5
@ -10,9 +10,9 @@ lazy-object-proxy==1.3.1
mccabe==0.6.1 mccabe==0.6.1
-e git+https://github.com/PyCQA/pylint.git#egg=pylint -e git+https://github.com/PyCQA/pylint.git#egg=pylint
./scripts/dev/pylint_checkers ./scripts/dev/pylint_checkers
requests==2.18.1 requests==2.18.2
six==1.10.0 six==1.10.0
uritemplate==3.0.0 uritemplate==3.0.0
uritemplate.py==3.0.2 uritemplate.py==3.0.2
urllib3==1.21.1 urllib3==1.22
wrapt==1.10.10 wrapt==1.10.10

View File

@ -1,7 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
astroid==1.5.3 astroid==1.5.3
certifi==2017.4.17 certifi==2017.7.27.1
chardet==3.0.4 chardet==3.0.4
github3.py==0.9.6 github3.py==0.9.6
idna==2.5 idna==2.5
@ -10,9 +10,9 @@ lazy-object-proxy==1.3.1
mccabe==0.6.1 mccabe==0.6.1
pylint==1.7.2 pylint==1.7.2
./scripts/dev/pylint_checkers ./scripts/dev/pylint_checkers
requests==2.18.1 requests==2.18.2
six==1.10.0 six==1.10.0
uritemplate==3.0.0 uritemplate==3.0.0
uritemplate.py==3.0.2 uritemplate.py==3.0.2
urllib3==1.21.1 urllib3==1.22
wrapt==1.10.10 wrapt==1.10.10

View File

@ -5,14 +5,14 @@ cheroot==5.7.0
click==6.7 click==6.7
# colorama==0.3.9 # colorama==0.3.9
coverage==4.4.1 coverage==4.4.1
decorator==4.1.1 decorator==4.1.2
EasyProcess==0.2.3 EasyProcess==0.2.3
fields==5.0.0 fields==5.0.0
Flask==0.12.2 Flask==0.12.2
glob2==0.5 glob2==0.5
httpbin==0.5.0 httpbin==0.5.0
hunter==1.4.1 hunter==1.4.1
hypothesis==3.13.0 hypothesis==3.14.0
itsdangerous==0.24 itsdangerous==0.24
# Jinja2==2.9.6 # Jinja2==2.9.6
Mako==1.0.7 Mako==1.0.7
@ -22,12 +22,12 @@ parse-type==0.3.4
py==1.4.34 py==1.4.34
pytest==3.1.3 pytest==3.1.3
pytest-bdd==2.18.2 pytest-bdd==2.18.2
pytest-benchmark==3.0.0 pytest-benchmark==3.1.1
pytest-catchlog==1.2.2 pytest-catchlog==1.2.2
pytest-cov==2.5.1 pytest-cov==2.5.1
pytest-faulthandler==1.3.1 pytest-faulthandler==1.3.1
pytest-instafail==0.3.0 pytest-instafail==0.3.0
pytest-mock==1.6.0 pytest-mock==1.6.2
pytest-qt==2.1.2 pytest-qt==2.1.2
pytest-repeat==0.4.1 pytest-repeat==0.4.1
pytest-rerunfailures==2.2 pytest-rerunfailures==2.2
@ -35,5 +35,5 @@ pytest-travis-fold==1.2.0
pytest-xvfb==1.0.0 pytest-xvfb==1.0.0
PyVirtualDisplay==0.2.1 PyVirtualDisplay==0.2.1
six==1.10.0 six==1.10.0
vulture==0.16 vulture==0.21
Werkzeug==0.12.2 Werkzeug==0.12.2

View File

@ -1,3 +1,3 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py # This file is automatically generated by scripts/dev/recompile_requirements.py
vulture==0.16 vulture==0.21

View File

@ -1,5 +1,5 @@
[pytest] [pytest]
addopts = --strict -rfEw --faulthandler-timeout=70 --instafail --pythonwarnings error addopts = --strict -rfEw --faulthandler-timeout=70 --instafail --pythonwarnings error --benchmark-columns=Min,Max,Median
testpaths = tests testpaths = tests
markers = markers =
gui: Tests using the GUI (e.g. spawning widgets) gui: Tests using the GUI (e.g. spawning widgets)

View File

@ -77,13 +77,9 @@ class UrlMarkManager(QObject):
Signals: Signals:
changed: Emitted when anything changed. changed: Emitted when anything changed.
added: Emitted when a new quickmark/bookmark was added.
removed: Emitted when an existing quickmark/bookmark was removed.
""" """
changed = pyqtSignal() changed = pyqtSignal()
added = pyqtSignal(str, str)
removed = pyqtSignal(str)
def __init__(self, parent=None): def __init__(self, parent=None):
"""Initialize and read quickmarks.""" """Initialize and read quickmarks."""
@ -121,7 +117,6 @@ class UrlMarkManager(QObject):
""" """
del self.marks[key] del self.marks[key]
self.changed.emit() self.changed.emit()
self.removed.emit(key)
class QuickmarkManager(UrlMarkManager): class QuickmarkManager(UrlMarkManager):
@ -133,7 +128,6 @@ class QuickmarkManager(UrlMarkManager):
- self.marks maps names to URLs. - self.marks maps names to URLs.
- changed gets emitted with the name as first argument and the URL as - changed gets emitted with the name as first argument and the URL as
second argument. second argument.
- removed gets emitted with the name as argument.
""" """
def _init_lineparser(self): def _init_lineparser(self):
@ -193,7 +187,6 @@ class QuickmarkManager(UrlMarkManager):
"""Really set the quickmark.""" """Really set the quickmark."""
self.marks[name] = url self.marks[name] = url
self.changed.emit() self.changed.emit()
self.added.emit(name, url)
log.misc.debug("Added quickmark {} for {}".format(name, url)) log.misc.debug("Added quickmark {} for {}".format(name, url))
if name in self.marks: if name in self.marks:
@ -243,7 +236,6 @@ class BookmarkManager(UrlMarkManager):
- self.marks maps URLs to titles. - self.marks maps URLs to titles.
- changed gets emitted with the URL as first argument and the title as - changed gets emitted with the URL as first argument and the title as
second argument. second argument.
- removed gets emitted with the URL as argument.
""" """
def _init_lineparser(self): def _init_lineparser(self):
@ -295,5 +287,4 @@ class BookmarkManager(UrlMarkManager):
else: else:
self.marks[urlstr] = title self.marks[urlstr] = title
self.changed.emit() self.changed.emit()
self.added.emit(title, urlstr)
return True return True

View File

@ -28,6 +28,9 @@ Module attributes:
""" """
import os import os
import sys
import ctypes
import ctypes.util
from PyQt5.QtGui import QFont from PyQt5.QtGui import QFont
from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile, from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile,
@ -199,6 +202,11 @@ def init(args):
if args.enable_webengine_inspector: if args.enable_webengine_inspector:
os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = str(utils.random_port()) os.environ['QTWEBENGINE_REMOTE_DEBUGGING'] = str(utils.random_port())
# WORKAROUND for
# https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826
if sys.platform == 'linux':
ctypes.CDLL(ctypes.util.find_library("GL"), mode=ctypes.RTLD_GLOBAL)
_init_profiles() _init_profiles()
# We need to do this here as a WORKAROUND for # We need to do this here as a WORKAROUND for

View File

@ -148,6 +148,8 @@ class CompletionView(QTreeView):
def _resize_columns(self): def _resize_columns(self):
"""Resize the completion columns based on column_widths.""" """Resize the completion columns based on column_widths."""
if self.model() is None:
return
width = self.size().width() width = self.size().width()
column_widths = self.model().column_widths column_widths = self.model().column_widths
pixel_widths = [(width * perc // 100) for perc in column_widths] pixel_widths = [(width * perc // 100) for perc in column_widths]
@ -253,6 +255,10 @@ class CompletionView(QTreeView):
selmodel.setCurrentIndex( selmodel.setCurrentIndex(
idx, QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows) idx, QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
# if the last item is focused, try to fetch more
if idx.row() == self.model().rowCount(idx.parent()) - 1:
self.expandAll()
count = self.model().count() count = self.model().count()
if count == 0: if count == 0:
self.hide() self.hide()

View File

@ -93,10 +93,12 @@ class HistoryCategory(QSqlQueryModel):
self._query.run(pat=pattern) self._query.run(pat=pattern)
self.setQuery(self._query) self.setQuery(self._query)
def removeRows(self, _row, _count, _parent=None): def removeRows(self, row, _count, _parent=None):
"""Override QAbstractItemModel::removeRows to re-run sql query.""" """Override QAbstractItemModel::removeRows to re-run sql query."""
# re-run query to reload updated table # re-run query to reload updated table
with debug.log_time('sql', 'Re-running completion query post-delete'): with debug.log_time('sql', 'Re-running completion query post-delete'):
self._query.run() self._query.run()
self.setQuery(self._query) self.setQuery(self._query)
while self.rowCount() < row:
self.fetchMore()
return True return True

View File

@ -60,17 +60,33 @@ def helptopic():
def quickmark(): def quickmark():
"""A CompletionModel filled with all quickmarks.""" """A CompletionModel filled with all quickmarks."""
def delete(data):
"""Delete a quickmark from the completion menu."""
name = data[0]
quickmark_manager = objreg.get('quickmark-manager')
log.completion.debug('Deleting quickmark {}'.format(name))
quickmark_manager.delete(name)
model = completionmodel.CompletionModel(column_widths=(30, 70, 0)) model = completionmodel.CompletionModel(column_widths=(30, 70, 0))
marks = objreg.get('quickmark-manager').marks.items() marks = objreg.get('quickmark-manager').marks.items()
model.add_category(listcategory.ListCategory('Quickmarks', marks)) model.add_category(listcategory.ListCategory('Quickmarks', marks,
delete_func=delete))
return model return model
def bookmark(): def bookmark():
"""A CompletionModel filled with all bookmarks.""" """A CompletionModel filled with all bookmarks."""
def delete(data):
"""Delete a bookmark from the completion menu."""
urlstr = data[0]
log.completion.debug('Deleting bookmark {}'.format(urlstr))
bookmark_manager = objreg.get('bookmark-manager')
bookmark_manager.delete(urlstr)
model = completionmodel.CompletionModel(column_widths=(30, 70, 0)) model = completionmodel.CompletionModel(column_widths=(30, 70, 0))
marks = objreg.get('bookmark-manager').marks.items() marks = objreg.get('bookmark-manager').marks.items()
model.add_category(listcategory.ListCategory('Bookmarks', marks)) model.add_category(listcategory.ListCategory('Bookmarks', marks,
delete_func=delete))
return model return model
@ -126,11 +142,12 @@ def bind(key):
key: the key being bound. key: the key being bound.
""" """
model = completionmodel.CompletionModel(column_widths=(20, 60, 20)) model = completionmodel.CompletionModel(column_widths=(20, 60, 20))
cmd_name = objreg.get('key-config').get_bindings_for('normal').get(key) cmd_text = objreg.get('key-config').get_bindings_for('normal').get(key)
if cmd_name: if cmd_text:
cmd_name = cmd_text.split(' ')[0]
cmd = cmdutils.cmd_dict.get(cmd_name) cmd = cmdutils.cmd_dict.get(cmd_name)
data = [(cmd_name, cmd.desc, key)] data = [(cmd_text, cmd.desc, key)]
model.add_category(listcategory.ListCategory("Current", data)) model.add_category(listcategory.ListCategory("Current", data))
cmdlist = _get_cmd_completions(include_hidden=True, include_aliases=True) cmdlist = _get_cmd_completions(include_hidden=True, include_aliases=True)

View File

@ -708,13 +708,16 @@ class TabbedBrowser(tabwidget.TabWidget):
} }
msg = messages[status] msg = messages[status]
def show_error_page(html):
tab.set_html(html)
log.webview.error(msg)
if qtutils.version_check('5.9'): if qtutils.version_check('5.9'):
url_string = tab.url(requested=True).toDisplayString() url_string = tab.url(requested=True).toDisplayString()
error_page = jinja.render( error_page = jinja.render(
'error.html', title="Error loading {}".format(url_string), 'error.html', title="Error loading {}".format(url_string),
url=url_string, error=msg, icon='') url=url_string, error=msg, icon='')
QTimer.singleShot(0, lambda: tab.set_html(error_page)) QTimer.singleShot(100, lambda: show_error_page(error_page))
log.webview.error(msg)
else: else:
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58698 # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58698
message.error(msg) message.error(msg)

View File

@ -36,7 +36,6 @@ import traceback
import signal import signal
import importlib import importlib
import datetime import datetime
import logging
try: try:
import tkinter import tkinter
except ImportError: except ImportError:
@ -344,12 +343,6 @@ def check_libraries(backend):
modules['PyQt5.QtWebEngineWidgets'] = _missing_str("QtWebEngine", modules['PyQt5.QtWebEngineWidgets'] = _missing_str("QtWebEngine",
webengine=True) webengine=True)
modules['PyQt5.QtOpenGL'] = _missing_str("PyQt5.QtOpenGL") modules['PyQt5.QtOpenGL'] = _missing_str("PyQt5.QtOpenGL")
# Workaround for a black screen with some setups
# https://github.com/spyder-ide/spyder/issues/3226
if not os.environ.get('QUTE_NO_OPENGL_WORKAROUND'):
# Hide "No OpenGL_accelerate module loaded: ..." message
logging.getLogger('OpenGL.acceleratesupport').propagate = False
modules['OpenGL.GL'] = _missing_str("PyOpenGL")
else: else:
assert backend == 'webkit' assert backend == 'webkit'
modules['PyQt5.QtWebKit'] = _missing_str("PyQt5.QtWebKit") modules['PyQt5.QtWebKit'] = _missing_str("PyQt5.QtWebKit")

View File

@ -23,7 +23,7 @@ import os
import os.path import os.path
import sip import sip
from PyQt5.QtCore import pyqtSignal, QUrl, QObject, QPoint, QTimer from PyQt5.QtCore import QUrl, QObject, QPoint, QTimer
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
import yaml import yaml
try: try:
@ -106,14 +106,8 @@ class SessionManager(QObject):
closed. closed.
_current: The name of the currently loaded session, or None. _current: The name of the currently loaded session, or None.
did_load: Set when a session was loaded. did_load: Set when a session was loaded.
Signals:
update_completion: Emitted when the session completion should get
updated.
""" """
update_completion = pyqtSignal()
def __init__(self, base_path, parent=None): def __init__(self, base_path, parent=None):
super().__init__(parent) super().__init__(parent)
self._current = None self._current = None
@ -303,8 +297,7 @@ class SessionManager(QObject):
encoding='utf-8', allow_unicode=True) encoding='utf-8', allow_unicode=True)
except (OSError, UnicodeEncodeError, yaml.YAMLError) as e: except (OSError, UnicodeEncodeError, yaml.YAMLError) as e:
raise SessionError(e) raise SessionError(e)
else:
self.update_completion.emit()
if load_next_time: if load_next_time:
state_config = objreg.get('state-config') state_config = objreg.get('state-config')
state_config['general']['session'] = name state_config['general']['session'] = name
@ -425,7 +418,6 @@ class SessionManager(QObject):
os.remove(path) os.remove(path)
except OSError as e: except OSError as e:
raise SessionError(e) raise SessionError(e)
self.update_completion.emit()
def list_sessions(self): def list_sessions(self):
"""Get a list of all session names.""" """Get a list of all session names."""

View File

@ -186,7 +186,6 @@ def _module_versions():
('yaml', ['__version__']), ('yaml', ['__version__']),
('cssutils', ['__version__']), ('cssutils', ['__version__']),
('typing', []), ('typing', []),
('OpenGL', ['__version__']),
('PyQt5.QtWebEngineWidgets', []), ('PyQt5.QtWebEngineWidgets', []),
('PyQt5.QtWebKitWidgets', []), ('PyQt5.QtWebKitWidgets', []),
]) ])

View File

@ -7,4 +7,3 @@ MarkupSafe==1.0
Pygments==2.2.0 Pygments==2.2.0
pyPEG2==2.15.2 pyPEG2==2.15.2
PyYAML==3.12 PyYAML==3.12
PyOpenGL==3.1.0

View File

@ -89,6 +89,12 @@ def whitelist_generator():
# vulture doesn't notice the hasattr() and thus thinks netrc_used is unused # vulture doesn't notice the hasattr() and thus thinks netrc_used is unused
# in NetworkManager.on_authentication_required # in NetworkManager.on_authentication_required
yield 'PyQt5.QtNetwork.QNetworkReply.netrc_used' yield 'PyQt5.QtNetwork.QNetworkReply.netrc_used'
yield 'qutebrowser.browser.downloads.last_used_directory'
yield 'PaintContext.clip' # from completiondelegate.py
yield 'logging.LogRecord.log_color' # from logging.py
yield 'scripts.utils.use_color' # from asciidoc2html.py
for attr in ['pyeval_output', 'log_clipboard', 'fake_clipboard']:
yield 'qutebrowser.misc.utilcmds.' + attr
for attr in ['fileno', 'truncate', 'closed', 'readable']: for attr in ['fileno', 'truncate', 'closed', 'readable']:
yield 'qutebrowser.utils.qtutils.PyQIODevice.' + attr yield 'qutebrowser.utils.qtutils.PyQIODevice.' + attr
@ -111,7 +117,7 @@ def filter_func(item):
True if the missing function should be filtered/ignored, False True if the missing function should be filtered/ignored, False
otherwise. otherwise.
""" """
return bool(re.match(r'[a-z]+[A-Z][a-zA-Z]+', str(item))) return bool(re.match(r'[a-z]+[A-Z][a-zA-Z]+', item.name))
def report(items): def report(items):
@ -125,7 +131,7 @@ def report(items):
relpath = os.path.relpath(item.filename) relpath = os.path.relpath(item.filename)
path = relpath if not relpath.startswith('..') else item.filename path = relpath if not relpath.startswith('..') else item.filename
output.append("{}:{}: Unused {} '{}'".format(path, item.lineno, output.append("{}:{}: Unused {} '{}'".format(path, item.lineno,
item.typ, item)) item.typ, item.name))
return output return output

View File

@ -65,6 +65,9 @@ def completionview(qtbot, status_command_stub, config_stub, win_registry,
} }
# mock the Completer that the widget creates in its constructor # mock the Completer that the widget creates in its constructor
mocker.patch('qutebrowser.completion.completer.Completer', autospec=True) mocker.patch('qutebrowser.completion.completer.Completer', autospec=True)
mocker.patch(
'qutebrowser.completion.completiondelegate.CompletionItemDelegate',
new=lambda *_: None)
view = completionwidget.CompletionView(win_id=0) view = completionwidget.CompletionView(win_id=0)
qtbot.addWidget(view) qtbot.addWidget(view)
return view return view
@ -186,6 +189,37 @@ def test_completion_item_focus_no_model(which, completionview, qtbot):
completionview.completion_item_focus(which) completionview.completion_item_focus(which)
def test_completion_item_focus_fetch(completionview, qtbot):
"""Test that on_next_prev_item moves the selection properly.
Args:
which: the direction in which to move the selection.
tree: Each list represents a completion category, with each string
being an item under that category.
expected: expected argument from on_selection_changed for each
successive movement. None implies no signal should be
emitted.
"""
model = completionmodel.CompletionModel()
cat = mock.Mock(spec=['layoutChanged', 'layoutAboutToBeChanged',
'canFetchMore', 'fetchMore', 'rowCount', 'index', 'data'])
cat.canFetchMore = lambda *_: True
cat.rowCount = lambda *_: 2
cat.fetchMore = mock.Mock()
model.add_category(cat)
completionview.set_model(model)
# clear the fetchMore call that happens on set_model
cat.reset_mock()
# not at end, fetchMore shouldn't be called
completionview.completion_item_focus('next')
assert not cat.fetchMore.called
# at end, fetchMore should be called
completionview.completion_item_focus('next')
assert cat.fetchMore.called
@pytest.mark.parametrize('show', ['always', 'auto', 'never']) @pytest.mark.parametrize('show', ['always', 'auto', 'never'])
@pytest.mark.parametrize('rows', [[], ['Aa'], ['Aa', 'Bb']]) @pytest.mark.parametrize('rows', [[], ['Aa'], ['Aa', 'Bb']])
@pytest.mark.parametrize('quick_complete', [True, False]) @pytest.mark.parametrize('quick_complete', [True, False])
@ -240,3 +274,8 @@ def test_completion_item_del_no_selection(completionview):
with pytest.raises(cmdexc.CommandError, match='No item selected!'): with pytest.raises(cmdexc.CommandError, match='No item selected!'):
completionview.completion_item_del() completionview.completion_item_del()
assert not func.called assert not func.called
def test_resize_no_model(completionview, qtbot):
"""Ensure no crash if resizeEvent is triggered with no model (#2854)."""
completionview.resizeEvent(None)

View File

@ -22,7 +22,6 @@
import datetime import datetime
import pytest import pytest
from PyQt5.QtCore import QModelIndex
from qutebrowser.misc import sql from qutebrowser.misc import sql
from qutebrowser.completion.models import histcategory from qutebrowser.completion.models import histcategory
@ -147,6 +146,22 @@ def test_remove_rows(hist, model_validator):
model_validator.set_model(cat) model_validator.set_model(cat)
cat.set_pattern('') cat.set_pattern('')
hist.delete('url', 'foo') hist.delete('url', 'foo')
# histcategory does not care which index was removed, it just regenerates cat.removeRows(0, 1)
cat.removeRows(QModelIndex(), 1)
model_validator.validate([('bar', 'Bar', '')]) model_validator.validate([('bar', 'Bar', '')])
def test_remove_rows_fetch(hist):
"""removeRows should fetch enough data to make the current index valid."""
# we cannot use model_validator as it will fetch everything up front
hist.insert_batch({'url': [str(i) for i in range(300)]})
cat = histcategory.HistoryCategory()
cat.set_pattern('')
# sanity check that we didn't fetch everything up front
assert cat.rowCount() < 300
cat.fetchMore()
assert cat.rowCount() == 300
hist.delete('url', '298')
cat.removeRows(297, 1)
assert cat.rowCount() == 299

View File

@ -252,6 +252,27 @@ def test_quickmark_completion(qtmodeltester, quickmarks):
}) })
@pytest.mark.parametrize('row, removed', [
(0, 'aw'),
(1, 'ddg'),
(2, 'wiki'),
])
def test_quickmark_completion_delete(qtmodeltester, quickmarks, row, removed):
"""Test deleting a quickmark from the quickmark completion model."""
model = miscmodels.quickmark()
model.set_pattern('')
qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model)
parent = model.index(0, 0)
idx = model.index(row, 0, parent)
before = set(quickmarks.marks.keys())
model.delete_cur_item(idx)
after = set(quickmarks.marks.keys())
assert before.difference(after) == {removed}
def test_bookmark_completion(qtmodeltester, bookmarks): def test_bookmark_completion(qtmodeltester, bookmarks):
"""Test the results of bookmark completion.""" """Test the results of bookmark completion."""
model = miscmodels.bookmark() model = miscmodels.bookmark()
@ -268,6 +289,27 @@ def test_bookmark_completion(qtmodeltester, bookmarks):
}) })
@pytest.mark.parametrize('row, removed', [
(0, 'http://qutebrowser.org'),
(1, 'https://github.com'),
(2, 'https://python.org'),
])
def test_bookmark_completion_delete(qtmodeltester, bookmarks, row, removed):
"""Test deleting a quickmark from the quickmark completion model."""
model = miscmodels.bookmark()
model.set_pattern('')
qtmodeltester.data_display_may_return_none = True
qtmodeltester.check(model)
parent = model.index(0, 0)
idx = model.index(row, 0, parent)
before = set(bookmarks.marks.keys())
model.delete_cur_item(idx)
after = set(bookmarks.marks.keys())
assert before.difference(after) == {removed}
def test_url_completion(qtmodeltester, web_history_populated, def test_url_completion(qtmodeltester, web_history_populated,
quickmarks, bookmarks): quickmarks, bookmarks):
"""Test the results of url completion. """Test the results of url completion.
@ -583,7 +625,7 @@ def test_bind_completion(qtmodeltester, monkeypatch, stubs, config_stub,
_patch_cmdutils(monkeypatch, stubs, _patch_cmdutils(monkeypatch, stubs,
'qutebrowser.completion.models.miscmodels.cmdutils') 'qutebrowser.completion.models.miscmodels.cmdutils')
config_stub.data['aliases'] = {'rock': 'roll'} config_stub.data['aliases'] = {'rock': 'roll'}
key_config_stub.set_bindings_for('normal', {'s': 'stop', key_config_stub.set_bindings_for('normal', {'s': 'stop now',
'rr': 'roll', 'rr': 'roll',
'ro': 'rock'}) 'ro': 'rock'})
model = miscmodels.bind('s') model = miscmodels.bind('s')
@ -593,14 +635,14 @@ def test_bind_completion(qtmodeltester, monkeypatch, stubs, config_stub,
_check_completions(model, { _check_completions(model, {
"Current": [ "Current": [
('stop', 'stop qutebrowser', 's'), ('stop now', 'stop qutebrowser', 's'),
], ],
"Commands": [ "Commands": [
('drop', 'drop all user data', ''), ('drop', 'drop all user data', ''),
('hide', '', ''), ('hide', '', ''),
('rock', "Alias for 'roll'", 'ro'), ('rock', "Alias for 'roll'", 'ro'),
('roll', 'never gonna give you up', 'rr'), ('roll', 'never gonna give you up', 'rr'),
('stop', 'stop qutebrowser', 's'), ('stop', 'stop qutebrowser', ''),
] ]
}) })

View File

@ -215,11 +215,6 @@ class TestSave:
objreg.delete('main-window', scope='window', window=0) objreg.delete('main-window', scope='window', window=0)
objreg.delete('tabbed-browser', scope='window', window=0) objreg.delete('tabbed-browser', scope='window', window=0)
def test_update_completion_signal(self, sess_man, tmpdir, qtbot):
session_path = tmpdir / 'foo.yml'
with qtbot.waitSignal(sess_man.update_completion):
sess_man.save(str(session_path))
def test_no_state_config(self, sess_man, tmpdir, state_config): def test_no_state_config(self, sess_man, tmpdir, state_config):
session_path = tmpdir / 'foo.yml' session_path = tmpdir / 'foo.yml'
sess_man.save(str(session_path)) sess_man.save(str(session_path))
@ -367,14 +362,6 @@ class TestLoadTab:
assert loaded_item.original_url == expected assert loaded_item.original_url == expected
def test_delete_update_completion_signal(sess_man, qtbot, tmpdir):
sess = tmpdir / 'foo.yml'
sess.ensure()
with qtbot.waitSignal(sess_man.update_completion):
sess_man.delete(str(sess))
class TestListSessions: class TestListSessions:
def test_no_sessions(self, tmpdir): def test_no_sessions(self, tmpdir):

View File

@ -495,7 +495,6 @@ class ImportFake:
('yaml', True), ('yaml', True),
('cssutils', True), ('cssutils', True),
('typing', True), ('typing', True),
('OpenGL', True),
('PyQt5.QtWebEngineWidgets', True), ('PyQt5.QtWebEngineWidgets', True),
('PyQt5.QtWebKitWidgets', True), ('PyQt5.QtWebKitWidgets', True),
]) ])