Merge branch 'incdec-setting' of https://github.com/Kingdread/qutebrowser into Kingdread-incdec-setting
This commit is contained in:
commit
7ff881c3e3
@ -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.
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
)),
|
||||
|
||||
|
@ -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')
|
||||
|
@ -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)
|
||||
|
@ -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."""
|
||||
|
@ -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"),
|
||||
])
|
||||
|
Loading…
Reference in New Issue
Block a user