Repository: epsy/clize

Issue:
Errors when using dictionnary to specify alternate commands 
Hi,

Thanks for this project....

I am reading examples from documentation and get problem/error trying to use  dictionnary as alternate commands list.

Following is the code (mainly copied from documentation) and the errors i get.

What i am doing wrong ? 

```python 

from clize import ArgumentError, Parameter, run

VERSION = "0.2"

#------------------------------------------------------------------------------
def echo(*text:Parameter.REQUIRED,
            prefix:'p'='', suffix:'s'='', reverse:'r'=False, repeat:'n'=1):
    """Echoes text back

    :param text: The text to echo back
    :param reverse: Reverse text before processing
    :param repeat: Amount of times to repeat text
    :param prefix: Prepend this to each line in word
    :param suffix: Append this to each line in word
    """
    text = ' '.join(text)

    if 'spam' in text:
        raise ArgumentError("I don't want any spam!")

    if reverse:
        text = text[::-1]

    text = text * repeat

    if prefix or suffix:
        return '\n'.join(prefix + line + suffix for line in text.split('\n'))

    return text

#------------------------------------------------------------------------------
def version():
    """Show the version"""
    return (f"version={VERSION}")

#------------------------------------------------------------------------------
def build_date(*,show_time=False):
    """Show the build date for this version"""
    print("Build date: 17 August 1979", end='')
    if show_time:
        print(" afternoon, about tea time")
    print()


#------------------------------------------------------------------------------
if __name__ == '__main__':
    # next is OK
    # run(echo, alt=[version, build_date])
    # this not working 
    run(echo,  alt= 
        {   "totally_not_the_version": version,
            "birth": build_date
        })
```

Tested with python 3.6.9 and 3.9.0

```bash
cd /home/user/MyPyProjs/Py390/CliTests
venv_activate
(venv) > python clize_t01.py

Traceback (most recent call last):
  File "/home/user/MyPyProjs/Py390/CliTests/clize_t01.py", line 51, in <module>
    run(echo, alt={
  File "/home/user/MyPyProjs/Py390/CliTests/venv/lib/python3.9/site-packages/sigtools/modifiers.py", line 161, in __call__
    return self.func(*args, **kwargs)
  File "/home/user/MyPyProjs/Py390/CliTests/venv/lib/python3.9/site-packages/clize/runner.py", line 395, in run
    ret = cli(*args)
  File "/home/user/MyPyProjs/Py390/CliTests/venv/lib/python3.9/site-packages/clize/runner.py", line 261, in __call__
    func, name, posargs, kwargs = self.read_commandline(args)
  File "/home/user/MyPyProjs/Py390/CliTests/venv/lib/python3.9/site-packages/clize/runner.py", line 271, in read_commandline
    ba = self.signature.read_arguments(args[1:], args[0])
  File "/home/user/MyPyProjs/Py390/CliTests/venv/lib/python3.9/site-packages/clize/util.py", line 121, in __get__
    ret = obj.__dict__[self.key] = self.func(obj)
  File "/home/user/MyPyProjs/Py390/CliTests/venv/lib/python3.9/site-packages/clize/runner.py", line 220, in signature
    return parser.CliSignature.from_signature(
  File "/home/user/MyPyProjs/Py390/CliTests/venv/lib/python3.9/site-packages/clize/parser.py", line 1143, in from_signature
    return cls(
  File "/home/user/MyPyProjs/Py390/CliTests/venv/lib/python3.9/site-packages/clize/parser.py", line 1107, in __init__
    for param in _develop_extras(parameters):
  File "/home/user/MyPyProjs/Py390/CliTests/venv/lib/python3.9/site-packages/clize/parser.py", line 1052, in _develop_extras
    for param in params:
  File "/home/user/MyPyProjs/Py390/CliTests/venv/lib/python3.9/site-packages/clize/runner.py", line 252, in _process_alt
    for name, func in util.dict_from_names(alt).items():
  File "/home/user/MyPyProjs/Py390/CliTests/venv/lib/python3.9/site-packages/clize/util.py", line 104, in dict_from_names
    receiver.update((x.__name__, x) for x in maybe_iter(obj))
  File "/home/user/MyPyProjs/Py390/CliTests/venv/lib/python3.9/site-packages/clize/util.py", line 104, in <genexpr>
    receiver.update((x.__name__, x) for x in maybe_iter(obj))
AttributeError: 'str' object has no attribute '__name__'
```




Discussion:

Patch:
diff --git a/clize/runner.py b/clize/runner.py
index ccfcc9c..36966b2 100644
--- a/clize/runner.py
+++ b/clize/runner.py
@@ -78,7 +78,7 @@ class Clize(object):
         update_wrapper(self, fn)
         self.func = fn
         self.owner = owner
-        self.alt = util.maybe_iter(alt)
+        self.alt = util.dict_from_names(alt)
         self.extra = extra
         self.help_names = help_names
         self.help_aliases = [util.name_py2cli(s, kw=True) for s in help_names]
@@ -103,7 +103,7 @@ class Clize(object):
         return (
             self.func,
             self.owner,
-            tuple(self.alt),
+            tuple(self.alt.items()),
             tuple(self.extra),
             tuple(self.help_names),
             tuple(self.help_aliases),
@@ -215,7 +215,7 @@ class Clize(object):
     @util.property_once
     def signature(self):
         """The `.parser.CliSignature` object used to parse arguments."""
-        extra = itertools.chain(self._process_alt(self.alt), self.extra)
+        extra = itertools.chain(self._process_alt(), self.extra)
         with self._move_warnings_to_func():
             return parser.CliSignature.from_signature(
                 self.func_signature,
@@ -242,14 +242,14 @@ class Clize(object):
                 registry = module_globals.setdefault("__warningregistry__", {})
                 warnings.warn_explicit(warning.message, warning.category, filename, lineno, module, registry, module_globals)
 
-    def _process_alt(self, alt):
+    def _process_alt(self):
         if self.help_names:
             p = parser.FallbackCommandParameter(
                 func=self.helper.cli, undocumented=self.hide_help,
                 aliases=self.help_aliases)
             yield p
 
-        for name, func in util.dict_from_names(alt).items():
+        for name, func in self.alt.items():
             func = self.get_cli(func)
             param = parser.AlternateCommandParameter(
                 undocumented=False, func=func,
diff --git a/docs/releases.rst b/docs/releases.rst
index 7537a71..beeabc9 100644
--- a/docs/releases.rst
+++ b/docs/releases.rst
@@ -5,6 +5,13 @@
 Release notes
 =============
 
+.. _v5.0.1:
+
+5.0.1 (2023-07-06)
+------------------
+
+* Fixed ``run(..., alt={...})`` not working correctly with dicts
+
 .. _v5.0:
 .. _v5.0.0:
 


Test patch:
diff --git a/clize/tests/test_runner.py b/clize/tests/test_runner.py
index e71a198..067a355 100644
--- a/clize/tests/test_runner.py
+++ b/clize/tests/test_runner.py
@@ -491,6 +491,32 @@ class RunnerTests(Tests):
         self.assertFalse(stderr.getvalue())
         self.assertEqual(stdout.getvalue(), '2\n')
 
+    def test_alt_list(self):
+        def base(): return '0'
+        def func1(): return '1'
+        def func2(): return '2'
+        alt = [func1, func2]
+        self.crun(base, alt=alt, args=['test'])
+        stdout, stderr = self.crun(base, alt=alt, args=['test', '--func1'])
+        self.assertFalse(stderr.getvalue())
+        self.assertEqual(stdout.getvalue(), '1\n')
+        stdout, stderr = self.crun(base, alt=alt, args=['test', '--func2'])
+        self.assertFalse(stderr.getvalue())
+        self.assertEqual(stdout.getvalue(), '2\n')
+
+    def test_alt_dict(self):
+        def base(): return '0'
+        def func1(): return '1'
+        def func2(): return '2'
+        alt = {'1': func1, '2': func2}
+        self.crun(base, alt=alt, args=['test'])
+        stdout, stderr = self.crun(base, alt=alt, args=['test', '-1'])
+        self.assertFalse(stderr.getvalue())
+        self.assertEqual(stdout.getvalue(), '1\n')
+        stdout, stderr = self.crun(base, alt=alt, args=['test', '-2'])
+        self.assertFalse(stderr.getvalue())
+        self.assertEqual(stdout.getvalue(), '2\n')
+
     def test_disable_help(self):
         def func1(): raise NotImplementedError
         stdout, stderr = self.crun(