Repository: ipython/traitlets (python)

Issue:
Multiple aliases for ArgParseConfigLoader
Hi, [in v5.0 L990](https://github.com/ipython/traitlets/commit/e3e01bc2cf2fcb4c88f6d954e2fd4e4dc283f76a#diff-faeef4cfbb1bced1eba160187ed70f9a7f115e86a98cf5ffdefcc9ef52fd527dR983), alias tuples are explicitly unpacked as 2-element tuples. I didn't manage to find any discussion on this, but I didn't see an obvious reason for introducing this restriction so I was wondering if this was accidental. The documentation currently just states that `A key in both those dictionaries [flags & aliases] might be a string or tuple of strings`. I can re-structure my applications to enumerate aliases separately, but it seems a little unnecessary and increases the length of the help menu.

 ```diff
                   if not isinstance(alias, tuple):
879,882c879,880
<                     short_alias, alias = alias, None
<                 else:
<                     short_alias, alias = alias
<                 for al in (short_alias, alias):
<                     if al is None:
<                         continue
---
>                     alias = (alias,)
>                 for al in alias:
```

Happy to submit PR / unit tests if I didn't miss something.             

Pull request description:
Re-support multiple-alias key for ArgParseConfigLoader
Its sometimes useful to be able to support multiple aliases for the same trait, e.g. multiple shorthand abbreviations or supporting both underscores and dashes. Re-add support for an arbitrary number of aliases to be possible as a key in `Application.aliases` and added a simple unit test.

Closes #686 

Patch:
diff --git a/traitlets/config/loader.py b/traitlets/config/loader.py
index 14a6863..d4ec90b 100644
--- a/traitlets/config/loader.py
+++ b/traitlets/config/loader.py
@@ -787,12 +787,13 @@ class ArgParseConfigLoader(CommandLineConfigLoader):
     """A loader that uses the argparse module to load from the command line."""
 
     parser_class = ArgumentParser
+    Flags = t.Union[str, t.Tuple[str, ...]]
 
     def __init__(
         self,
         argv: t.Optional[t.List[str]] = None,
-        aliases: t.Optional[t.Dict[str, str]] = None,
-        flags: t.Optional[t.Dict[str, str]] = None,
+        aliases: t.Optional[t.Dict[Flags, str]] = None,
+        flags: t.Optional[t.Dict[Flags, str]] = None,
         log: t.Any = None,
         classes: t.Optional[t.List[t.Type[t.Any]]] = None,
         *parser_args: t.Any,
@@ -815,9 +816,9 @@ class ArgParseConfigLoader(CommandLineConfigLoader):
             A tuple of keyword arguments that will be passed to the
             constructor of :class:`argparse.ArgumentParser`.
         aliases : dict of str to str
-            Dict of aliases to full traitlests names for CLI parsing
+            Dict of aliases to full traitlets names for CLI parsing
         flags : dict of str to str
-            Dict of flags to full traitlests names for CLI parsing
+            Dict of flags to full traitlets names for CLI parsing
         log
             Passed to `ConfigLoader`
 
@@ -902,12 +903,8 @@ class ArgParseConfigLoader(CommandLineConfigLoader):
                 if alias in self.flags:
                     continue
                 if not isinstance(alias, tuple):
-                    short_alias, alias = alias, None  # type:ignore[assignment]
-                else:
-                    short_alias, alias = alias
-                for al in (short_alias, alias):
-                    if al is None:
-                        continue
+                    alias = (alias,)
+                for al in alias:
                     if len(al) == 1:
                         unpacked_aliases["-" + al] = "--" + alias_target
                     unpacked_aliases["--" + al] = "--" + alias_target


Test patch:
diff --git a/traitlets/config/tests/test_application.py b/traitlets/config/tests/test_application.py
index f38765c..6904d88 100644
--- a/traitlets/config/tests/test_application.py
+++ b/traitlets/config/tests/test_application.py
@@ -371,6 +371,24 @@ class TestApplication(TestCase):
         app.init_foo()
         self.assertEqual(app.foo.j, 10)
 
+    def test_aliases_multiple(self):
+        # Test multiple > 2 aliases for the same argument
+        class TestMultiAliasApp(Application):
+            foo = Integer(config=True)
+            aliases = {("f", "bar", "qux"): "TestMultiAliasApp.foo"}
+
+        app = TestMultiAliasApp()
+        app.parse_command_line(["-f", "3"])
+        self.assertEqual(app.foo, 3)
+
+        app = TestMultiAliasApp()
+        app.parse_command_line(["--bar", "4"])
+        self.assertEqual(app.foo, 4)
+
+        app = TestMultiAliasApp()
+        app.parse_command_line(["--qux", "5"])
+        self.assertEqual(app.foo, 5)
+
     def test_aliases_help_msg(self):
         app = MyApp()
         stdout = io.StringIO()