Repository: tomerfiliba/plumbum

Issue:
"IndexError: tuple index out of range" on missing arguments
![image](https://user-images.githubusercontent.com/19155205/34913485-79a6fe3a-f8c4-11e7-8771-2f6066a9bba6.png)

When trying to run a subcommand without the correct amount of arguments I get a python exception.

Using conda and python 3.6
plumbum version: - plumbum=1.6.5=py36_0

Comments:

Patch:
diff --git a/build.py b/build.py
index 1cfd980..cacb752 100755
--- a/build.py
+++ b/build.py
@@ -25,7 +25,7 @@ class BuildProject(cli.Application):
             if twine is None:
                 print("Twine not installed, cannot securely upload. Install twine.")
             else:
-                twine['upload','dist/*gztar' 'dist/*.exe' '*.whl'] & FG
+                twine['upload', 'dist/*tar.gz', 'dist/*.exe', 'dist/*.whl'] & FG
 
 
 if __name__ == "__main__":
diff --git a/dev-requirements.txt b/dev-requirements.txt
index db117b1..ebc89d8 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -1,5 +1,6 @@
 pytest
 pytest-cov
+pytest-mock
 pycparser<2.18 ; python_version < '2.7'
 paramiko<2.4 ; python_version < '2.7'
 paramiko ; python_version >= '2.7'
diff --git a/plumbum/cli/application.py b/plumbum/cli/application.py
index 75bad0e..5434de1 100644
--- a/plumbum/cli/application.py
+++ b/plumbum/cli/application.py
@@ -406,15 +406,13 @@ class Application(object):
                 ngettext(
                     "Expected at least {0} positional argument, got {1}",
                     "Expected at least {0} positional arguments, got {1}",
-                    min_args).format(
-                        min_args, tailargs))
+                    min_args).format(min_args, tailargs))
         elif len(tailargs) > max_args:
             raise PositionalArgumentsError(
                 ngettext(
                     "Expected at most {0} positional argument, got {1}",
                     "Expected at most {0} positional arguments, got {1}",
-                    max_args).format(
-                        max_args, tailargs))
+                    max_args).format(max_args, tailargs))
 
         # Positional arguement validataion
         if hasattr(self.main, 'positional'):
diff --git a/plumbum/cli/i18n.py b/plumbum/cli/i18n.py
index b85db2c..45ef8b9 100644
--- a/plumbum/cli/i18n.py
+++ b/plumbum/cli/i18n.py
@@ -8,9 +8,9 @@ if loc is None or loc.startswith('en'):
             return str
         def ngettext(self, str1, strN, n):
             if n==1:
-                return str1.format(n)
+                return str1.replace("{0}", str(n))
             else:
-                return strN.format(n)
+                return strN.replace("{0}", str(n))
 
     def get_translation_for(package_name):
         return NullTranslation()
diff --git a/plumbum/machines/ssh_machine.py b/plumbum/machines/ssh_machine.py
index e963745..68819a2 100644
--- a/plumbum/machines/ssh_machine.py
+++ b/plumbum/machines/ssh_machine.py
@@ -276,6 +276,7 @@ class PuttyMachine(SshMachine):
             user = local.env.user
         if port is not None:
             ssh_opts.extend(["-P", str(port)])
+            scp_opts = list(scp_opts) + ["-P", str(port)]
             port = None
         SshMachine.__init__(self, host, user, port, keyfile = keyfile, ssh_command = ssh_command,
             scp_command = scp_command, ssh_opts = ssh_opts, scp_opts = scp_opts, encoding = encoding,
@@ -292,5 +293,3 @@ class PuttyMachine(SshMachine):
     def session(self, isatty = False, new_session = False):
         return ShellSession(self.popen((), (["-t"] if isatty else ["-T"]), new_session = new_session),
             self.custom_encoding, isatty, self.connect_timeout)
-
-


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 95bc7c6..71298de 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -25,7 +25,9 @@ class SimpleApp(cli.Application):
         self.eggs = old
         self.tailargs = args
 
-
+class PositionalApp(cli.Application):
+    def main(self, one):
+        print("Got", one)
 
 class Geet(cli.Application):
     debug = cli.Flag("--debug")
@@ -123,6 +125,24 @@ class TestCLI:
         _, rc = SimpleApp.run(["foo", "--bacon=hello"], exit = False)
         assert rc == 2
 
+
+    # Testing #371
+    def test_extra_args(self, capsys):
+
+        _, rc = PositionalApp.run(["positionalapp"], exit = False)
+        assert rc != 0
+        stdout, stderr = capsys.readouterr()
+        assert "Expected at least" in stdout
+
+        _, rc = PositionalApp.run(["positionalapp", "one"], exit = False)
+        assert rc == 0
+        stdout, stderr = capsys.readouterr()
+
+        _, rc = PositionalApp.run(["positionalapp", "one", "two"], exit = False)
+        assert rc != 0
+        stdout, stderr = capsys.readouterr()
+        assert "Expected at most" in stdout
+
     def test_subcommands(self):
         _, rc = Geet.run(["geet", "--debug"], exit = False)
         assert rc == 0
diff --git a/tests/test_putty.py b/tests/test_putty.py
new file mode 100644
index 0000000..763407f
--- /dev/null
+++ b/tests/test_putty.py
@@ -0,0 +1,69 @@
+"""Test that PuttyMachine initializes its SshMachine correctly"""
+
+import pytest
+from plumbum import PuttyMachine, SshMachine
+
+from plumbum._testtools import xfail_on_pypy
+
+
+@pytest.fixture(params=['default', '322'])
+def ssh_port(request):
+    return request.param
+
+
+class TestPuttyMachine:
+    @xfail_on_pypy
+    def test_putty_command(self, mocker, ssh_port):
+        local = mocker.patch('plumbum.machines.ssh_machine.local')
+        init = mocker.spy(SshMachine, '__init__')
+        mocker.patch('plumbum.machines.ssh_machine.BaseRemoteMachine')
+
+        host = mocker.MagicMock()
+        user = local.env.user
+        port = keyfile = None
+        ssh_command = local["plink"]
+        scp_command = local["pscp"]
+        ssh_opts = ["-ssh"]
+        if ssh_port == 'default':
+            putty_port = None
+            scp_opts = ()
+        else:
+            putty_port = int(ssh_port)
+            ssh_opts.extend(['-P', ssh_port])
+            scp_opts = ['-P', ssh_port]
+        encoding = mocker.MagicMock()
+        connect_timeout = 20
+        new_session = True
+
+        PuttyMachine(
+            host,
+            port=putty_port,
+            connect_timeout=connect_timeout,
+            new_session=new_session,
+            encoding=encoding,
+        )
+
+        init.assert_called_with(
+            mocker.ANY,
+            host,
+            user,
+            port,
+            keyfile=keyfile,
+            ssh_command=ssh_command,
+            scp_command=scp_command,
+            ssh_opts=ssh_opts,
+            scp_opts=scp_opts,
+            encoding=encoding,
+            connect_timeout=connect_timeout,
+            new_session=new_session,
+        )
+
+    def test_putty_str(self, mocker):
+        local = mocker.patch('plumbum.machines.ssh_machine.local')
+        mocker.patch('plumbum.machines.ssh_machine.BaseRemoteMachine')
+
+        host = mocker.MagicMock()
+        user = local.env.user
+
+        machine = PuttyMachine(host)
+        assert str(machine) == 'putty-ssh://{0}@{1}'.format(user, host)