From 28c8e5682a00fb5b246f828b8bc048bb7f7b64d8 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sat, 15 Sep 2018 12:37:34 -0400 Subject: [PATCH 1/8] Unit test CompletionItemDelegate.paint. There were no unit tests for this whole module. It is difficult to test due to all the private logic and Qt dependencies, but with a lot of mocking we can at least validate some of the text handling. This is a setup to start testing the solution to #4199. I picked '{' and '}' as placeholders in the test data because they draw the eye to the 'highlighted' part, and vim even highlights them with python syntax highlighting. It could be confusing though, as they look like format strings but are not used that way. --- .../completion/test_completiondelegate.py | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 tests/unit/completion/test_completiondelegate.py diff --git a/tests/unit/completion/test_completiondelegate.py b/tests/unit/completion/test_completiondelegate.py new file mode 100644 index 000000000..b90c096a2 --- /dev/null +++ b/tests/unit/completion/test_completiondelegate.py @@ -0,0 +1,106 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2018 Ryan Roden-Corrent (rcorre) +# +# 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 . + +"""Tests for the CompletionView Object.""" + +from unittest import mock + +import pytest + +from PyQt5.QtCore import QModelIndex +from PyQt5.QtWidgets import QStyleOptionViewItem, QStyledItemDelegate +from PyQt5.QtGui import QPainter + +from qutebrowser.completion import completiondelegate + + +@pytest.fixture +def painter(): + """Create the CompletionView used for testing.""" + return mock.Mock(spec=QPainter) + + +def _qt_mock(klass, mocker): + m = mocker.patch( + 'qutebrowser.completion.completiondelegate.{}'.format(klass), + autospec=True) + return m + + +@pytest.fixture +def mock_style_option(mocker): + """Create the CompletionView used for testing.""" + return _qt_mock('QStyleOptionViewItem', mocker) + + +@pytest.fixture +def mock_text_document(mocker): + """Create the CompletionView used for testing.""" + return _qt_mock('QTextDocument', mocker) + + +@pytest.fixture +def view(): + return mock.Mock() + + +@pytest.fixture +def delegate(mock_style_option, mock_text_document, config_stub, mocker, view): + _qt_mock('QStyle', mocker) + _qt_mock('QTextOption', mocker) + _qt_mock('QAbstractTextDocumentLayout', mocker) + completiondelegate._cached_stylesheet = mock.Mock() + delegate = completiondelegate.CompletionItemDelegate() + parent = mock.Mock() + parent.return_value = view + delegate.parent = parent + delegate.initStyleOption = mock.Mock() + delegate.setTextDirection = mock.Mock() + return delegate + + +@pytest.mark.parametrize('pat,txt_in,txt_out', [ + # { and } represent the open/close html tags for highlighting + ('foo', 'foo', '{foo}'), + ('foo', 'foobar', '{foo}bar'), + ('foo', 'barfoo', 'bar{foo}'), + ('foo', 'barfoobaz', 'bar{foo}baz'), + ('foo', 'barfoobazfoo', 'bar{foo}baz{foo}'), + ('foo', 'foofoo', '{foo}{foo}'), + ('a b', 'cadb', 'c{a}d{b}'), + ('foo', '', '<{foo}>'), + ('', "bc", '{<a>}bc'), + ('foo', "'foo'", "'{foo}'"), +]) +def test_paint(delegate, painter, view, mock_style_option, mock_text_document, + pat, txt_in, txt_out): + """Ensure set_model actually sets the model and expands all categories.""" + view.pattern = pat + mock_style_option().text = txt_in + index = mock.Mock() + index.column.return_value = 0 + index.model.return_value.columns_to_filter.return_value = [0] + opt = mock_style_option() + delegate.paint(painter, opt, index) + expected = txt_out.replace( + '{', '' + ).replace( + '}', '' + ) + mock_text_document().setHtml.assert_called_once_with(expected) From 4f99af5876a35ec23e91a2f94a7d44cd81269138 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sat, 15 Sep 2018 13:35:32 -0400 Subject: [PATCH 2/8] Don't escape quotes in completion text. Resolves the example case in #4199, but not the larger problem. We don't need to escape quotes as we don't put the string in an attribute value. From the docs at https://docs.python.org/3/library/html.html#html.escape: > If the optional flag quote is true, the characters (") and (') are also > translated; this helps for inclusion in an HTML attribute value > delimited by quotes, as in . Escaping quotes means we end up with a literal ' in the completion view wherever there is a quote in the source text. However, problem in #4199, where unexpected parts of the text are highlighted, can also happen with '<', '>', and '&', which still must be escaped. --- qutebrowser/completion/completiondelegate.py | 5 +++-- tests/unit/completion/test_completiondelegate.py | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/qutebrowser/completion/completiondelegate.py b/qutebrowser/completion/completiondelegate.py index 779906a83..076b99bed 100644 --- a/qutebrowser/completion/completiondelegate.py +++ b/qutebrowser/completion/completiondelegate.py @@ -203,8 +203,9 @@ class CompletionItemDelegate(QStyledItemDelegate): columns_to_filter = index.model().columns_to_filter(index) if index.column() in columns_to_filter and pattern: repl = r'\g<0>' - pat = html.escape(re.escape(pattern)).replace(r'\ ', r'|') - txt = html.escape(self._opt.text) + pat = html.escape(re.escape(pattern), quote=False).replace( + r'\ ', r'|') + txt = html.escape(self._opt.text, quote=False) text = re.sub(pat, repl, txt, flags=re.IGNORECASE) self._doc.setHtml(text) else: diff --git a/tests/unit/completion/test_completiondelegate.py b/tests/unit/completion/test_completiondelegate.py index b90c096a2..9d22002ee 100644 --- a/tests/unit/completion/test_completiondelegate.py +++ b/tests/unit/completion/test_completiondelegate.py @@ -86,7 +86,10 @@ def delegate(mock_style_option, mock_text_document, config_stub, mocker, view): ('a b', 'cadb', 'c{a}d{b}'), ('foo', '', '<{foo}>'), ('', "bc", '{<a>}bc'), - ('foo', "'foo'", "'{foo}'"), + + # https://github.com/qutebrowser/qutebrowser/issues/4199 + ('foo', "'foo'", "'{foo}'"), + ('x', "'x'", "'{x}'"), ]) def test_paint(delegate, painter, view, mock_style_option, mock_text_document, pat, txt_in, txt_out): From 102c6b99dd7c8380a0681ef3fe67bd645fae0d15 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sat, 15 Sep 2018 14:06:28 -0400 Subject: [PATCH 3/8] Don't highlight html escapes in completion. Resolves #4199. To avoid accidentally highlighting characters that were introduced by html escaping the text before feeding it to setHtml, we can't just escape the whole string before adding the highlighting. Instead, we need to break the string up on the pattern, format and escape the individual parts, then join them back together. re.escape includes empty strings if there is a match at the start/end, which ensures that matches always land on odd indices: https://docs.python.org/3/library/re.html#re.split > If there are capturing groups in the separator and it matches at the > start of the string, the result will start with an empty string. The > same holds for the end of the string --- qutebrowser/completion/completiondelegate.py | 15 ++++++++++----- tests/unit/completion/test_completiondelegate.py | 1 + 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/qutebrowser/completion/completiondelegate.py b/qutebrowser/completion/completiondelegate.py index 076b99bed..bc856d1a6 100644 --- a/qutebrowser/completion/completiondelegate.py +++ b/qutebrowser/completion/completiondelegate.py @@ -202,11 +202,16 @@ class CompletionItemDelegate(QStyledItemDelegate): pattern = view.pattern columns_to_filter = index.model().columns_to_filter(index) if index.column() in columns_to_filter and pattern: - repl = r'\g<0>' - pat = html.escape(re.escape(pattern), quote=False).replace( - r'\ ', r'|') - txt = html.escape(self._opt.text, quote=False) - text = re.sub(pat, repl, txt, flags=re.IGNORECASE) + pat = '({})'.format(re.escape(pattern).replace(r'\ ', r'|')) + parts = re.split(pat, self._opt.text, flags=re.IGNORECASE) + fmt = '{}' + escape = lambda s: html.escape(s, quote=False) + highlight = lambda s: fmt.format(escape(s)) + # matches are at every odd index + text = ''.join([ + highlight(s) if i % 2 == 1 else escape(s) + for i, s in enumerate(parts) + ]) self._doc.setHtml(text) else: self._doc.setPlainText(self._opt.text) diff --git a/tests/unit/completion/test_completiondelegate.py b/tests/unit/completion/test_completiondelegate.py index 9d22002ee..2118a4528 100644 --- a/tests/unit/completion/test_completiondelegate.py +++ b/tests/unit/completion/test_completiondelegate.py @@ -90,6 +90,7 @@ def delegate(mock_style_option, mock_text_document, config_stub, mocker, view): # https://github.com/qutebrowser/qutebrowser/issues/4199 ('foo', "'foo'", "'{foo}'"), ('x', "'x'", "'{x}'"), + ('lt', " Date: Sat, 15 Sep 2018 14:45:18 -0400 Subject: [PATCH 4/8] Remove unused imports in test_completiondelegate. --- tests/unit/completion/test_completiondelegate.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/unit/completion/test_completiondelegate.py b/tests/unit/completion/test_completiondelegate.py index 2118a4528..863f52b45 100644 --- a/tests/unit/completion/test_completiondelegate.py +++ b/tests/unit/completion/test_completiondelegate.py @@ -23,8 +23,6 @@ from unittest import mock import pytest -from PyQt5.QtCore import QModelIndex -from PyQt5.QtWidgets import QStyleOptionViewItem, QStyledItemDelegate from PyQt5.QtGui import QPainter from qutebrowser.completion import completiondelegate From 2eacf4bd94a9892c2786d25fc3f9fcbe8d696e5e Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Tue, 18 Sep 2018 20:40:06 -0400 Subject: [PATCH 5/8] Clean up completiondelegate tests. Respond to code review comments to reduce mocking and clean up comments. --- .../completion/test_completiondelegate.py | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/tests/unit/completion/test_completiondelegate.py b/tests/unit/completion/test_completiondelegate.py index 863f52b45..626085d30 100644 --- a/tests/unit/completion/test_completiondelegate.py +++ b/tests/unit/completion/test_completiondelegate.py @@ -16,13 +16,10 @@ # # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . - -"""Tests for the CompletionView Object.""" - from unittest import mock import pytest - +from PyQt5.QtCore import QModelIndex, QObject from PyQt5.QtGui import QPainter from qutebrowser.completion import completiondelegate @@ -30,32 +27,32 @@ from qutebrowser.completion import completiondelegate @pytest.fixture def painter(): - """Create the CompletionView used for testing.""" return mock.Mock(spec=QPainter) def _qt_mock(klass, mocker): - m = mocker.patch( - 'qutebrowser.completion.completiondelegate.{}'.format(klass), - autospec=True) + m = mocker.patch.object(completiondelegate, klass, autospec=True) return m @pytest.fixture def mock_style_option(mocker): - """Create the CompletionView used for testing.""" return _qt_mock('QStyleOptionViewItem', mocker) @pytest.fixture def mock_text_document(mocker): - """Create the CompletionView used for testing.""" return _qt_mock('QTextDocument', mocker) @pytest.fixture def view(): - return mock.Mock() + class FakeView(QObject): + def __init__(self): + super().__init__() + self.pattern = None + + return FakeView() @pytest.fixture @@ -63,13 +60,9 @@ def delegate(mock_style_option, mock_text_document, config_stub, mocker, view): _qt_mock('QStyle', mocker) _qt_mock('QTextOption', mocker) _qt_mock('QAbstractTextDocumentLayout', mocker) - completiondelegate._cached_stylesheet = mock.Mock() - delegate = completiondelegate.CompletionItemDelegate() - parent = mock.Mock() - parent.return_value = view - delegate.parent = parent + completiondelegate._cached_stylesheet = '' + delegate = completiondelegate.CompletionItemDelegate(parent=view) delegate.initStyleOption = mock.Mock() - delegate.setTextDirection = mock.Mock() return delegate @@ -92,10 +85,9 @@ def delegate(mock_style_option, mock_text_document, config_stub, mocker, view): ]) def test_paint(delegate, painter, view, mock_style_option, mock_text_document, pat, txt_in, txt_out): - """Ensure set_model actually sets the model and expands all categories.""" view.pattern = pat mock_style_option().text = txt_in - index = mock.Mock() + index = mock.Mock(spec=QModelIndex) index.column.return_value = 0 index.model.return_value.columns_to_filter.return_value = [0] opt = mock_style_option() From 0fed563a02955aa74cb833300a4f32ae7ef620fc Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sat, 22 Sep 2018 11:58:35 -0400 Subject: [PATCH 6/8] Use QSyntaxHighlighter for completion. This is a more "Qt" way of highlighting syntax, and works around the problems of #4199 without resorting to complicated html escaping. The tests are more straightforward with less mocking, but do involve testing a private class. --- qutebrowser/app.py | 4 - qutebrowser/completion/completiondelegate.py | 61 ++++-------- .../completion/test_completiondelegate.py | 95 +++++-------------- 3 files changed, 44 insertions(+), 116 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 594764c37..3359d07dc 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -59,7 +59,6 @@ except ImportError: import qutebrowser import qutebrowser.resources -from qutebrowser.completion import completiondelegate from qutebrowser.completion.models import miscmodels from qutebrowser.commands import cmdutils, runners, cmdexc from qutebrowser.config import config, websettings, configfiles, configinit @@ -452,9 +451,6 @@ def _init_modules(args, crash_handler): pre_text='Error initializing SQL') sys.exit(usertypes.Exit.err_init) - log.init.debug("Initializing completion...") - completiondelegate.init() - log.init.debug("Initializing command history...") cmdhistory.init() diff --git a/qutebrowser/completion/completiondelegate.py b/qutebrowser/completion/completiondelegate.py index bc856d1a6..6b1309378 100644 --- a/qutebrowser/completion/completiondelegate.py +++ b/qutebrowser/completion/completiondelegate.py @@ -28,13 +28,27 @@ import html from PyQt5.QtWidgets import QStyle, QStyleOptionViewItem, QStyledItemDelegate from PyQt5.QtCore import QRectF, QSize, Qt from PyQt5.QtGui import (QIcon, QPalette, QTextDocument, QTextOption, - QAbstractTextDocumentLayout) + QAbstractTextDocumentLayout, QSyntaxHighlighter, + QTextCharFormat) from qutebrowser.config import config -from qutebrowser.utils import qtutils, jinja +from qutebrowser.utils import qtutils -_cached_stylesheet = None +class _Highlighter(QSyntaxHighlighter): + + def __init__(self, doc, pattern, color): + super().__init__(doc) + self._format = QTextCharFormat() + self._format.setForeground(color) + self._pattern = pattern + + def highlightBlock(self, text): + """Override highlightBlock for custom highlighting.""" + for match in re.finditer(self._pattern, text): + start, end = match.span() + length = end - start + self.setFormat(start, length, self._format) class CompletionItemDelegate(QStyledItemDelegate): @@ -194,27 +208,15 @@ class CompletionItemDelegate(QStyledItemDelegate): self._doc.setDefaultTextOption(text_option) self._doc.setDocumentMargin(2) - assert _cached_stylesheet is not None - self._doc.setDefaultStyleSheet(_cached_stylesheet) - if index.parent().isValid(): view = self.parent() pattern = view.pattern columns_to_filter = index.model().columns_to_filter(index) + self._doc.setPlainText(self._opt.text) if index.column() in columns_to_filter and pattern: - pat = '({})'.format(re.escape(pattern).replace(r'\ ', r'|')) - parts = re.split(pat, self._opt.text, flags=re.IGNORECASE) - fmt = '{}' - escape = lambda s: html.escape(s, quote=False) - highlight = lambda s: fmt.format(escape(s)) - # matches are at every odd index - text = ''.join([ - highlight(s) if i % 2 == 1 else escape(s) - for i, s in enumerate(parts) - ]) - self._doc.setHtml(text) - else: - self._doc.setPlainText(self._opt.text) + pat = re.escape(pattern).replace(r'\ ', r'|') + _Highlighter(self._doc, pat, + config.val.colors.completion.match.fg) else: self._doc.setHtml( '{}'.format( @@ -289,24 +291,3 @@ class CompletionItemDelegate(QStyledItemDelegate): self._draw_focus_rect() self._painter.restore() - - -@config.change_filter('colors.completion.match.fg', function=True) -def _update_stylesheet(): - """Update the cached stylesheet.""" - stylesheet = """ - .highlight { - color: {{ conf.colors.completion.match.fg }}; - } - """ - with jinja.environment.no_autoescape(): - template = jinja.environment.from_string(stylesheet) - - global _cached_stylesheet - _cached_stylesheet = template.render(conf=config.val) - - -def init(): - """Initialize the cached stylesheet.""" - _update_stylesheet() - config.instance.changed.connect(_update_stylesheet) diff --git a/tests/unit/completion/test_completiondelegate.py b/tests/unit/completion/test_completiondelegate.py index 626085d30..f9f6dc492 100644 --- a/tests/unit/completion/test_completiondelegate.py +++ b/tests/unit/completion/test_completiondelegate.py @@ -19,82 +19,33 @@ from unittest import mock import pytest -from PyQt5.QtCore import QModelIndex, QObject -from PyQt5.QtGui import QPainter +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QTextDocument from qutebrowser.completion import completiondelegate -@pytest.fixture -def painter(): - return mock.Mock(spec=QPainter) - - -def _qt_mock(klass, mocker): - m = mocker.patch.object(completiondelegate, klass, autospec=True) - return m - - -@pytest.fixture -def mock_style_option(mocker): - return _qt_mock('QStyleOptionViewItem', mocker) - - -@pytest.fixture -def mock_text_document(mocker): - return _qt_mock('QTextDocument', mocker) - - -@pytest.fixture -def view(): - class FakeView(QObject): - def __init__(self): - super().__init__() - self.pattern = None - - return FakeView() - - -@pytest.fixture -def delegate(mock_style_option, mock_text_document, config_stub, mocker, view): - _qt_mock('QStyle', mocker) - _qt_mock('QTextOption', mocker) - _qt_mock('QAbstractTextDocumentLayout', mocker) - completiondelegate._cached_stylesheet = '' - delegate = completiondelegate.CompletionItemDelegate(parent=view) - delegate.initStyleOption = mock.Mock() - return delegate - - -@pytest.mark.parametrize('pat,txt_in,txt_out', [ - # { and } represent the open/close html tags for highlighting - ('foo', 'foo', '{foo}'), - ('foo', 'foobar', '{foo}bar'), - ('foo', 'barfoo', 'bar{foo}'), - ('foo', 'barfoobaz', 'bar{foo}baz'), - ('foo', 'barfoobazfoo', 'bar{foo}baz{foo}'), - ('foo', 'foofoo', '{foo}{foo}'), - ('a b', 'cadb', 'c{a}d{b}'), - ('foo', '', '<{foo}>'), - ('', "bc", '{<a>}bc'), +@pytest.mark.parametrize('pat,txt,segments', [ + ('foo', 'foo', [(0, 3)]), + ('foo', 'foobar', [(0, 3)]), + ('foo', 'barfoo', [(3, 3)]), + ('foo', 'barfoobaz', [(3, 3)]), + ('foo', 'barfoobazfoo', [(3, 3), (9, 3)]), + ('foo', 'foofoo', [(0, 3), (3, 3)]), + ('a|b', 'cadb', [(1, 1), (3, 1)]), + ('foo', '', [(1, 3)]), + ('', "bc", [(0, 3)]), # https://github.com/qutebrowser/qutebrowser/issues/4199 - ('foo', "'foo'", "'{foo}'"), - ('x', "'x'", "'{x}'"), - ('lt', "' - ).replace( - '}', '' - ) - mock_text_document().setHtml.assert_called_once_with(expected) +def test_highlight(pat, txt, segments): + doc = QTextDocument(txt) + highlighter = completiondelegate._Highlighter(doc, pat, Qt.red) + highlighter.setFormat = mock.Mock() + highlighter.highlightBlock(txt) + highlighter.setFormat.assert_has_calls([ + mock.call(s[0], s[1], mock.ANY) for s in segments + ]) From 66cc5f5ea493b4e1cab4053647db74e88924cd46 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Tue, 2 Oct 2018 17:30:43 -0400 Subject: [PATCH 7/8] Add support for more values in QtColor config type. Recent changes in the completion highlighter mandate that config.val.colors.completion.match be changed from a QssColor to a QtColor. However, the latter accepts fewer formats. To avoid breaking configs, this allows QtColors to be specified using all the same formats as QssColors, excluding gradients. I separated the QssColor and QtColor tests as the previous approach of generating the tests made adding tests for QtColor more complicated. While working on this I discovered that Qt's css parser is potentially broken around parsing hsv percentages and filed https://bugreports.qt.io/browse/QTBUG-70897. For consistency, I made our parser similarly broken. You can show the bug in qutebrowser right now by noting that the following have different effects: ``` set colors.completion.odd.bg 'hsv(100%, 100%, 100%)' set colors.completion.odd.bg 'hsv(358, 255, 255)' ``` --- qutebrowser/config/configtypes.py | 31 ++++++ tests/unit/config/test_configtypes.py | 149 +++++++++++++------------- 2 files changed, 106 insertions(+), 74 deletions(-) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 1c3bff8fb..842eeaf29 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -905,11 +905,42 @@ class QtColor(BaseType): * transparent (no color) """ + def _parse_value(self, val): + try: + return int(val) + except ValueError: + pass + + mult = 255.0 + if val.endswith('%'): + val = val[:-1] + mult = 255.0 / 100 + return int(float(val) * 255.0 / 100.0) + + try: + return int(float(val) * mult) + except ValueError: + raise configexc.ValidationError(val, "must be a valid color value") + def to_py(self, value): self._basic_py_validation(value, str) if not value: return None + if value.endswith(')'): + openparen = value.index('(') + kind = value[:openparen] + vals = value[openparen+1:-1].split(',') + vals = [self._parse_value(v) for v in vals] + if kind == 'rgba' and len(vals) == 4: + return QColor.fromRgb(*vals) + if kind == 'rgb' and len(vals) == 3: + return QColor.fromRgb(*vals) + if kind == 'hsva' and len(vals) == 4: + return QColor.fromHsv(*vals) + if kind == 'hsv' and len(vals) == 3: + return QColor.fromHsv(*vals) + color = QColor(value) if color.isValid(): return color diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index 46119be7e..83835faa0 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -1222,87 +1222,88 @@ class TestCommand: assert ('cmd2', "desc 2") in items -class ColorTests: +class TestQtColor: - """Generator for tests for TestColors.""" + """Test QtColor.""" - TYPES = [configtypes.QtColor, configtypes.QssColor] + @pytest.mark.parametrize('val, expected', [ + ('#123', QColor('#123')), + ('#112233', QColor('#112233')), + ('#111222333', QColor('#111222333')), + ('#111122223333', QColor('#111122223333')), + ('red', QColor('red')), - TESTS = [ - ('#123', TYPES), - ('#112233', TYPES), - ('#111222333', TYPES), - ('#111122223333', TYPES), - ('red', TYPES), + ('rgb(0, 0, 0)', QColor.fromRgb(0, 0, 0)), + ('rgb(0,0,0)', QColor.fromRgb(0, 0, 0)), - ('#00000G', []), - ('#123456789ABCD', []), - ('#12', []), - ('foobar', []), - ('42', []), - ('foo(1, 2, 3)', []), - ('rgb(1, 2, 3', []), + ('rgba(255, 255, 255, 1.0)', QColor.fromRgb(255, 255, 255, 255)), - ('rgb(0, 0, 0)', [configtypes.QssColor]), - ('rgb(0,0,0)', [configtypes.QssColor]), + # this should be (36, 25, 25) as hue goes to 359 + # however this is consistent with Qt's CSS parser + # https://bugreports.qt.io/browse/QTBUG-70897 + ('hsv(10%,10%,10%)', QColor.fromHsv(25, 25, 25)), + ]) + def test_valid(self, val, expected): + act = configtypes.QtColor().to_py(val) + print(expected.hue(), expected.saturation(), expected.value(), expected.alpha()) + print(act.hue(), act.saturation(), act.value(), act.alpha()) + assert configtypes.QtColor().to_py(val) == expected - ('rgba(255, 255, 255, 1.0)', [configtypes.QssColor]), - ('hsv(10%,10%,10%)', [configtypes.QssColor]), - - ('qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 white, ' - 'stop: 0.4 gray, stop:1 green)', [configtypes.QssColor]), - ('qconicalgradient(cx:0.5, cy:0.5, angle:30, stop:0 white, ' - 'stop:1 #00FF00)', [configtypes.QssColor]), - ('qradialgradient(cx:0, cy:0, radius: 1, fx:0.5, fy:0.5, ' - 'stop:0 white, stop:1 green)', [configtypes.QssColor]), - ] - - COMBINATIONS = list(itertools.product(TESTS, TYPES)) - - def __init__(self): - self.valid = list(self._generate_valid()) - self.invalid = list(self._generate_invalid()) - - def _generate_valid(self): - for (val, valid_classes), klass in self.COMBINATIONS: - if klass in valid_classes: - yield klass, val - - def _generate_invalid(self): - for (val, valid_classes), klass in self.COMBINATIONS: - if klass not in valid_classes: - yield klass, val - - -class TestColors: - - """Test QtColor/QssColor.""" - - TESTS = ColorTests() - - @pytest.fixture(params=ColorTests.TYPES) - def klass_fixt(self, request): - """Fixture which provides all ColorTests classes. - - Named klass_fix so it has a different name from the parametrized klass, - see https://github.com/pytest-dev/pytest/issues/979. - """ - return request.param - - def test_test_generator(self): - """Some sanity checks for ColorTests.""" - assert self.TESTS.valid - assert self.TESTS.invalid - - @pytest.mark.parametrize('klass, val', TESTS.valid) - def test_to_py_valid(self, klass, val): - expected = QColor(val) if klass is configtypes.QtColor else val - assert klass().to_py(val) == expected - - @pytest.mark.parametrize('klass, val', TESTS.invalid) - def test_to_py_invalid(self, klass, val): + @pytest.mark.parametrize('val', [ + '#00000G', + '#123456789ABCD', + '#12', + 'foobar', + '42', + 'foo(1, 2, 3)', + 'rgb(1, 2, 3', + ]) + def test_invalid(self, val): with pytest.raises(configexc.ValidationError): - klass().to_py(val) + configtypes.QtColor().to_py(val) + + +class TestQssColor: + + """Test QssColor.""" + + @pytest.mark.parametrize('val', [ + '#123', + '#112233', + '#111222333', + '#111122223333', + 'red', + + 'rgb(0, 0, 0)', + 'rgb(0,0,0)', + + 'rgba(255, 255, 255, 1.0)', + 'hsv(10%,10%,10%)', + + 'qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 white, ' + 'stop: 0.4 gray, stop:1 green)', + + 'qconicalgradient(cx:0.5, cy:0.5, angle:30, stop:0 white, ' + 'stop:1 #00FF00)', + + 'qradialgradient(cx:0, cy:0, radius: 1, fx:0.5, fy:0.5, ' + 'stop:0 white, stop:1 green)', + ]) + def test_valid(self, val): + assert configtypes.QssColor().to_py(val) == val + + @pytest.mark.parametrize('val', [ + '#00000G', + '#123456789ABCD', + '#12', + 'foobar', + '42', + 'foo(1, 2, 3)', + 'rgb(1, 2, 3', + ]) + def test_invalid(self, val): + with pytest.raises(configexc.ValidationError): + configtypes.QssColor().to_py(val) @attr.s From 46683b82e75bff12d7643432e2dc515cb6e1c77e Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Tue, 2 Oct 2018 17:47:33 -0400 Subject: [PATCH 8/8] Change colors.completion.match.fg to QtColor. The color is now used in a custom Highlighter class, not set in a stylesheet. --- qutebrowser/config/configdata.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index b7648a4be..ed78ff824 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -1798,7 +1798,7 @@ colors.completion.item.selected.border.bottom: colors.completion.match.fg: default: '#ff4444' - type: QssColor + type: QtColor desc: Foreground color of the matched text in the completion. colors.completion.scrollbar.fg: