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-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-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-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''
|
.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
|
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
|
== ui
|
||||||
General options related to the user interface.
|
General options related to the user interface.
|
||||||
|
|
||||||
|
@ -471,8 +471,13 @@ class CommandDispatcher:
|
|||||||
background: Open the link in a new background tab.
|
background: Open the link in a new background tab.
|
||||||
window: Open the link in a new window.
|
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:
|
try:
|
||||||
new_url = urlutils.incdec_number(url, incdec)
|
new_url = urlutils.incdec_number(url, incdec, segments=segments)
|
||||||
except urlutils.IncDecError as error:
|
except urlutils.IncDecError as error:
|
||||||
raise cmdexc.CommandError(error.msg)
|
raise cmdexc.CommandError(error.msg)
|
||||||
self._open(new_url, tab, background, window)
|
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 "
|
"The name of the session to save by default, or empty for the "
|
||||||
"last loaded session."),
|
"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
|
readonly=readonly
|
||||||
)),
|
)),
|
||||||
|
|
||||||
|
@ -286,6 +286,58 @@ class List(BaseType):
|
|||||||
raise configexc.ValidationError(value, "items may not be empty!")
|
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):
|
class Bool(BaseType):
|
||||||
|
|
||||||
"""Base class for a boolean setting."""
|
"""Base class for a boolean setting."""
|
||||||
@ -1364,7 +1416,7 @@ class AcceptCookies(BaseType):
|
|||||||
('never', "Don't accept cookies at all."))
|
('never', "Don't accept cookies at all."))
|
||||||
|
|
||||||
|
|
||||||
class ConfirmQuit(List):
|
class ConfirmQuit(FlagList):
|
||||||
|
|
||||||
"""Whether to display a confirmation when the window is closed."""
|
"""Whether to display a confirmation when the window is closed."""
|
||||||
|
|
||||||
@ -1379,18 +1431,11 @@ class ConfirmQuit(List):
|
|||||||
combinable_values = ('multiple-tabs', 'downloads')
|
combinable_values = ('multiple-tabs', 'downloads')
|
||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
self._basic_validation(value)
|
super().validate(value)
|
||||||
if not value:
|
if not value:
|
||||||
return
|
return
|
||||||
values = []
|
values = [x for x in self.transform(value) if x]
|
||||||
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!")
|
|
||||||
# Never can't be set with other options
|
# Never can't be set with other options
|
||||||
if 'never' in values and len(values) > 1:
|
if 'never' in values and len(values) > 1:
|
||||||
raise configexc.ValidationError(
|
raise configexc.ValidationError(
|
||||||
@ -1399,30 +1444,6 @@ class ConfirmQuit(List):
|
|||||||
elif 'always' in values and len(values) > 1:
|
elif 'always' in values and len(values) > 1:
|
||||||
raise configexc.ValidationError(
|
raise configexc.ValidationError(
|
||||||
value, "List cannot contain always!")
|
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):
|
class ForwardUnboundKeys(BaseType):
|
||||||
@ -1602,3 +1623,10 @@ class TabBarShow(BaseType):
|
|||||||
"is open."),
|
"is open."),
|
||||||
('switching', "Show the tab bar when switching "
|
('switching', "Show the tab bar when switching "
|
||||||
"tabs."))
|
"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())
|
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.
|
"""Find a number in the url and increment or decrement it.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
url: The current url
|
url: The current url
|
||||||
incdec: Either 'increment' or 'decrement'
|
incdec: Either 'increment' or 'decrement'
|
||||||
|
segments: A set of URL segments to search. Valid segments are:
|
||||||
|
'host', 'path', 'query', 'anchor'.
|
||||||
|
Default: {'path', 'query'}
|
||||||
|
|
||||||
Return:
|
Return:
|
||||||
The new url with the number incremented/decremented.
|
The new url with the number incremented/decremented.
|
||||||
@ -471,11 +474,33 @@ def incdec_number(url, incdec):
|
|||||||
if not url.isValid():
|
if not url.isValid():
|
||||||
raise InvalidUrlError(url)
|
raise InvalidUrlError(url)
|
||||||
|
|
||||||
path = url.path()
|
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
|
||||||
|
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
|
# Get the last number in a string
|
||||||
match = re.match(r'(.*\D|^)(\d+)(.*)', path)
|
match = re.match(r'(.*\D|^)(\d+)(.*)', getter())
|
||||||
if not match:
|
if not match:
|
||||||
raise IncDecError("No number found in URL!", url)
|
continue
|
||||||
|
|
||||||
pre, number, post = match.groups()
|
pre, number, post = match.groups()
|
||||||
# This should always succeed because we match \d+
|
# This should always succeed because we match \d+
|
||||||
val = int(number)
|
val = int(number)
|
||||||
@ -487,8 +512,8 @@ def incdec_number(url, incdec):
|
|||||||
val += 1
|
val += 1
|
||||||
else:
|
else:
|
||||||
raise ValueError("Invalid value {} for indec!".format(incdec))
|
raise ValueError("Invalid value {} for indec!".format(incdec))
|
||||||
new_path = ''.join([pre, str(val), post])
|
new_value = ''.join([pre, str(val), post])
|
||||||
# Make a copy of the QUrl so we don't modify the original
|
setter(new_value)
|
||||||
new_url = QUrl(url)
|
return url
|
||||||
new_url.setPath(new_path)
|
|
||||||
return new_url
|
raise IncDecError("No number found in URL!", url)
|
||||||
|
@ -368,6 +368,81 @@ class TestList:
|
|||||||
assert klass().transform(val) == expected
|
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:
|
class TestBool:
|
||||||
|
|
||||||
"""Test Bool."""
|
"""Test Bool."""
|
||||||
|
@ -531,23 +531,46 @@ class TestIncDecNumber:
|
|||||||
|
|
||||||
"""Tests for urlutils.incdec_number()."""
|
"""Tests for urlutils.incdec_number()."""
|
||||||
|
|
||||||
@pytest.mark.parametrize('url, incdec, output', [
|
@pytest.mark.parametrize('incdec', ['increment', 'decrement'])
|
||||||
("http://example.com/index1.html", "increment", "http://example.com/index2.html"),
|
@pytest.mark.parametrize('value', [
|
||||||
("http://foo.bar/folder_1/image_2", "increment", "http://foo.bar/folder_1/image_3"),
|
'{}foo', 'foo{}', 'foo{}bar', '42foo{}'
|
||||||
("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"),
|
|
||||||
])
|
])
|
||||||
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."""
|
"""Test incdec_number with valid URLs."""
|
||||||
new_url = urlutils.incdec_number(QUrl(url), incdec)
|
# The integer used should not affect test output, as long as it's
|
||||||
assert new_url == QUrl(output)
|
# 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', [
|
@pytest.mark.parametrize('url', [
|
||||||
"http://example.com/long/path/but/no/number",
|
"http://example.com/long/path/but/no/number",
|
||||||
@ -582,6 +605,12 @@ class TestIncDecNumber:
|
|||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
urlutils.incdec_number(valid_url, "foobar")
|
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", [
|
@pytest.mark.parametrize("url, msg, expected_str", [
|
||||||
("http://example.com", "Invalid", "Invalid: http://example.com"),
|
("http://example.com", "Invalid", "Invalid: http://example.com"),
|
||||||
])
|
])
|
||||||
|
Loading…
Reference in New Issue
Block a user