Decouple categories from completionmodel.

Instead of add_list and add_sqltable, the completion model now supports
add_category, and callees either pass in a SqlCategory or ListCategory. This
makes unit testing much easier.

This also folds CompletionFilterModel into the ListCategory class.
This commit is contained in:
Ryan Roden-Corrent 2017-03-03 08:31:28 -05:00
parent ce3c555712
commit f95dff4d9e
19 changed files with 484 additions and 775 deletions

View File

@ -41,7 +41,7 @@ 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.completion.models import sortfilter, urlmodel, miscmodels
from qutebrowser.completion.models import urlmodel, miscmodels
class CommandDispatcher:
@ -1024,10 +1024,9 @@ class CommandDispatcher:
int(part)
except ValueError:
model = miscmodels.buffer()
sf = sortfilter.CompletionFilterModel(source=model)
sf.set_pattern(index)
if sf.count() > 0:
index = sf.data(sf.first_item())
model.set_pattern(index)
if model.count() > 0:
index = model.data(model.first_item())
index_parts = index.split('/', 1)
else:
raise cmdexc.CommandError(

View File

@ -275,6 +275,7 @@ class CompletionView(QTreeView):
self.hide()
return
model.setParent(self)
old_model = self.model()
if model is not old_model:
sel_model = self.selectionModel()

View File

@ -24,7 +24,6 @@ import re
from PyQt5.QtCore import Qt, QModelIndex, QAbstractItemModel
from qutebrowser.utils import log, qtutils
from qutebrowser.completion.models import sortfilter, listcategory, sqlcategory
class CompletionModel(QAbstractItemModel):
@ -65,33 +64,8 @@ class CompletionModel(QAbstractItemModel):
return self._categories[index.row()]
return None
def add_list(self, name, items):
"""Add a list of items as a completion category.
Args:
name: Title of the category.
items: List of tuples.
"""
cat = listcategory.ListCategory(name, items, parent=self)
filtermodel = sortfilter.CompletionFilterModel(cat,
self.columns_to_filter)
self._categories.append(filtermodel)
def add_sqltable(self, name, *, select='*', where=None, sort_by=None,
sort_order=None):
"""Create a new completion category and add it to this model.
Args:
name: Name of category, and the table in the database.
select: A custom result column expression for the select statement.
where: An optional clause to filter out some rows.
sort_by: The name of the field to sort by, or None for no sorting.
sort_order: Either 'asc' or 'desc', if sort_by is non-None
"""
cat = sqlcategory.SqlCategory(name, parent=self, sort_by=sort_by,
sort_order=sort_order,
select=select, where=where,
columns_to_filter=self.columns_to_filter)
def add_category(self, cat):
"""Add a completion category to the model."""
self._categories.append(cat)
def data(self, index, role=Qt.DisplayRole):
@ -210,7 +184,7 @@ class CompletionModel(QAbstractItemModel):
# TODO: should pattern be saved in the view layer instead?
self.pattern = pattern
for cat in self._categories:
cat.set_pattern(pattern)
cat.set_pattern(pattern, self.columns_to_filter)
def first_item(self):
"""Return the index of the first child (non-category) in the model."""

View File

@ -20,7 +20,7 @@
"""Functions that return config-related completion models."""
from qutebrowser.config import configdata, configexc
from qutebrowser.completion.models import completionmodel
from qutebrowser.completion.models import completionmodel, listcategory
from qutebrowser.utils import objreg
@ -29,7 +29,7 @@ def section():
model = completionmodel.CompletionModel(column_widths=(20, 70, 10))
sections = ((name, configdata.SECTION_DESC[name].splitlines()[0].strip())
for name in configdata.DATA)
model.add_list("Sections", sections)
model.add_category(listcategory.ListCategory("Sections", sections))
return model
@ -57,7 +57,7 @@ def option(sectname):
config = objreg.get('config')
val = config.get(sectname, name, raw=True)
options.append((name, desc, val))
model.add_list(sectname, options)
model.add_category(listcategory.ListCategory(sectname, options))
return model
@ -88,8 +88,9 @@ def value(sectname, optname):
# Different type for each value (KeyValue)
vals = configdata.DATA[sectname][optname].typ.complete()
model.add_list("Current/Default", [(current, "Current value"),
(default, "Default value")])
cur_cat = listcategory.ListCategory("Current/Default",
[(current, "Current value"), (default, "Default value")])
model.add_category(cur_cat)
if vals is not None:
model.add_list("Completions", vals)
model.add_category(listcategory.ListCategory("Completions", vals))
return model

View File

@ -23,44 +23,100 @@ Module attributes:
Role: An enum of user defined model roles.
"""
from PyQt5.QtCore import Qt
import re
from PyQt5.QtCore import Qt, QSortFilterProxyModel, QModelIndex
from PyQt5.QtGui import QStandardItem, QStandardItemModel
from qutebrowser.utils import qtutils, debug, log
class ListCategory(QStandardItemModel):
class ListCategory(QSortFilterProxyModel):
"""Expose a list of items as a category for the CompletionModel."""
def __init__(self, name, items, parent=None):
super().__init__(parent)
self.name = name
# self.setColumnCount(3) TODO needed?
# TODO: batch insert?
# TODO: can I just insert a tuple instead of a list?
self.srcmodel = QStandardItemModel(parent=self)
self.pattern = ''
self.pattern_re = None
for item in items:
self.appendRow([QStandardItem(x) for x in item])
self.srcmodel.appendRow([QStandardItem(x) for x in item])
self.setSourceModel(self.srcmodel)
def flags(self, index):
"""Return the item flags for index.
Override QAbstractItemModel::flags.
def set_pattern(self, val, columns_to_filter):
"""Setter for pattern.
Args:
index: The QModelIndex to get item flags for.
val: The value to set.
"""
with debug.log_time(log.completion, 'Setting filter pattern'):
self.columns_to_filter = columns_to_filter
self.pattern = val
val = re.sub(r' +', r' ', val) # See #1919
val = re.escape(val)
val = val.replace(r'\ ', '.*')
self.pattern_re = re.compile(val, re.IGNORECASE)
self.invalidate()
sortcol = 0
self.sort(sortcol)
def filterAcceptsRow(self, row, parent):
"""Custom filter implementation.
Override QSortFilterProxyModel::filterAcceptsRow.
Args:
row: The row of the item.
parent: The parent item QModelIndex.
Return:
The item flags, or Qt.NoItemFlags on error.
True if self.pattern is contained in item, or if it's a root item
(category). False in all other cases
"""
if not index.isValid():
return
if not self.pattern:
return True
if index.parent().isValid():
# item
return (Qt.ItemIsEnabled | Qt.ItemIsSelectable |
Qt.ItemNeverHasChildren)
for col in self.columns_to_filter:
idx = self.srcmodel.index(row, col, parent)
if not idx.isValid(): # pragma: no cover
# this is a sanity check not hit by any test case
continue
data = self.srcmodel.data(idx)
if not data:
continue
elif self.pattern_re.search(data):
return True
return False
def lessThan(self, lindex, rindex):
"""Custom sorting implementation.
Prefers all items which start with self.pattern. Other than that, uses
normal Python string sorting.
Args:
lindex: The QModelIndex of the left item (*left* < right)
rindex: The QModelIndex of the right item (left < *right*)
Return:
True if left < right, else False
"""
qtutils.ensure_valid(lindex)
qtutils.ensure_valid(rindex)
left = self.srcmodel.data(lindex)
right = self.srcmodel.data(rindex)
leftstart = left.startswith(self.pattern)
rightstart = right.startswith(self.pattern)
if leftstart and rightstart:
return left < right
elif leftstart:
return True
elif rightstart:
return False
else:
# category
return Qt.NoItemFlags
def set_pattern(self, pattern):
pass
return left < right

View File

@ -22,14 +22,14 @@
from qutebrowser.config import config, configdata
from qutebrowser.utils import objreg, log, qtutils
from qutebrowser.commands import cmdutils
from qutebrowser.completion.models import completionmodel
from qutebrowser.completion.models import completionmodel, listcategory
def command():
"""A CompletionModel filled with non-hidden commands and descriptions."""
model = completionmodel.CompletionModel(column_widths=(20, 60, 20))
cmdlist = _get_cmd_completions(include_aliases=True, include_hidden=False)
model.add_list("Commands", cmdlist)
model.add_category(listcategory.ListCategory("Commands", cmdlist))
return model
@ -53,22 +53,24 @@ def helptopic():
name = '{}->{}'.format(sectname, optname)
settings.append((name, desc))
model.add_list("Commands", cmdlist)
model.add_list("Settings", settings)
model.add_category(listcategory.ListCategory("Commands", cmdlist))
model.add_category(listcategory.ListCategory("Settings", settings))
return model
def quickmark():
"""A CompletionModel filled with all quickmarks."""
model = completionmodel.CompletionModel(column_widths=(30, 70, 0))
model.add_list('Quickmarks', objreg.get('quickmark-manager').marks.items())
marks = objreg.get('quickmark-manager').marks.items()
model.add_category(listcategory.ListCategory('Quickmarks', marks))
return model
def bookmark():
"""A CompletionModel filled with all bookmarks."""
model = completionmodel.CompletionModel(column_widths=(30, 70, 0))
model.add_list('Bookmarks', objreg.get('bookmark-manager').marks.items())
marks = objreg.get('bookmark-manager').marks.items()
model.add_category(listcategory.ListCategory('Bookmarks', marks))
return model
@ -79,7 +81,7 @@ def session():
manager = objreg.get('session-manager')
sessions = ((name,) for name in manager.list_sessions()
if not name.startswith('_'))
model.add_list("Sessions", sessions)
model.add_category(listcategory.ListCategory("Sessions", sessions))
except OSError:
log.completion.exception("Failed to list sessions!")
return model
@ -122,7 +124,8 @@ def buffer():
tabs.append(("{}/{}".format(win_id, idx + 1),
tab.url().toDisplayString(),
tabbed_browser.page_title(idx)))
model.add_list("{}".format(win_id), tabs)
cat = listcategory.ListCategory("{}".format(win_id), tabs)
model.add_category(cat)
return model
@ -135,7 +138,7 @@ def bind(_key):
# TODO: offer a 'Current binding' completion based on the key.
model = completionmodel.CompletionModel(column_widths=(20, 60, 20))
cmdlist = _get_cmd_completions(include_hidden=True, include_aliases=True)
model.add_list("Commands", cmdlist)
model.add_category(listcategory.ListCategory("Commands", cmdlist))
return model

View File

@ -1,133 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# 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 <http://www.gnu.org/licenses/>.
"""A filtering/sorting base model for completions.
Contains:
CompletionFilterModel -- A QSortFilterProxyModel subclass for completions.
"""
import re
from PyQt5.QtCore import QSortFilterProxyModel, QModelIndex
from qutebrowser.utils import log, qtutils, debug
class CompletionFilterModel(QSortFilterProxyModel):
"""Subclass of QSortFilterProxyModel with custom filtering.
Attributes:
pattern: The pattern to filter with.
srcmodel: The current source model.
Kept as attribute because calling `sourceModel` takes quite
a long time for some reason.
"""
def __init__(self, source, columns_to_filter, parent=None):
super().__init__(parent)
super().setSourceModel(source)
self.srcmodel = source
self.pattern = ''
self.pattern_re = None
self.columns_to_filter = columns_to_filter
self.name = source.name
def set_pattern(self, val):
"""Setter for pattern.
Args:
val: The value to set.
"""
with debug.log_time(log.completion, 'Setting filter pattern'):
self.pattern = val
val = re.sub(r' +', r' ', val) # See #1919
val = re.escape(val)
val = val.replace(r'\ ', '.*')
self.pattern_re = re.compile(val, re.IGNORECASE)
self.invalidate()
sortcol = 0
self.sort(sortcol)
def setSourceModel(self, model):
"""Override QSortFilterProxyModel's setSourceModel to clear pattern."""
log.completion.debug("Setting source model: {}".format(model))
self.set_pattern('')
super().setSourceModel(model)
self.srcmodel = model
def filterAcceptsRow(self, row, parent):
"""Custom filter implementation.
Override QSortFilterProxyModel::filterAcceptsRow.
Args:
row: The row of the item.
parent: The parent item QModelIndex.
Return:
True if self.pattern is contained in item, or if it's a root item
(category). False in all other cases
"""
if not self.pattern:
return True
for col in self.columns_to_filter:
idx = self.srcmodel.index(row, col, parent)
if not idx.isValid(): # pragma: no cover
# this is a sanity check not hit by any test case
continue
data = self.srcmodel.data(idx)
if not data:
continue
elif self.pattern_re.search(data):
return True
return False
def lessThan(self, lindex, rindex):
"""Custom sorting implementation.
Prefers all items which start with self.pattern. Other than that, uses
normal Python string sorting.
Args:
lindex: The QModelIndex of the left item (*left* < right)
rindex: The QModelIndex of the right item (left < *right*)
Return:
True if left < right, else False
"""
qtutils.ensure_valid(lindex)
qtutils.ensure_valid(rindex)
left = self.srcmodel.data(lindex)
right = self.srcmodel.data(rindex)
leftstart = left.startswith(self.pattern)
rightstart = right.startswith(self.pattern)
if leftstart and rightstart:
return left < right
elif leftstart:
return True
elif rightstart:
return False
else:
return left < right

View File

@ -29,36 +29,48 @@ from qutebrowser.misc import sql
class SqlCategory(QSqlQueryModel):
"""Wraps a SqlQuery for use as a completion category."""
def __init__(self, name, *, sort_by, sort_order, select, where,
columns_to_filter, parent=None):
def __init__(self, name, *, sort_by=None, sort_order=None, select='*',
where=None, parent=None):
"""Create a new completion category backed by a sql table.
Args:
name: Name of category, and the table in the database.
select: A custom result column expression for the select statement.
where: An optional clause to filter out some rows.
sort_by: The name of the field to sort by, or None for no sorting.
sort_order: Either 'asc' or 'desc', if sort_by is non-None
"""
super().__init__(parent=parent)
self.name = name
self._sort_by = sort_by
self._sort_order = sort_order
self._select = select
self._where = where
self.set_pattern('', [0])
query = sql.run_query('select * from {} limit 1'.format(name))
self._fields = [query.record().fieldName(i) for i in columns_to_filter]
def set_pattern(self, pattern, columns_to_filter):
query = sql.run_query('select * from {} limit 1'.format(self.name))
fields = [query.record().fieldName(i) for i in columns_to_filter]
querystr = 'select {} from {} where ('.format(select, name)
querystr = 'select {} from {} where ('.format(self._select, self.name)
# the incoming pattern will have literal % and _ escaped with '\'
# we need to tell sql to treat '\' as an escape character
querystr += ' or '.join("{} like ? escape '\\'".format(f)
for f in self._fields)
for f in fields)
querystr += ')'
if where:
querystr += ' and ' + where
if self._where:
querystr += ' and ' + self._where
if sort_by:
assert sort_order == 'asc' or sort_order == 'desc'
querystr += ' order by {} {}'.format(sort_by, sort_order)
if self._sort_by:
assert self._sort_order in ['asc', 'desc']
querystr += ' order by {} {}'.format(self._sort_by,
self._sort_order)
self._querystr = querystr
self.set_pattern('')
def set_pattern(self, pattern):
# escape to treat a user input % or _ as a literal, not a wildcard
pattern = pattern.replace('%', '\\%')
pattern = pattern.replace('_', '\\_')
# treat spaces as wildcards to match any of the typed words
pattern = re.sub(r' +', '%', pattern)
pattern = '%{}%'.format(pattern)
query = sql.run_query(self._querystr, [pattern for _ in self._fields])
query = sql.run_query(querystr, [pattern for _ in fields])
self.setQuery(query)

View File

@ -19,7 +19,8 @@
"""Function to return the url completion model for the `open` command."""
from qutebrowser.completion.models import completionmodel
from qutebrowser.completion.models import (completionmodel, listcategory,
sqlcategory)
from qutebrowser.config import config
from qutebrowser.utils import qtutils, log, objreg
@ -46,8 +47,7 @@ def _delete_url(completion):
log.completion.debug('Deleting bookmark {}'.format(urlstr))
bookmark_manager = objreg.get('bookmark-manager')
bookmark_manager.delete(urlstr)
else:
assert catname == 'Quickmarks', 'Unknown category {}'.format(catname)
elif catname == 'Quickmarks':
quickmark_manager = objreg.get('quickmark-manager')
sibling = index.sibling(index.row(), _TEXTCOL)
qtutils.ensure_valid(sibling)
@ -66,15 +66,17 @@ def url():
columns_to_filter=[_URLCOL, _TEXTCOL],
delete_cur_item=_delete_url)
quickmarks = objreg.get('quickmark-manager').marks.items()
model.add_list('Quickmarks', ((url, name) for (name, url) in quickmarks))
quickmarks = ((url, name) for (name, url)
in objreg.get('quickmark-manager').marks.items())
bookmarks = objreg.get('bookmark-manager').marks.items()
model.add_list('Bookmarks', objreg.get('bookmark-manager').marks.items())
model.add_category(listcategory.ListCategory('Quickmarks', quickmarks))
model.add_category(listcategory.ListCategory('Bookmarks', bookmarks))
timefmt = config.get('completion', 'timestamp-format')
select_time = "strftime('{}', atime, 'unixepoch')".format(timefmt)
model.add_sqltable('History',
sort_order='desc', sort_by='atime',
select='url, title, {}'.format(select_time),
where='not redirect')
hist_cat = sqlcategory.SqlCategory(
'History', sort_order='desc', sort_by='atime',
select='url, title, {}'.format(select_time), where='not redirect')
model.add_category(hist_cat)
return model

View File

@ -158,8 +158,6 @@ PERFECT_FILES = [
'completion/models/base.py'),
('tests/unit/completion/test_models.py',
'completion/models/urlmodel.py'),
('tests/unit/completion/test_sortfilter.py',
'completion/models/sortfilter.py'),
]

View File

@ -239,6 +239,24 @@ def host_blocker_stub(stubs):
objreg.delete('host-blocker')
@pytest.fixture
def quickmark_manager_stub(stubs):
"""Fixture which provides a fake quickmark manager object."""
stub = stubs.QuickmarkManagerStub()
objreg.register('quickmark-manager', stub)
yield stub
objreg.delete('quickmark-manager')
@pytest.fixture
def bookmark_manager_stub(stubs):
"""Fixture which provides a fake bookmark manager object."""
stub = stubs.BookmarkManagerStub()
objreg.register('bookmark-manager', stub)
yield stub
objreg.delete('bookmark-manager')
@pytest.fixture
def session_manager_stub(stubs):
"""Fixture which provides a fake web-history object."""

View File

@ -193,11 +193,10 @@ def test_update_completion(txt, kind, pattern, pos_args, status_command_stub,
completer_obj.schedule_completion_update()
assert completion_widget_stub.set_model.call_count == 1
args = completion_widget_stub.set_model.call_args[0]
# the outer model is just for sorting; srcmodel is the completion model
if kind is None:
assert args[0] is None
else:
model = args[0].srcmodel
model = args[0]
assert model.kind == kind
assert model.pos_args == pos_args
assert args[1] == pattern

View File

@ -19,128 +19,59 @@
"""Tests for CompletionModel."""
import sys
import pytest
import hypothesis
from unittest import mock
from hypothesis import strategies
from qutebrowser.completion.models import completionmodel, sortfilter
from qutebrowser.completion.models import completionmodel
def _create_model(data, filter_cols=None):
"""Create a completion model populated with the given data.
data: A list of lists, where each sub-list represents a category, each
tuple in the sub-list represents an item, and each value in the
tuple represents the item data for that column
filter_cols: Columns to filter, or None for default.
"""
model = completionmodel.CompletionModel(columns_to_filter=filter_cols)
for catdata in data:
model.add_list('', catdata)
return model
@hypothesis.given(strategies.lists(min_size=0, max_size=3,
elements=strategies.integers(min_value=0, max_value=2**31)))
def test_first_last_item(counts):
"""Test that first() and last() index to the first and last items."""
model = completionmodel.CompletionModel()
for c in counts:
cat = mock.Mock()
cat.rowCount = mock.Mock(return_value=c)
model.add_category(cat)
nonempty = [i for i, rowCount in enumerate(counts) if rowCount > 0]
if not nonempty:
# with no items, first and last should be an invalid index
assert not model.first_item().isValid()
assert not model.last_item().isValid()
else:
first = nonempty[0]
last = nonempty[-1]
# first item of the first nonempty category
assert model.first_item().row() == 0
assert model.first_item().parent().row() == first
# last item of the last nonempty category
assert model.last_item().row() == counts[last] - 1
assert model.last_item().parent().row() == last
def _extract_model_data(model):
"""Express a model's data as a list for easier comparison.
Return: A list of lists, where each sub-list represents a category, each
tuple in the sub-list represents an item, and each value in the
tuple represents the item data for that column
"""
data = []
for i in range(0, model.rowCount()):
cat_idx = model.index(i, 0)
row = []
for j in range(0, model.rowCount(cat_idx)):
row.append((model.data(cat_idx.child(j, 0)),
model.data(cat_idx.child(j, 1)),
model.data(cat_idx.child(j, 2))))
data.append(row)
return data
@hypothesis.given(strategies.lists(elements=strategies.integers(),
min_size=0, max_size=3))
def test_count(counts):
model = completionmodel.CompletionModel()
for c in counts:
cat = mock.Mock(spec=['rowCount'])
cat.rowCount = mock.Mock(return_value=c)
model.add_category(cat)
assert model.count() == sum(counts)
@pytest.mark.parametrize('tree, first, last', [
([[('Aa',)]], 'Aa', 'Aa'),
([[('Aa',)], [('Ba',)]], 'Aa', 'Ba'),
([[('Aa',), ('Ab',), ('Ac',)], [('Ba',), ('Bb',)], [('Ca',)]],
'Aa', 'Ca'),
([[], [('Ba',)]], 'Ba', 'Ba'),
([[], [], [('Ca',)]], 'Ca', 'Ca'),
([[], [], [('Ca',), ('Cb',)]], 'Ca', 'Cb'),
([[('Aa',)], []], 'Aa', 'Aa'),
([[('Aa',)], []], 'Aa', 'Aa'),
([[('Aa',)], [], []], 'Aa', 'Aa'),
([[('Aa',)], [], [('Ca',)]], 'Aa', 'Ca'),
([[], []], None, None),
])
def test_first_last_item(tree, first, last):
"""Test that first() and last() return indexes to the first and last items.
Args:
tree: Each list represents a completion category, with each string
being an item under that category.
first: text of the first item
last: text of the last item
"""
model = _create_model(tree)
assert model.data(model.first_item()) == first
assert model.data(model.last_item()) == last
@pytest.mark.parametrize('tree, expected', [
([[('Aa',)]], 1),
([[('Aa',)], [('Ba',)]], 2),
([[('Aa',), ('Ab',), ('Ac',)], [('Ba',), ('Bb',)], [('Ca',)]], 6),
([[], [('Ba',)]], 1),
([[], [], [('Ca',)]], 1),
([[], [], [('Ca',), ('Cb',)]], 2),
([[('Aa',)], []], 1),
([[('Aa',)], []], 1),
([[('Aa',)], [], []], 1),
([[('Aa',)], [], [('Ca',)]], 2),
])
def test_count(tree, expected):
model = _create_model(tree)
assert model.count() == expected
@pytest.mark.parametrize('pattern, filter_cols, before, after', [
('foo', [0],
[[('foo', '', ''), ('bar', '', '')]],
[[('foo', '', '')]]),
('foo', [0],
[[('foob', '', ''), ('fooc', '', ''), ('fooa', '', '')]],
[[('fooa', '', ''), ('foob', '', ''), ('fooc', '', '')]]),
('foo', [0],
[[('foo', '', '')], [('bar', '', '')]],
[[('foo', '', '')], []]),
# prefer foobar as it starts with the pattern
('foo', [0],
[[('barfoo', '', ''), ('foobar', '', '')]],
[[('foobar', '', ''), ('barfoo', '', '')]]),
# however, don't rearrange categories
('foo', [0],
[[('barfoo', '', '')], [('foobar', '', '')]],
[[('barfoo', '', '')], [('foobar', '', '')]]),
('foo', [1],
[[('foo', 'bar', ''), ('bar', 'foo', '')]],
[[('bar', 'foo', '')]]),
('foo', [0, 1],
[[('foo', 'bar', ''), ('bar', 'foo', ''), ('bar', 'bar', '')]],
[[('foo', 'bar', ''), ('bar', 'foo', '')]]),
('foo', [0, 1, 2],
[[('foo', '', ''), ('bar', '')]],
[[('foo', '', '')]]),
])
def test_set_pattern(pattern, filter_cols, before, after):
@hypothesis.given(strategies.text())
def test_set_pattern(pat):
"""Validate the filtering and sorting results of set_pattern."""
# TODO: just test that it calls the mock on its child categories
model = _create_model(before, filter_cols)
model.set_pattern(pattern)
actual = _extract_model_data(model)
assert actual == after
cols = [1, 2, 3]
model = completionmodel.CompletionModel(columns_to_filter=cols)
cats = [mock.Mock(spec=['set_pattern'])] * 3
for c in cats:
c.set_pattern = mock.Mock()
model.add_category(c)
model.set_pattern(pat)
assert all(c.set_pattern.called_with([pat, cols]) for c in cats)

View File

@ -19,13 +19,13 @@
"""Tests for the CompletionView Object."""
import unittest.mock
from unittest import mock
import pytest
from PyQt5.QtGui import QStandardItem, QColor
from qutebrowser.completion import completionwidget
from qutebrowser.completion.models import completionmodel
from qutebrowser.completion.models import completionmodel, listcategory
from qutebrowser.commands import cmdexc
@ -70,19 +70,11 @@ def completionview(qtbot, status_command_stub, config_stub, win_registry,
return view
@pytest.fixture
def simplemodel(completionview):
"""A completion model with one item."""
model = completionmodel.CompletionModel()
model.add_list('', [('foo'),])
return model
def test_set_model(completionview):
"""Ensure set_model actually sets the model and expands all categories."""
model = completionmodel.CompletionModel()
for i in range(3):
model.add_list(str(i), [('foo',)])
cat = listcategory.ListCategory('', [('foo',)])
completionview.set_model(model)
assert completionview.model() is model
for i in range(model.rowCount()):
@ -91,7 +83,7 @@ def test_set_model(completionview):
def test_set_pattern(completionview):
model = completionmodel.CompletionModel()
model.set_pattern = unittest.mock.Mock()
model.set_pattern = mock.Mock()
completionview.set_model(model, 'foo')
model.set_pattern.assert_called_with('foo')
@ -158,7 +150,8 @@ def test_completion_item_focus(which, tree, expected, completionview, qtbot):
"""
model = completionmodel.CompletionModel()
for catdata in tree:
model.add_list('', (x,) for x in catdata)
cat = listcategory.ListCategory('', ((x,) for x in catdata))
model.add_category(cat)
completionview.set_model(model)
for entry in expected:
if entry is None:
@ -203,7 +196,8 @@ def test_completion_show(show, rows, quick_complete, completionview,
model = completionmodel.CompletionModel()
for name in rows:
model.add_list('', [(name,)])
cat = listcategory.ListCategory('', [(name,)])
model.add_category(cat)
assert not completionview.isVisible()
completionview.set_model(model)
@ -217,27 +211,33 @@ def test_completion_show(show, rows, quick_complete, completionview,
assert not completionview.isVisible()
def test_completion_item_del(completionview, simplemodel):
def test_completion_item_del(completionview):
"""Test that completion_item_del invokes delete_cur_item in the model."""
simplemodel.srcmodel.delete_cur_item = unittest.mock.Mock()
completionview.set_model(simplemodel)
func = mock.Mock()
model = completionmodel.CompletionModel(delete_cur_item=func)
model.add_category(listcategory.ListCategory('', [('foo',)]))
completionview.set_model(model)
completionview.completion_item_focus('next')
completionview.completion_item_del()
assert simplemodel.srcmodel.delete_cur_item.called
assert func.called
def test_completion_item_del_no_selection(completionview, simplemodel):
def test_completion_item_del_no_selection(completionview):
"""Test that completion_item_del with no selected index."""
simplemodel.srcmodel.delete_cur_item = unittest.mock.Mock()
completionview.set_model(simplemodel)
func = mock.Mock()
model = completionmodel.CompletionModel(delete_cur_item=func)
model.add_category(listcategory.ListCategory('', [('foo',)]))
completionview.set_model(model)
with pytest.raises(cmdexc.CommandError):
completionview.completion_item_del()
assert not simplemodel.srcmodel.delete_cur_item.called
assert not func.called
def test_completion_item_del_no_func(completionview, simplemodel):
def test_completion_item_del_no_func(completionview):
"""Test completion_item_del with no delete_cur_item in the model."""
completionview.set_model(simplemodel)
model = completionmodel.CompletionModel()
model.add_category(listcategory.ListCategory('', [('foo',)]))
completionview.set_model(model)
completionview.completion_item_focus('next')
with pytest.raises(cmdexc.CommandError):
completionview.completion_item_del()

View File

@ -0,0 +1,70 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# 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 <http://www.gnu.org/licenses/>.
"""Tests for CompletionFilterModel."""
import pytest
from qutebrowser.completion.models import listcategory
def _validate(cat, expected):
"""Check that a category contains the expected items in the given order.
Args:
cat: The category to inspect.
expected: A list of tuples containing the expected items.
"""
assert cat.rowCount() == len(expected)
for row, items in enumerate(expected):
for col, item in enumerate(items):
assert cat.data(cat.index(row, col)) == item
@pytest.mark.parametrize('pattern, filter_cols, before, after', [
('foo', [0],
[('foo', '', ''), ('bar', '', '')],
[('foo', '', '')]),
('foo', [0],
[('foob', '', ''), ('fooc', '', ''), ('fooa', '', '')],
[('fooa', '', ''), ('foob', '', ''), ('fooc', '', '')]),
# prefer foobar as it starts with the pattern
('foo', [0],
[('barfoo', '', ''), ('foobar', '', '')],
[('foobar', '', ''), ('barfoo', '', '')]),
('foo', [1],
[('foo', 'bar', ''), ('bar', 'foo', '')],
[('bar', 'foo', '')]),
('foo', [0, 1],
[('foo', 'bar', ''), ('bar', 'foo', ''), ('bar', 'bar', '')],
[('foo', 'bar', ''), ('bar', 'foo', '')]),
('foo', [0, 1, 2],
[('foo', '', ''), ('bar', '')],
[('foo', '', '')]),
])
def test_set_pattern(pattern, filter_cols, before, after):
"""Validate the filtering and sorting results of set_pattern."""
cat = listcategory.ListCategory('Foo', before)
cat.set_pattern(pattern, filter_cols)
_validate(cat, after)

View File

@ -134,27 +134,25 @@ def _mock_view_index(model, category_num, child_num, qtbot):
@pytest.fixture
def quickmarks(init_sql):
"""Pre-populate the quickmark database."""
table = sql.SqlTable('Quickmarks', ['name', 'url'], primary_key='name')
table.insert(['aw', 'https://wiki.archlinux.org'])
table.insert(['ddg', 'https://duckduckgo.com'])
table.insert(['wiki', 'https://wikipedia.org'])
objreg.register('quickmark-manager', table)
yield table
objreg.delete('quickmark-manager')
def quickmarks(quickmark_manager_stub):
"""Pre-populate the quickmark-manager stub with some quickmarks."""
quickmark_manager_stub.marks = collections.OrderedDict([
('aw', 'https://wiki.archlinux.org'),
('ddg', 'https://duckduckgo.com'),
('wiki', 'https://wikipedia.org'),
])
return quickmark_manager_stub
@pytest.fixture
def bookmarks(init_sql):
"""Pre-populate the bookmark database."""
table = sql.SqlTable('Bookmarks', ['url', 'title'], primary_key='url')
table.insert(['https://github.com', 'GitHub'])
table.insert(['https://python.org', 'Welcome to Python.org'])
table.insert(['http://qutebrowser.org', 'qutebrowser | qutebrowser'])
objreg.register('bookmark-manager', table)
yield table
objreg.delete('bookmark-manager')
def bookmarks(bookmark_manager_stub):
"""Pre-populate the bookmark-manager stub with some quickmarks."""
bookmark_manager_stub.marks = collections.OrderedDict([
('https://github.com', 'GitHub'),
('https://python.org', 'Welcome to Python.org'),
('http://qutebrowser.org', 'qutebrowser | qutebrowser'),
])
return bookmark_manager_stub
@pytest.fixture
@ -315,9 +313,9 @@ def test_url_completion_delete_bookmark(qtmodeltester, config_stub,
# delete item (1, 0) -> (bookmarks, 'https://github.com' )
view = _mock_view_index(model, 1, 0, qtbot)
model.delete_cur_item(view)
assert 'https://github.com' not in bookmarks
assert 'https://python.org' in bookmarks
assert 'http://qutebrowser.org' in bookmarks
assert 'https://github.com' not in bookmarks.marks
assert 'https://python.org' in bookmarks.marks
assert 'http://qutebrowser.org' in bookmarks.marks
def test_url_completion_delete_quickmark(qtmodeltester, config_stub,
@ -332,9 +330,9 @@ def test_url_completion_delete_quickmark(qtmodeltester, config_stub,
# delete item (0, 1) -> (quickmarks, 'ddg' )
view = _mock_view_index(model, 0, 1, qtbot)
model.delete_cur_item(view)
assert 'aw' in quickmarks
assert 'ddg' not in quickmarks
assert 'wiki' in quickmarks
assert 'aw' in quickmarks.marks
assert 'ddg' not in quickmarks.marks
assert 'wiki' in quickmarks.marks
def test_session_completion(qtmodeltester, session_manager_stub):

View File

@ -1,146 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2015-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# 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 <http://www.gnu.org/licenses/>.
"""Tests for CompletionFilterModel."""
import pytest
from qutebrowser.completion.models import listcategory, sortfilter
# TODO: merge listcategory and sortfilter
def _create_model(data):
"""Create a completion model populated with the given data.
data: A list of lists, where each sub-list represents a category, each
tuple in the sub-list represents an item, and each value in the
tuple represents the item data for that column
"""
model = completionmodel.CompletionModel()
for catdata in data:
cat = model.add_list(itemdata)
return model
def _extract_model_data(model):
"""Express a model's data as a list for easier comparison.
Return: A list of lists, where each sub-list represents a category, each
tuple in the sub-list represents an item, and each value in the
tuple represents the item data for that column
"""
data = []
for i in range(0, model.rowCount()):
cat_idx = model.index(i, 0)
row = []
for j in range(0, model.rowCount(cat_idx)):
row.append((model.data(cat_idx.child(j, 0)),
model.data(cat_idx.child(j, 1)),
model.data(cat_idx.child(j, 2))))
data.append(row)
return data
@pytest.mark.parametrize('pattern, data, expected', [
('foo', 'barfoobar', True),
('foo bar', 'barfoobar', True),
('foo bar', 'barfoobar', True),
('foo bar', 'barfoobazbar', True),
('foo bar', 'barfoobazbar', True),
('foo', 'barFOObar', True),
('Foo', 'barfOObar', True),
('ab', 'aonebtwo', False),
('33', 'l33t', True),
('x', 'blah', False),
('4', 'blah', False),
])
def test_filter_accepts_row(pattern, data, expected):
source_model = completionmodel.CompletionModel()
cat = source_model.new_category('test')
source_model.new_item(cat, data)
filter_model = sortfilter.CompletionFilterModel(source_model)
filter_model.set_pattern(pattern)
assert filter_model.rowCount() == 1 # "test" category
idx = filter_model.index(0, 0)
assert idx.isValid()
row_count = filter_model.rowCount(idx)
assert row_count == (1 if expected else 0)
def test_set_source_model():
"""Ensure setSourceModel sets source_model and clears the pattern."""
model1 = base.CompletionModel()
model2 = base.CompletionModel()
filter_model = sortfilter.CompletionFilterModel(model1)
filter_model.set_pattern('foo')
# sourceModel() is cached as srcmodel, so make sure both match
assert filter_model.srcmodel is model1
assert filter_model.sourceModel() is model1
assert filter_model.pattern == 'foo'
filter_model.setSourceModel(model2)
assert filter_model.srcmodel is model2
assert filter_model.sourceModel() is model2
assert not filter_model.pattern
@pytest.mark.parametrize('pattern, filter_cols, before, after', [
('foo', [0],
[[('foo', '', ''), ('bar', '', '')]],
[[('foo', '', '')]]),
('foo', [0],
[[('foob', '', ''), ('fooc', '', ''), ('fooa', '', '')]],
[[('fooa', '', ''), ('foob', '', ''), ('fooc', '', '')]]),
('foo', [0],
[[('foo', '', '')], [('bar', '', '')]],
[[('foo', '', '')], []]),
# prefer foobar as it starts with the pattern
('foo', [0],
[[('barfoo', '', ''), ('foobar', '', '')]],
[[('foobar', '', ''), ('barfoo', '', '')]]),
# however, don't rearrange categories
('foo', [0],
[[('barfoo', '', '')], [('foobar', '', '')]],
[[('barfoo', '', '')], [('foobar', '', '')]]),
('foo', [1],
[[('foo', 'bar', ''), ('bar', 'foo', '')]],
[[('bar', 'foo', '')]]),
('foo', [0, 1],
[[('foo', 'bar', ''), ('bar', 'foo', ''), ('bar', 'bar', '')]],
[[('foo', 'bar', ''), ('bar', 'foo', '')]]),
('foo', [0, 1, 2],
[[('foo', '', ''), ('bar', '')]],
[[('foo', '', '')]]),
])
def test_set_pattern(pattern, filter_cols, before, after):
"""Validate the filtering and sorting results of set_pattern."""
model = _create_model(before)
model.columns_to_filter = filter_cols
filter_model = sortfilter.CompletionFilterModel(model)
filter_model.set_pattern(pattern)
actual = _extract_model_data(filter_model)
assert actual == after

View File

@ -0,0 +1,156 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016-2017 Ryan Roden-Corrent (rcorre) <ryan@rcorre.net>
#
# 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 <http://www.gnu.org/licenses/>.
"""Test SQL-based completions."""
import pytest
from qutebrowser.misc import sql
from qutebrowser.completion.models import sqlcategory
pytestmark = pytest.mark.usefixtures('init_sql')
def _validate(cat, expected):
"""Check that a category contains the expected items in the given order.
Args:
cat: The category to inspect.
expected: A list of tuples containing the expected items.
"""
assert cat.rowCount() == len(expected)
for row, items in enumerate(expected):
for col, item in enumerate(items):
assert cat.data(cat.index(row, col)) == item
@pytest.mark.parametrize('sort_by, sort_order, data, expected', [
(None, 'asc',
[('B', 'C', 'D'), ('A', 'F', 'C'), ('C', 'A', 'G')],
[('B', 'C', 'D'), ('A', 'F', 'C'), ('C', 'A', 'G')]),
('a', 'asc',
[('B', 'C', 'D'), ('A', 'F', 'C'), ('C', 'A', 'G')],
[('A', 'F', 'C'), ('B', 'C', 'D'), ('C', 'A', 'G')]),
('a', 'desc',
[('B', 'C', 'D'), ('A', 'F', 'C'), ('C', 'A', 'G')],
[('C', 'A', 'G'), ('B', 'C', 'D'), ('A', 'F', 'C')]),
('b', 'asc',
[('B', 'C', 'D'), ('A', 'F', 'C'), ('C', 'A', 'G')],
[('C', 'A', 'G'), ('B', 'C', 'D'), ('A', 'F', 'C')]),
('b', 'desc',
[('B', 'C', 'D'), ('A', 'F', 'C'), ('C', 'A', 'G')],
[('A', 'F', 'C'), ('B', 'C', 'D'), ('C', 'A', 'G')]),
('c', 'asc',
[('B', 'C', 2), ('A', 'F', 0), ('C', 'A', 1)],
[('A', 'F', 0), ('C', 'A', 1), ('B', 'C', 2)]),
('c', 'desc',
[('B', 'C', 2), ('A', 'F', 0), ('C', 'A', 1)],
[('B', 'C', 2), ('C', 'A', 1), ('A', 'F', 0)]),
])
def test_sorting(sort_by, sort_order, data, expected):
table = sql.SqlTable('Foo', ['a', 'b', 'c'], primary_key='a')
for row in data:
table.insert(row)
cat = sqlcategory.SqlCategory('Foo', sort_by=sort_by,
sort_order=sort_order)
_validate(cat, expected)
@pytest.mark.parametrize('pattern, filter_cols, before, after', [
('foo', [0],
[('foo', '', ''), ('bar', '', ''), ('aafobbb', '', '')],
[('foo',)]),
('foo', [0],
[('baz', 'bar', 'foo'), ('foo', '', ''), ('bar', 'foo', '')],
[('foo', '', '')]),
('foo', [0],
[('fooa', '', ''), ('foob', '', ''), ('fooc', '', '')],
[('fooa', '', ''), ('foob', '', ''), ('fooc', '', '')]),
('foo', [1],
[('foo', 'bar', ''), ('bar', 'foo', '')],
[('bar', 'foo', '')]),
('foo', [0, 1],
[('foo', 'bar', ''), ('bar', 'foo', ''), ('biz', 'baz', 'foo')],
[('foo', 'bar', ''), ('bar', 'foo', '')]),
('foo', [0, 1, 2],
[('foo', '', ''), ('bar', '', ''), ('baz', 'bar', 'foo')],
[('foo', '', ''), ('baz', 'bar', 'foo')]),
('foo bar', [0],
[('foo', '', ''), ('bar foo', '', ''), ('xfooyybarz', '', '')],
[('xfooyybarz', '', '')]),
('foo%bar', [0],
[('foo%bar', '', ''), ('foo bar', '', ''), ('foobar', '', '')],
[('foo%bar', '', '')]),
('_', [0],
[('a_b', '', ''), ('__a', '', ''), ('abc', '', '')],
[('a_b', '', ''), ('__a', '', '')]),
('%', [0, 1],
[('\\foo', '\\bar', '')],
[]),
("can't", [0],
[("can't touch this", '', ''), ('a', '', '')],
[("can't touch this", '', '')]),
])
def test_set_pattern(pattern, filter_cols, before, after):
"""Validate the filtering and sorting results of set_pattern."""
table = sql.SqlTable('Foo', ['a', 'b', 'c'], primary_key='a')
for row in before:
table.insert(row)
cat = sqlcategory.SqlCategory('Foo')
cat.set_pattern(pattern, filter_cols)
_validate(cat, after)
def test_select():
table = sql.SqlTable('Foo', ['a', 'b', 'c'], primary_key='a')
table.insert(['foo', 'bar', 'baz'])
cat = sqlcategory.SqlCategory('Foo', select='b, c, a')
_validate(cat, [('bar', 'baz', 'foo')])
def test_where():
table = sql.SqlTable('Foo', ['a', 'b', 'c'], primary_key='a')
table.insert(['foo', 'bar', False])
table.insert(['baz', 'biz', True])
cat = sqlcategory.SqlCategory('Foo', where='not c')
_validate(cat, [('foo', 'bar', False)])
def test_entry():
table = sql.SqlTable('Foo', ['a', 'b', 'c'], primary_key='a')
assert hasattr(table.Entry, 'a')
assert hasattr(table.Entry, 'b')
assert hasattr(table.Entry, 'c')

View File

@ -1,230 +0,0 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016-2017 Ryan Roden-Corrent (rcorre) <ryan@rcorre.net>
#
# 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 <http://www.gnu.org/licenses/>.
"""Tests for the base sql completion model."""
import pytest
from qutebrowser.misc import sql
from qutebrowser.completion.models import sqlmodel
pytestmark = pytest.mark.usefixtures('init_sql')
def _check_model(model, expected):
"""Check that a model contains the expected items in the given order.
Args:
expected: A list of form
[
(cat, [(name, desc, misc), (name, desc, misc), ...]),
(cat, [(name, desc, misc), (name, desc, misc), ...]),
...
]
"""
assert model.rowCount() == len(expected)
for i, (expected_title, expected_items) in enumerate(expected):
catidx = model.index(i, 0)
assert model.data(catidx) == expected_title
assert model.rowCount(catidx) == len(expected_items)
for j, (name, desc, misc) in enumerate(expected_items):
assert model.data(model.index(j, 0, catidx)) == name
assert model.data(model.index(j, 1, catidx)) == desc
assert model.data(model.index(j, 2, catidx)) == misc
@pytest.mark.parametrize('rowcounts, expected', [
([0], 0),
([1], 1),
([2], 2),
([0, 0], 0),
([0, 0, 0], 0),
([1, 1], 2),
([3, 2, 1], 6),
([0, 2, 0], 2),
])
def test_count(rowcounts, expected):
model = sqlmodel.SqlCompletionModel()
for i, rowcount in enumerate(rowcounts):
name = 'Foo' + str(i)
table = sql.SqlTable(name, ['a'], primary_key='a')
for rownum in range(rowcount):
table.insert([rownum])
model.new_category(name)
assert model.count() == expected
@pytest.mark.parametrize('sort_by, sort_order, data, expected', [
(None, 'asc',
[('B', 'C', 'D'), ('A', 'F', 'C'), ('C', 'A', 'G')],
[('B', 'C', 'D'), ('A', 'F', 'C'), ('C', 'A', 'G')]),
('a', 'asc',
[('B', 'C', 'D'), ('A', 'F', 'C'), ('C', 'A', 'G')],
[('A', 'F', 'C'), ('B', 'C', 'D'), ('C', 'A', 'G')]),
('a', 'desc',
[('B', 'C', 'D'), ('A', 'F', 'C'), ('C', 'A', 'G')],
[('C', 'A', 'G'), ('B', 'C', 'D'), ('A', 'F', 'C')]),
('b', 'asc',
[('B', 'C', 'D'), ('A', 'F', 'C'), ('C', 'A', 'G')],
[('C', 'A', 'G'), ('B', 'C', 'D'), ('A', 'F', 'C')]),
('b', 'desc',
[('B', 'C', 'D'), ('A', 'F', 'C'), ('C', 'A', 'G')],
[('A', 'F', 'C'), ('B', 'C', 'D'), ('C', 'A', 'G')]),
('c', 'asc',
[('B', 'C', 2), ('A', 'F', 0), ('C', 'A', 1)],
[('A', 'F', 0), ('C', 'A', 1), ('B', 'C', 2)]),
('c', 'desc',
[('B', 'C', 2), ('A', 'F', 0), ('C', 'A', 1)],
[('B', 'C', 2), ('C', 'A', 1), ('A', 'F', 0)]),
])
def test_sorting(sort_by, sort_order, data, expected):
table = sql.SqlTable('Foo', ['a', 'b', 'c'], primary_key='a')
for row in data:
table.insert(row)
model = sqlmodel.SqlCompletionModel()
model.new_category('Foo', sort_by=sort_by, sort_order=sort_order)
_check_model(model, [('Foo', expected)])
@pytest.mark.parametrize('pattern, filter_cols, before, after', [
('foo', [0],
[('A', [('foo', '', ''), ('bar', '', ''), ('aafobbb', '', '')])],
[('A', [('foo', '', '')])]),
('foo', [0],
[('A', [('baz', 'bar', 'foo'), ('foo', '', ''), ('bar', 'foo', '')])],
[('A', [('foo', '', '')])]),
('foo', [0],
[('A', [('foo', '', ''), ('bar', '', '')]),
('B', [('foo', '', ''), ('bar', '', '')])],
[('A', [('foo', '', '')]), ('B', [('foo', '', '')])]),
('foo', [0],
[('A', [('fooa', '', ''), ('foob', '', ''), ('fooc', '', '')])],
[('A', [('fooa', '', ''), ('foob', '', ''), ('fooc', '', '')])]),
('foo', [0],
[('A', [('foo', '', '')]), ('B', [('bar', '', '')])],
[('A', [('foo', '', '')]), ('B', [])]),
('foo', [1],
[('A', [('foo', 'bar', ''), ('bar', 'foo', '')])],
[('A', [('bar', 'foo', '')])]),
('foo', [0, 1],
[('A', [('foo', 'bar', ''), ('bar', 'foo', '')])],
[('A', [('foo', 'bar', ''), ('bar', 'foo', '')])]),
('foo', [0, 1, 2],
[('A', [('foo', '', ''), ('bar', '', '')])],
[('A', [('foo', '', '')])]),
('foo bar', [0],
[('A', [('foo', '', ''), ('bar foo', '', ''), ('xfooyybarz', '', '')])],
[('A', [('xfooyybarz', '', '')])]),
('foo%bar', [0],
[('A', [('foo%bar', '', ''), ('foo bar', '', ''), ('foobar', '', '')])],
[('A', [('foo%bar', '', '')])]),
('_', [0],
[('A', [('a_b', '', ''), ('__a', '', ''), ('abc', '', '')])],
[('A', [('a_b', '', ''), ('__a', '', '')])]),
('%', [0, 1],
[('A', [('\\foo', '\\bar', '')])],
[('A', [])]),
("can't", [0],
[('A', [("can't touch this", '', ''), ('a', '', '')])],
[('A', [("can't touch this", '', '')])]),
])
def test_set_pattern(pattern, filter_cols, before, after):
"""Validate the filtering and sorting results of set_pattern."""
model = sqlmodel.SqlCompletionModel(columns_to_filter=filter_cols)
for name, rows in before:
table = sql.SqlTable(name, ['a', 'b', 'c'], primary_key='a')
for row in rows:
table.insert(row)
model.new_category(name)
model.set_pattern(pattern)
_check_model(model, after)
@pytest.mark.parametrize('data, first, last', [
([('A', ['Aa'])], 'Aa', 'Aa'),
([('A', ['Aa', 'Ba'])], 'Aa', 'Ba'),
([('A', ['Aa', 'Ab', 'Ac']), ('B', ['Ba', 'Bb']),
('C', ['Ca'])], 'Aa', 'Ca'),
([('A', []), ('B', ['Ba'])], 'Ba', 'Ba'),
([('A', []), ('B', []), ('C', ['Ca'])], 'Ca', 'Ca'),
([('A', []), ('B', []), ('C', ['Ca', 'Cb'])], 'Ca', 'Cb'),
([('A', ['Aa']), ('B', [])], 'Aa', 'Aa'),
([('A', ['Aa']), ('B', []), ('C', [])], 'Aa', 'Aa'),
([('A', ['Aa']), ('B', []), ('C', ['Ca'])], 'Aa', 'Ca'),
([('A', []), ('B', [])], None, None),
])
def test_first_last_item(data, first, last):
"""Test that first() and last() return indexes to the first and last items.
Args:
data: Input to _make_model
first: text of the first item
last: text of the last item
"""
model = sqlmodel.SqlCompletionModel()
for name, rows in data:
table = sql.SqlTable(name, ['a'], primary_key='a')
for row in rows:
table.insert([row])
model.new_category(name)
assert model.data(model.first_item()) == first
assert model.data(model.last_item()) == last
def test_select():
table = sql.SqlTable('test_select', ['a', 'b', 'c'], primary_key='a')
table.insert(['foo', 'bar', 'baz'])
model = sqlmodel.SqlCompletionModel()
model.new_category('test_select', select='b, c, a')
_check_model(model, [('test_select', [('bar', 'baz', 'foo')])])
def test_where():
table = sql.SqlTable('test_where', ['a', 'b', 'c'], primary_key='a')
table.insert(['foo', 'bar', False])
table.insert(['baz', 'biz', True])
model = sqlmodel.SqlCompletionModel()
model.new_category('test_where', where='not c')
_check_model(model, [('test_where', [('foo', 'bar', False)])])
def test_entry():
table = sql.SqlTable('test_entry', ['a', 'b', 'c'], primary_key='a')
assert hasattr(table.Entry, 'a')
assert hasattr(table.Entry, 'b')
assert hasattr(table.Entry, 'c')