Repository: qutebrowser/qutebrowser

Issue:
Behavior of cmdutils.arg_or_count
I am looking at making `tab_focus` work the same way as the new `tab_move`, with `count` overriding the `index` argument if present. Currently `tab_focus` and one other command, `zoom`, compute their indices with the `arg_or_count` method. As a consequence, each:
- Uses the argument or count if only one is given
- Fails if both are given (with appropriate error message)
- Uses a default value if neither is given.
- Also, `tab_focus` uses the last tab if the count is zero, but I don't see how it's possible to pass a zero count.

This seems overly complicated to me, and it buries functionality that's cleanly expressed with conditionals in a function in a different file. So I propose getting rid of `arg_or_count` and replacing it with something like

``` python
index = count if count is not None else index
if index is None:
    index = # default value
```

The change in behavior for both commands would be to use the count rather than giving an error if both count and index are given.



Discussion:
I don't really have a strong opinion on this, but I can see how it could be useful to bind a key to something with an index, and then override that index when passing a count.

Your proposal sounds good to me, want to go ahead and implement it?

> Also, `tab_focus` uses the last tab if the count is zero, but I don't see how it's possible to pass a zero count.

I have no idea either off-hand :laughing: This was introduced over 2 years ago in 5a6208862ecfd9065d6fa6d61812b5b210296031 FWIW (and the handling of 0 existed even before that). I'm fine with getting rid of it.

On it. I assumed this issue was overkill, but it doesn't hurt to check.


Resolving patch:
diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py
--- a/qutebrowser/browser/commands.py
+++ b/qutebrowser/browser/commands.py
@@ -717,18 +717,17 @@ def zoom_out(self, count=1):
     def zoom(self, zoom: int=None, count=None):
         """Set the zoom level for the current tab.
 
-        The zoom can be given as argument or as [count]. If neither of both is
-        given, the zoom is set to the default zoom.
+        The zoom can be given as argument or as [count]. If neither is
+        given, the zoom is set to the default zoom. If both are given,
+        use [count].
 
         Args:
             zoom: The zoom percentage to set.
             count: The zoom percentage to set.
         """
-        try:
-            default = config.get('ui', 'default-zoom')
-            level = cmdutils.arg_or_count(zoom, count, default=default)
-        except ValueError as e:
-            raise cmdexc.CommandError(e)
+        level = count if count is not None else zoom
+        if level is None:
+            level = config.get('ui', 'default-zoom')
         tab = self._current_widget()
 
         try:
@@ -904,32 +903,30 @@ def tab_focus(self, index: typing.Union[str, int]=None, count=None):
         """Select the tab given as argument/[count].
 
         If neither count nor index are given, it behaves like tab-next.
+        If both are given, use count.
 
         Args:
             index: The tab index to focus, starting with 1. The special value
-                   `last` focuses the last focused tab. Negative indexes
-                   counts from the end, such that -1 is the last tab.
+                   `last` focuses the last focused tab (regardless of count).
+                   Negative indices count from the end, such that -1 is the
+                   last tab.
             count: The tab index to focus, starting with 1.
         """
         if index == 'last':
             self._tab_focus_last()
             return
-        if index is None and count is None:
+        index = count if count is not None else index
+        if index is None:
             self.tab_next()
             return
-        if index is not None and index < 0:
+        if index < 0:
             index = self._count() + index + 1
-        try:
-            idx = cmdutils.arg_or_count(index, count, default=1,
-                                        countzero=self._count())
-        except ValueError as e:
-            raise cmdexc.CommandError(e)
-        cmdutils.check_overflow(idx + 1, 'int')
-        if 1 <= idx <= self._count():
-            self._set_current_index(idx - 1)
+
+        if 1 <= index <= self._count():
+            self._set_current_index(index - 1)
         else:
             raise cmdexc.CommandError("There's no tab with index {}!".format(
-                idx))
+                index))
 
     @cmdutils.register(instance='command-dispatcher', scope='window')
     @cmdutils.argument('index', choices=['+', '-'])
diff --git a/qutebrowser/commands/cmdutils.py b/qutebrowser/commands/cmdutils.py
--- a/qutebrowser/commands/cmdutils.py
+++ b/qutebrowser/commands/cmdutils.py
@@ -48,36 +48,6 @@ def check_overflow(arg, ctype):
             "representation.".format(ctype))
 
 
-def arg_or_count(arg, count, default=None, countzero=None):
-    """Get a value based on an argument and count given to a command.
-
-    If both arg and count are set, ValueError is raised.
-    If only arg/count is set, it is used.
-    If none is set, a default is returned or ValueError is raised.
-
-    Args:
-        arg: The argument given to a command.
-        count: The count given to a command.
-        countzero: Special value if count is 0.
-
-    Return:
-        The value to use.
-    """
-    if count is not None and arg is not None:
-        raise ValueError("Both count and argument given!")
-    elif arg is not None:
-        return arg
-    elif count is not None:
-        if countzero is not None and count == 0:
-            return countzero
-        else:
-            return count
-    elif default is not None:
-        return default
-    else:
-        raise ValueError("Either count or argument have to be set!")
-
-
 def check_exclusive(flags, names):
     """Check if only one flag is set with exclusive flags.
 


Test patch:
diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature
--- a/tests/end2end/features/tabs.feature
+++ b/tests/end2end/features/tabs.feature
@@ -200,7 +200,7 @@ Feature: Tab management
 
     Scenario: :tab-focus with very big index
         When I run :tab-focus 99999999999999
-        Then the error "Numeric argument is too large for internal int representation." should be shown
+        Then the error "There's no tab with index 99999999999999!" should be shown
 
     Scenario: :tab-focus with count
         When I open data/numbers/1.txt
@@ -213,8 +213,14 @@ Feature: Tab management
             - data/numbers/3.txt
 
     Scenario: :tab-focus with count and index
-        When I run :tab-focus 2 with count 2
-        Then the error "Both count and argument given!" should be shown
+        When I open data/numbers/1.txt
+        And I open data/numbers/2.txt in a new tab
+        And I open data/numbers/3.txt in a new tab
+        And I run :tab-focus 4 with count 2
+        Then the following tabs should be open:
+            - data/numbers/1.txt
+            - data/numbers/2.txt (active)
+            - data/numbers/3.txt
 
     Scenario: :tab-focus last
         When I open data/numbers/1.txt
diff --git a/tests/end2end/features/zoom.feature b/tests/end2end/features/zoom.feature
--- a/tests/end2end/features/zoom.feature
+++ b/tests/end2end/features/zoom.feature
@@ -71,7 +71,8 @@ Feature: Zooming in and out
 
     Scenario: Setting zoom with argument and count
         When I run :zoom 50 with count 60
-        Then the error "Both count and argument given!" should be shown
+        Then the message "Zoom level: 60%" should be shown
+        And the zoom should be 60%
 
     # Fixed in QtWebEngine branch
     @xfail
diff --git a/tests/unit/commands/test_cmdutils.py b/tests/unit/commands/test_cmdutils.py
--- a/tests/unit/commands/test_cmdutils.py
+++ b/tests/unit/commands/test_cmdutils.py
@@ -64,29 +64,6 @@ def test_bad(self):
         assert str(excinfo.value) == expected_str
 
 
-class TestArgOrCount:
-
-    @pytest.mark.parametrize('arg, count', [(None, None), (1, 1)])
-    def test_exceptions(self, arg, count):
-        with pytest.raises(ValueError):
-            cmdutils.arg_or_count(arg, count)
-
-    @pytest.mark.parametrize('arg, count', [(1, None), (None, 1)])
-    def test_normal(self, arg, count):
-        assert cmdutils.arg_or_count(arg, count) == 1
-
-    @pytest.mark.parametrize('arg, count, countzero, expected', [
-        (0, None, 2, 0),
-        (None, 0, 2, 2),
-    ])
-    def test_countzero(self, arg, count, countzero, expected):
-        ret = cmdutils.arg_or_count(arg, count, countzero=countzero)
-        assert ret == expected
-
-    def test_default(self):
-        assert cmdutils.arg_or_count(None, None, default=2) == 2
-
-
 class TestCheckExclusive:
 
     @pytest.mark.parametrize('flags', [[], [False, True], [False, False]])