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.
- The QtWebEngine backend is now used by default if available.
- 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
~~~~~~~~~~~~~

View File

@ -45,8 +45,8 @@ pointers:
* https://github.com/qutebrowser/qutebrowser/labels/easy[Issues which should
be easy to solve]
* https://github.com/qutebrowser/qutebrowser/labels/not%20code[Issues which
require little/no coding]
* https://github.com/qutebrowser/qutebrowser/labels/component%3A%20docs[Documentation
* issues which require little/no coding]
If you prefer C++ or Javascript to Python, see the relevant issues which involve
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://pygments.org/[pygments]
* http://pyyaml.org/wiki/PyYAML[PyYAML]
* http://pyopengl.sourceforge.net/[PyOpenGL] when using QtWebEngine
The following libraries are optional:

View File

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

View File

@ -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.2
flake8-deprecated==1.2.1
flake8-docstrings==1.0.3 # rq.filter: < 1.1.0
flake8-future-import==0.4.3
flake8-mock==0.3

View File

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

View File

@ -1,7 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
-e git+https://github.com/PyCQA/astroid.git#egg=astroid
certifi==2017.4.17
certifi==2017.7.27.1
chardet==3.0.4
github3.py==0.9.6
idna==2.5
@ -10,9 +10,9 @@ 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.18.1
requests==2.18.2
six==1.10.0
uritemplate==3.0.0
uritemplate.py==3.0.2
urllib3==1.21.1
urllib3==1.22
wrapt==1.10.10

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
[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
markers =
gui: Tests using the GUI (e.g. spawning widgets)

View File

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

View File

@ -28,6 +28,9 @@ Module attributes:
"""
import os
import sys
import ctypes
import ctypes.util
from PyQt5.QtGui import QFont
from PyQt5.QtWebEngineWidgets import (QWebEngineSettings, QWebEngineProfile,
@ -199,6 +202,11 @@ def init(args):
if args.enable_webengine_inspector:
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()
# We need to do this here as a WORKAROUND for

View File

@ -148,6 +148,8 @@ class CompletionView(QTreeView):
def _resize_columns(self):
"""Resize the completion columns based on column_widths."""
if self.model() is None:
return
width = self.size().width()
column_widths = self.model().column_widths
pixel_widths = [(width * perc // 100) for perc in column_widths]
@ -253,6 +255,10 @@ class CompletionView(QTreeView):
selmodel.setCurrentIndex(
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()
if count == 0:
self.hide()

View File

@ -93,10 +93,12 @@ class HistoryCategory(QSqlQueryModel):
self._query.run(pat=pattern)
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."""
# re-run query to reload updated table
with debug.log_time('sql', 'Re-running completion query post-delete'):
self._query.run()
self.setQuery(self._query)
while self.rowCount() < row:
self.fetchMore()
return True

View File

@ -60,17 +60,33 @@ def helptopic():
def quickmark():
"""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))
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
def bookmark():
"""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))
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
@ -126,11 +142,12 @@ def bind(key):
key: the key being bound.
"""
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)
data = [(cmd_name, cmd.desc, key)]
data = [(cmd_text, cmd.desc, key)]
model.add_category(listcategory.ListCategory("Current", data))
cmdlist = _get_cmd_completions(include_hidden=True, include_aliases=True)

View File

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

View File

@ -36,7 +36,6 @@ import traceback
import signal
import importlib
import datetime
import logging
try:
import tkinter
except ImportError:
@ -344,12 +343,6 @@ def check_libraries(backend):
modules['PyQt5.QtWebEngineWidgets'] = _missing_str("QtWebEngine",
webengine=True)
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:
assert backend == 'webkit'
modules['PyQt5.QtWebKit'] = _missing_str("PyQt5.QtWebKit")

View File

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

View File

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

View File

@ -7,4 +7,3 @@ MarkupSafe==1.0
Pygments==2.2.0
pyPEG2==2.15.2
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
# in NetworkManager.on_authentication_required
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']:
yield 'qutebrowser.utils.qtutils.PyQIODevice.' + attr
@ -111,7 +117,7 @@ def filter_func(item):
True if the missing function should be filtered/ignored, False
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):
@ -125,7 +131,7 @@ def report(items):
relpath = os.path.relpath(item.filename)
path = relpath if not relpath.startswith('..') else item.filename
output.append("{}:{}: Unused {} '{}'".format(path, item.lineno,
item.typ, item))
item.typ, item.name))
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
mocker.patch('qutebrowser.completion.completer.Completer', autospec=True)
mocker.patch(
'qutebrowser.completion.completiondelegate.CompletionItemDelegate',
new=lambda *_: None)
view = completionwidget.CompletionView(win_id=0)
qtbot.addWidget(view)
return view
@ -186,6 +189,37 @@ def test_completion_item_focus_no_model(which, completionview, qtbot):
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('rows', [[], ['Aa'], ['Aa', 'Bb']])
@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!'):
completionview.completion_item_del()
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 pytest
from PyQt5.QtCore import QModelIndex
from qutebrowser.misc import sql
from qutebrowser.completion.models import histcategory
@ -147,6 +146,22 @@ def test_remove_rows(hist, model_validator):
model_validator.set_model(cat)
cat.set_pattern('')
hist.delete('url', 'foo')
# histcategory does not care which index was removed, it just regenerates
cat.removeRows(QModelIndex(), 1)
cat.removeRows(0, 1)
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):
"""Test the results of bookmark completion."""
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,
quickmarks, bookmarks):
"""Test the results of url completion.
@ -583,7 +625,7 @@ def test_bind_completion(qtmodeltester, monkeypatch, stubs, config_stub,
_patch_cmdutils(monkeypatch, stubs,
'qutebrowser.completion.models.miscmodels.cmdutils')
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',
'ro': 'rock'})
model = miscmodels.bind('s')
@ -593,14 +635,14 @@ def test_bind_completion(qtmodeltester, monkeypatch, stubs, config_stub,
_check_completions(model, {
"Current": [
('stop', 'stop qutebrowser', 's'),
('stop now', 'stop qutebrowser', 's'),
],
"Commands": [
('drop', 'drop all user data', ''),
('hide', '', ''),
('rock', "Alias for 'roll'", 'ro'),
('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('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):
session_path = tmpdir / 'foo.yml'
sess_man.save(str(session_path))
@ -367,14 +362,6 @@ class TestLoadTab:
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:
def test_no_sessions(self, tmpdir):

View File

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