Repository: ipython/traitlets
Pull request #810 (2022-12-07 09:52:20)

Minor fixes for Application.aliases
1. Fix the type annotations for Applications.flags / aliases, which support tuples as keys and results in linting errors (e.g. in the test suite).
2. Move the logic for determining what to do with an unrecognized command-line argument to a helper method. Currently a warning is logged, but in past versions of traitlets an error was raised, and this might be more user-friendly in some cases especially where the stderr can be pretty verbose (e.g. launching Jupyterlab).


Patch:
diff --git a/traitlets/config/application.py b/traitlets/config/application.py
index 533bffb..ec2b43e 100644
--- a/traitlets/config/application.py
+++ b/traitlets/config/application.py
@@ -355,14 +355,18 @@ class Application(SingletonConfigurable):
 
     #: the alias map for configurables
     #: Keys might strings or tuples for additional options; single-letter alias accessed like `-v`.
-    #: Values might be like "Class.trait" strings of two-tuples: (Class.trait, help-text).
-    aliases: t.Dict[str, t.Any] = {"log-level": "Application.log_level"}
+    #: Values might be like "Class.trait" strings of two-tuples: (Class.trait, help-text),
+    #  or just the "Class.trait" string, in which case the help text is inferred from the
+    #  corresponding trait
+    aliases: t.Dict[t.Union[str, t.Tuple[str, ...]], t.Union[str, t.Tuple[str, str]]] = {
+        "log-level": "Application.log_level"
+    }
 
     # flags for loading Configurables or store_const style flags
     # flags are loaded from this dict by '--key' flags
     # this must be a dict of two-tuples, the first element being the Config/dict
     # and the second being the help string for the flag
-    flags: t.Dict[str, t.Any] = {
+    flags: t.Dict[t.Union[str, t.Tuple[str, ...]], t.Tuple[t.Union[t.Dict, Config], str]] = {
         "debug": (
             {
                 "Application": {
diff --git a/traitlets/config/loader.py b/traitlets/config/loader.py
index d4ec90b..9c970b1 100644
--- a/traitlets/config/loader.py
+++ b/traitlets/config/loader.py
@@ -1049,8 +1049,7 @@ class KVArgParseConfigLoader(ArgParseConfigLoader):
 
             lhs = lhs.replace(_DOT_REPLACEMENT, ".")
             if "." not in lhs:
-                # probably a mistyped alias, but not technically illegal
-                self.log.warning("Unrecognized alias: '%s', it will have no effect.", lhs)
+                self._handle_unrecognized_alias(lhs)
                 trait = None
 
             if isinstance(rhs, list):
@@ -1075,6 +1074,15 @@ class KVArgParseConfigLoader(ArgParseConfigLoader):
         for subc in self.parsed_data._flags:
             self._load_flag(subc)
 
+    def _handle_unrecognized_alias(self, arg: str):
+        """Handling for unrecognized alias arguments
+
+        Probably a mistyped alias. By default just log a warning,
+        but users can override this to raise an error instead, e.g.
+        self.parser.error("Unrecognized alias: '%s'" % arg)
+        """
+        self.log.warning("Unrecognized alias: '%s', it will have no effect.", arg)
+
 
 class KeyValueConfigLoader(KVArgParseConfigLoader):
     """Deprecated in traitlets 5.0


Test patch:
diff --git a/traitlets/config/tests/test_application.py b/traitlets/config/tests/test_application.py
index 6904d88..57d71ba 100644
--- a/traitlets/config/tests/test_application.py
+++ b/traitlets/config/tests/test_application.py
@@ -21,7 +21,7 @@ from pytest import mark
 from traitlets import Bool, Bytes, Dict, HasTraits, Integer, List, Set, Tuple, Unicode
 from traitlets.config.application import Application
 from traitlets.config.configurable import Configurable
-from traitlets.config.loader import Config
+from traitlets.config.loader import Config, KVArgParseConfigLoader
 from traitlets.tests.utils import (
     check_help_all_output,
     check_help_output,
@@ -401,6 +401,30 @@ class TestApplication(TestCase):
         self.assertIn("Equivalent to: [--Foo.j]", hmsg)
         self.assertIn("Equivalent to: [--Foo.name]", hmsg)
 
+    def test_alias_unrecognized(self):
+        """Check ability to override handling for unrecognized aliases"""
+
+        class StrictLoader(KVArgParseConfigLoader):
+            def _handle_unrecognized_alias(self, arg):
+                self.parser.error("Unrecognized alias: %s" % arg)
+
+        class StrictApplication(Application):
+            def _create_loader(self, argv, aliases, flags, classes):
+                return StrictLoader(argv, aliases, flags, classes=classes, log=self.log)
+
+        app = StrictApplication()
+        app.initialize(["--log-level=20"])  # recognized alias
+        assert app.log_level == 20
+
+        app = StrictApplication()
+        with pytest.raises(SystemExit, match="2"):
+            app.initialize(["--unrecognized=20"])
+
+        # Ideally we would use pytest capsys fixture, but fixtures are incompatible
+        # with unittest.TestCase-style classes :(
+        # stderr = capsys.readouterr().err
+        # assert "Unrecognized alias: unrecognized" in stderr
+
     def test_flag_clobber(self):
         """test that setting flags doesn't clobber existing settings"""
         app = MyApp()