Repository: qutebrowser/qutebrowser

Issue:
What parts of the URL should Ctrl-A increment?
Minor thing spotted by @Carpetsmoker - `http://mdweb.arp242.net/test-dir` changes to arp243.net when doing Ctrl-A, which makes little sense.

What parts of the URL should Ctrl-A increment?
Minor thing spotted by @Carpetsmoker - `http://mdweb.arp242.net/test-dir` changes to arp243.net when doing Ctrl-A, which makes little sense.



Discussion:

Resolving patch:
diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py
--- 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,29 +471,10 @@ def _navigate_incdec(self, url, incdec, tab, background, window):
             background: Open the link in a new background tab.
             window: Open the link in a new window.
         """
-        encoded = bytes(url.toEncoded()).decode('ascii')
-        # Get the last number in a string
-        match = re.match(r'(.*\D|^)(\d+)(.*)', encoded)
-        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))
-        urlstr = ''.join([pre, str(val), post]).encode('ascii')
-        new_url = QUrl.fromEncoded(urlstr)
+            new_url = urlutils.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
--- a/qutebrowser/utils/urlutils.py
+++ b/qutebrowser/utils/urlutils.py
@@ -443,3 +443,59 @@ def __str__(self):
             return self.msg
         else:
             return '{}: {}'.format(self.msg, self.url.errorString())
+
+
+class IncDecError(Exception):
+
+    """Exception raised by 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 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.
+    """
+    if not url.isValid():
+        raise ValueError(get_errstring(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])
+    # 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


Test patch:
diff --git a/tests/utils/test_urlutils.py b/tests/utils/test_urlutils.py
--- a/tests/utils/test_urlutils.py
+++ b/tests/utils/test_urlutils.py
@@ -524,3 +524,70 @@ 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))
+
+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"),
+    ])
+    def test_incdec_number(self, url, incdec, output):
+        """Test incdec_number with valid URLs."""
+        new_url = urlutils.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://example.com/number/in/anchor#5",
+        "http://www2.ex4mple.com:42/all/of/the/%C3%A4bove#5",
+    ])
+    def test_no_number(self, url):
+        """Test incdec_number with URLs that don't contain a number."""
+        with pytest.raises(urlutils.IncDecError):
+            urlutils.incdec_number(QUrl(url), "increment")
+
+    def test_number_below_0(self):
+        """Test incdec_number with a number that would be below zero
+        after decrementing."""
+        with pytest.raises(urlutils.IncDecError):
+            urlutils.incdec_number(QUrl('http://example.com/page_0.html'),
+                    'decrement')
+
+    def test_invalid_url(self):
+        """Test if incdec_number rejects an invalid URL."""
+        with pytest.raises(ValueError):
+            urlutils.incdec_number(QUrl(""), "increment")
+
+    def test_wrong_mode(self):
+        """Test if incdec_number rejects a wrong parameter for the incdec
+        argument."""
+        valid_url = QUrl("http://example.com/0")
+        with pytest.raises(ValueError):
+            urlutils.incdec_number(valid_url, "foobar")
+
+    @pytest.mark.parametrize("url, msg, expected_str", [
+        ("http://example.com", "Invalid", "Invalid: http://example.com"),
+    ])
+    def test_incdec_error(self, url, msg, expected_str):
+        """Test IncDecError."""
+        url = QUrl(url)
+        with pytest.raises(urlutils.IncDecError) as excinfo:
+            raise urlutils.IncDecError(msg, url)
+
+        assert excinfo.value.url == url
+        assert str(excinfo.value) == expected_str