Merge branch 'incdec-setting' of https://github.com/Kingdread/qutebrowser into Kingdread-incdec-setting

This commit is contained in:
Florian Bruhin 2015-10-04 17:04:40 +02:00
commit 7ff881c3e3
7 changed files with 253 additions and 72 deletions

View File

@ -23,6 +23,7 @@
|<<general-log-javascript-console,log-javascript-console>>|Whether to log javascript console messages.
|<<general-save-session,save-session>>|Whether to always save the open pages.
|<<general-session-default-name,session-default-name>>|The name of the session to save by default, or empty for the last loaded session.
|<<general-url-incdec-segments,url-incdec-segments>>|The URL segments where `:navigate increment/decrement` will search for a number.
|==============
.Quick reference for section ``ui''
@ -457,6 +458,19 @@ The name of the session to save by default, or empty for the last loaded session
Default: empty
[[general-url-incdec-segments]]
=== url-incdec-segments
The URL segments where `:navigate increment/decrement` will search for a number.
Valid values:
* +host+
* +path+
* +query+
* +anchor+
Default: +pass:[path,query]+
== ui
General options related to the user interface.

View File

@ -471,8 +471,13 @@ class CommandDispatcher:
background: Open the link in a new background tab.
window: Open the link in a new window.
"""
segments = config.get('general', 'url-incdec-segments')
if segments is None:
segments = set()
else:
segments = set(segments)
try:
new_url = urlutils.incdec_number(url, incdec)
new_url = urlutils.incdec_number(url, incdec, segments=segments)
except urlutils.IncDecError as error:
raise cmdexc.CommandError(error.msg)
self._open(new_url, tab, background, window)

View File

@ -225,6 +225,11 @@ def data(readonly=False):
"The name of the session to save by default, or empty for the "
"last loaded session."),
('url-incdec-segments',
SettingValue(typ.URLSegmentList(none_ok=True), 'path,query'),
"The URL segments where `:navigate increment/decrement` will "
"search for a number."),
readonly=readonly
)),

View File

@ -286,6 +286,58 @@ class List(BaseType):
raise configexc.ValidationError(value, "items may not be empty!")
class FlagList(List):
"""Base class for a list setting that contains one or more flags.
Lists with duplicate flags are invalid and each item is checked against
self.valid_values (if not empty).
"""
combinable_values = None
def validate(self, value):
self._basic_validation(value)
if not value:
return
vals = self.transform(value)
if None in vals and not self.none_ok:
raise configexc.ValidationError(
value, "May not contain empty values!")
# Check for duplicate values
if len(set(vals)) != len(vals):
raise configexc.ValidationError(
value, "List contains duplicate values!")
# Check if each value is valid, ignores None values
set_vals = set(val for val in vals if val)
if (self.valid_values is not None and
not set_vals.issubset(set(self.valid_values))):
raise configexc.ValidationError(
value, "List contains invalid values!")
def complete(self):
if self.valid_values is None:
return None
out = []
# Single value completions
for value in self.valid_values:
desc = self.valid_values.descriptions.get(value, "")
out.append((value, desc))
combinables = self.combinable_values
if combinables is None:
combinables = list(self.valid_values)
# Generate combinations of each possible value combination
for size in range(2, len(combinables) + 1):
for combination in itertools.combinations(combinables, size):
out.append((','.join(combination), ''))
return out
class Bool(BaseType):
"""Base class for a boolean setting."""
@ -1364,7 +1416,7 @@ class AcceptCookies(BaseType):
('never', "Don't accept cookies at all."))
class ConfirmQuit(List):
class ConfirmQuit(FlagList):
"""Whether to display a confirmation when the window is closed."""
@ -1379,18 +1431,11 @@ class ConfirmQuit(List):
combinable_values = ('multiple-tabs', 'downloads')
def validate(self, value):
self._basic_validation(value)
super().validate(value)
if not value:
return
values = []
for v in self.transform(value):
if v:
values.append(v)
elif self.none_ok:
pass
else:
raise configexc.ValidationError(value, "May not contain empty "
"values!")
values = [x for x in self.transform(value) if x]
# Never can't be set with other options
if 'never' in values and len(values) > 1:
raise configexc.ValidationError(
@ -1399,30 +1444,6 @@ class ConfirmQuit(List):
elif 'always' in values and len(values) > 1:
raise configexc.ValidationError(
value, "List cannot contain always!")
# Values have to be valid
elif not set(values).issubset(set(self.valid_values.values)):
raise configexc.ValidationError(
value, "List contains invalid values!")
# List can't have duplicates
elif len(set(values)) != len(values):
raise configexc.ValidationError(
value, "List contains duplicate values!")
def complete(self):
combinations = []
# Generate combinations of the options that can be combined
for size in range(2, len(self.combinable_values) + 1):
combinations += list(
itertools.combinations(self.combinable_values, size))
out = []
# Add valid single values
for val in self.valid_values:
out.append((val, self.valid_values.descriptions[val]))
# Add combinations to list of options
for val in combinations:
desc = ''
out.append((','.join(val), desc))
return out
class ForwardUnboundKeys(BaseType):
@ -1602,3 +1623,10 @@ class TabBarShow(BaseType):
"is open."),
('switching', "Show the tab bar when switching "
"tabs."))
class URLSegmentList(FlagList):
"""A list of URL segments."""
valid_values = ValidValues('host', 'path', 'query', 'anchor')

View File

@ -456,12 +456,15 @@ class IncDecError(Exception):
return '{}: {}'.format(self.msg, self.url.toString())
def incdec_number(url, incdec):
def incdec_number(url, incdec, segments=None):
"""Find a number in the url and increment or decrement it.
Args:
url: The current url
incdec: Either 'increment' or 'decrement'
segments: A set of URL segments to search. Valid segments are:
'host', 'path', 'query', 'anchor'.
Default: {'path', 'query'}
Return:
The new url with the number incremented/decremented.
@ -471,24 +474,46 @@ def incdec_number(url, incdec):
if not url.isValid():
raise InvalidUrlError(url)
path = url.path()
# Get the last number in a string
match = re.match(r'(.*\D|^)(\d+)(.*)', path)
if not match:
raise IncDecError("No number found in URL!", url)
pre, number, post = match.groups()
# This should always succeed because we match \d+
val = int(number)
if incdec == 'decrement':
if val <= 0:
raise IncDecError("Can't decrement {}!".format(val), url)
val -= 1
elif incdec == 'increment':
val += 1
else:
raise ValueError("Invalid value {} for indec!".format(incdec))
new_path = ''.join([pre, str(val), post])
if segments is None:
segments = {'path', 'query'}
valid_segments = {'host', 'path', 'query', 'anchor'}
if segments - valid_segments:
extra_elements = segments - valid_segments
raise IncDecError("Invalid segments: {}".format(
', '.join(extra_elements)), url)
# Make a copy of the QUrl so we don't modify the original
new_url = QUrl(url)
new_url.setPath(new_path)
return new_url
url = QUrl(url)
# Order as they appear in a URL
segment_modifiers = [
('host', url.host, url.setHost),
('path', url.path, url.setPath),
('query', url.query, url.setQuery),
('anchor', url.fragment, url.setFragment),
]
# We're searching the last number so we walk the url segments backwards
for segment, getter, setter in reversed(segment_modifiers):
if segment not in segments:
continue
# Get the last number in a string
match = re.match(r'(.*\D|^)(\d+)(.*)', getter())
if not match:
continue
pre, number, post = match.groups()
# This should always succeed because we match \d+
val = int(number)
if incdec == 'decrement':
if val <= 0:
raise IncDecError("Can't decrement {}!".format(val), url)
val -= 1
elif incdec == 'increment':
val += 1
else:
raise ValueError("Invalid value {} for indec!".format(incdec))
new_value = ''.join([pre, str(val), post])
setter(new_value)
return url
raise IncDecError("No number found in URL!", url)

View File

@ -368,6 +368,81 @@ class TestList:
assert klass().transform(val) == expected
class FlagListSubclass(configtypes.FlagList):
"""A subclass of FlagList which we use in tests.
Valid values are 'foo', 'bar' and 'baz'.
"""
valid_values = configtypes.ValidValues('foo', 'bar', 'baz')
combinable_values = ['foo', 'bar']
class TestFlagList:
"""Test FlagList."""
@pytest.fixture
def klass(self):
return FlagListSubclass
@pytest.fixture
def klass_valid_none(self):
"""Return a FlagList with valid_values = None"""
return configtypes.FlagList
@pytest.mark.parametrize('val', ['', 'foo', 'foo,bar', 'foo,'])
def test_validate_valid(self, klass, val):
klass(none_ok=True).validate(val)
@pytest.mark.parametrize('val', ['qux', 'foo,qux', 'foo,foo'])
def test_validate_invalid(self, klass, val):
with pytest.raises(configexc.ValidationError):
klass(none_ok=True).validate(val)
@pytest.mark.parametrize('val', ['', 'foo,', 'foo,,bar'])
def test_validate_empty_value_not_okay(self, klass, val):
with pytest.raises(configexc.ValidationError):
klass(none_ok=False).validate(val)
@pytest.mark.parametrize('val, expected', [
('', None),
('foo', ['foo']),
('foo,bar', ['foo', 'bar']),
])
def test_transform(self, klass, val, expected):
assert klass().transform(val) == expected
@pytest.mark.parametrize('val', ['spam', 'spam,eggs'])
def test_validate_values_none(self, klass_valid_none, val):
klass_valid_none().validate(val)
def test_complete(self, klass):
"""Test completing by doing some samples."""
completions = [e[0] for e in klass().complete()]
assert 'foo' in completions
assert 'bar' in completions
assert 'baz' in completions
assert 'foo,bar' in completions
for val in completions:
assert 'baz,' not in val
assert ',baz' not in val
def test_complete_all_valid_values(self, klass):
inst = klass()
inst.combinable_values = None
completions = [e[0] for e in inst.complete()]
assert 'foo' in completions
assert 'bar' in completions
assert 'baz' in completions
assert 'foo,bar' in completions
assert 'foo,baz' in completions
def test_complete_no_valid_values(self, klass_valid_none):
assert klass_valid_none().complete() == None
class TestBool:
"""Test Bool."""

View File

@ -531,23 +531,46 @@ class TestIncDecNumber:
"""Tests for urlutils.incdec_number()."""
@pytest.mark.parametrize('url, incdec, output', [
("http://example.com/index1.html", "increment", "http://example.com/index2.html"),
("http://foo.bar/folder_1/image_2", "increment", "http://foo.bar/folder_1/image_3"),
("http://bbc.c0.uk:80/story_1", "increment", "http://bbc.c0.uk:80/story_2"),
("http://mydomain.tld/1_%C3%A4", "increment", "http://mydomain.tld/2_%C3%A4"),
("http://example.com/site/5#5", "increment", "http://example.com/site/6#5"),
("http://example.com/index10.html", "decrement", "http://example.com/index9.html"),
("http://foo.bar/folder_1/image_3", "decrement", "http://foo.bar/folder_1/image_2"),
("http://bbc.c0.uk:80/story_1", "decrement", "http://bbc.c0.uk:80/story_0"),
("http://mydomain.tld/2_%C3%A4", "decrement", "http://mydomain.tld/1_%C3%A4"),
("http://example.com/site/5#5", "decrement", "http://example.com/site/4#5"),
@pytest.mark.parametrize('incdec', ['increment', 'decrement'])
@pytest.mark.parametrize('value', [
'{}foo', 'foo{}', 'foo{}bar', '42foo{}'
])
def test_incdec_number(self, url, incdec, output):
@pytest.mark.parametrize('url', [
'http://example.com:80/v1/path/{}/test',
'http://example.com:80/v1/query_test?value={}',
'http://example.com:80/v1/anchor_test#{}',
'http://host_{}_test.com:80',
'http://m4ny.c0m:80/number5/3very?where=yes#{}'
])
def test_incdec_number(self, incdec, value, url):
"""Test incdec_number with valid URLs."""
new_url = urlutils.incdec_number(QUrl(url), incdec)
assert new_url == QUrl(output)
# The integer used should not affect test output, as long as it's
# bigger than 1
# 20 was chosen by dice roll, guaranteed to be random
base_value = value.format(20)
if incdec == 'increment':
expected_value = value.format(21)
else:
expected_value = value.format(19)
base_url = QUrl(url.format(base_value))
expected_url = QUrl(url.format(expected_value))
new_url = urlutils.incdec_number(
base_url, incdec, segments={'host', 'path', 'query', 'anchor'})
assert new_url == expected_url
@pytest.mark.parametrize('url, segments, expected', [
('http://ex4mple.com/test_4?page=3#anchor2', {'host'},
'http://ex5mple.com/test_4?page=3#anchor2'),
('http://ex4mple.com/test_4?page=3#anchor2', {'host', 'path'},
'http://ex4mple.com/test_5?page=3#anchor2'),
('http://ex4mple.com/test_4?page=3#anchor5', {'host', 'path', 'query'},
'http://ex4mple.com/test_4?page=4#anchor5'),
])
def test_incdec_segment_ignored(self, url, segments, expected):
new_url = urlutils.incdec_number(QUrl(url), 'increment',
segments=segments)
assert new_url == QUrl(expected)
@pytest.mark.parametrize('url', [
"http://example.com/long/path/but/no/number",
@ -582,6 +605,12 @@ class TestIncDecNumber:
with pytest.raises(ValueError):
urlutils.incdec_number(valid_url, "foobar")
def test_wrong_segment(self):
"""Test if incdec_number rejects a wrong segment"""
with pytest.raises(urlutils.IncDecError):
urlutils.incdec_number(QUrl('http://example.com'),
'increment', segments={'foobar'})
@pytest.mark.parametrize("url, msg, expected_str", [
("http://example.com", "Invalid", "Invalid: http://example.com"),
])