diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py index 057b54a24..2c9404f93 100644 --- a/qutebrowser/browser/commands.py +++ b/qutebrowser/browser/commands.py @@ -19,7 +19,6 @@ """Command dispatcher for TabbedBrowser.""" -import re import os import shlex import posixpath @@ -472,31 +471,10 @@ class CommandDispatcher: background: Open the link in a new background tab. window: Open the link in a new window. """ - path = url.path() - # Get the last number in a string - match = re.match(r'(.*\D|^)(\d+)(.*)', path) - if not match: - raise cmdexc.CommandError("No number found in URL!") - pre, number, post = match.groups() - if not number: - raise cmdexc.CommandError("No number found in URL!") try: - val = int(number) - except ValueError: - raise cmdexc.CommandError("Could not parse number '{}'.".format( - number)) - if incdec == 'decrement': - if val <= 0: - raise cmdexc.CommandError("Can't decrement {}!".format(val)) - val -= 1 - elif incdec == 'increment': - val += 1 - else: - raise ValueError("Invalid value {} for indec!".format(incdec)) - new_path = ''.join([pre, str(val), post]) - # Make a copy of the QUrl so we don't modify the original - new_url = QUrl(url) - new_url.setPath(new_path) + new_url = urlutils.url_incdec_number(url, incdec) + except urlutils.IncDecError as error: + raise cmdexc.CommandError(error.msg) self._open(new_url, tab, background, window) def _navigate_up(self, url, tab, background, window): diff --git a/qutebrowser/utils/urlutils.py b/qutebrowser/utils/urlutils.py index 50f60d523..f6bfe5701 100644 --- a/qutebrowser/utils/urlutils.py +++ b/qutebrowser/utils/urlutils.py @@ -443,3 +443,60 @@ class FuzzyUrlError(Exception): return self.msg else: return '{}: {}'.format(self.msg, self.url.errorString()) + + +class IncDecError(Exception): + + """Exception raised by url_incdec_number on problems. + + Attributes: + msg: The error message. + url: The QUrl which caused the error. + """ + + def __init__(self, msg, url): + super().__init__(msg) + self.url = url + self.msg = msg + + def __str__(self): + return '{}: {}'.format(self.msg, self.url.toString()) + + +def url_incdec_number(url, incdec): + """Find a number in the url and increment or decrement it. + + Args: + url: The current url + incdec: Either 'increment' or 'decrement' + + Return: + The new url with the number incremented/decremented. + + Raises IncDecError if the url contains no number. + """ + 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() + if not number: + raise IncDecError("No number found in URL!", url) + try: + val = int(number) + except ValueError: + raise IncDecError("Could not parse number '{}'.".format(number), url) + 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]) + # 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 diff --git a/tests/utils/test_urlutils.py b/tests/utils/test_urlutils.py index fa1cfa665..520f3d891 100644 --- a/tests/utils/test_urlutils.py +++ b/tests/utils/test_urlutils.py @@ -524,3 +524,39 @@ def test_same_domain_invalid_url(url1, url2): """Test same_domain with invalid URLs.""" with pytest.raises(ValueError): urlutils.same_domain(QUrl(url1), QUrl(url2)) + +@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/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"), +]) +def test_url_incdec_number(url, incdec, output): + """Test url_incdec_number with valid URLs.""" + new_url = urlutils.url_incdec_number(QUrl(url), incdec) + assert new_url == QUrl(output) + +@pytest.mark.parametrize('url', [ + "http://example.com/long/path/but/no/number", + "http://ex4mple.com/number/in/hostname", + "http://example.com:42/number/in/port", + "http://www2.example.com/number/in/subdomain", + "http://example.com/%C3%B6/urlencoded/data", + "http://www2.ex4mple.com:42/all/of/the/%C3%A4bove", +]) +def test_url_incdec_number_invalid(url): + """Test url_incdec_number with URLs that don't contain a number.""" + with pytest.raises(urlutils.IncDecError): + urlutils.url_incdec_number(QUrl(url), "increment") + +def test_url_incdec_number_below_0(): + """Test url_incdec_number with a number that would be below zero + after decrementing.""" + with pytest.raises(urlutils.IncDecError): + urlutils.url_incdec_number(QUrl('http://example.com/page_0.html'), + 'decrement')