diff --git a/traitlets/config/loader.py b/traitlets/config/loader.py index d6d90ee4..d47925f8 100644 --- a/traitlets/config/loader.py +++ b/traitlets/config/loader.py @@ -408,7 +408,10 @@ def get_value(self, trait): s = str(self) try: return trait.from_string(s) - except Exception: + except Exception as e: + warnings.warn( + f"Error calling {trait.name}.from_string({s!r}): {e}", RuntimeWarning + ) # exception casting from string, # let the original string lie. # this will raise a more informative error when config is loaded. @@ -435,22 +438,28 @@ class DeferredConfigList(list, DeferredConfig): """ def get_value(self, trait): """Get the value stored in this string""" - if hasattr(trait, "from_string_list"): + if getattr(trait, "from_string_list", None) is not None: src = list(self) cast = trait.from_string_list + method_name = "from_string_list" else: # only allow one item if len(self) > 1: raise ValueError(f"{trait.name} only accepts one value, got {len(self)}: {list(self)}") src = self[0] cast = trait.from_string + method_name = "from_string" try: return cast(src) - except Exception: + except Exception as e: # exception casting from string, # let the original value lie. # this will raise a more informative error when config is loaded. + warnings.warn( + f"Error calling {trait.name}.{method_name}({src!r}): {e}", + RuntimeWarning, + ) return src def __repr__(self): diff --git a/traitlets/config/tests/test_application.py b/traitlets/config/tests/test_application.py index f0b5540b..9ec731bf 100644 --- a/traitlets/config/tests/test_application.py +++ b/traitlets/config/tests/test_application.py @@ -12,6 +12,7 @@ import logging import os import sys +from binascii import a2b_hex from io import StringIO from tempfile import TemporaryDirectory from unittest import TestCase @@ -629,6 +630,25 @@ def test_loaded_config_files(self): self.assertEqual(app.running, False) +def test_custom_from_string(): + path_items = sys.path[:5] + + path_str = os.pathsep.join(path_items) + + def from_path_string(s): + return s.split(os.pathsep) + + class App(Application): + path = List().tag(config=True, from_string=from_path_string) + hex = Bytes().tag(config=True, from_string=a2b_hex) + aliases = {"path": "App.path", "hex": "App.hex"} + + app = App() + app.parse_command_line(["--path", path_str, "--hex", "a1b2"]) + assert app.path == path_items + assert app.hex == b"\xa1\xb2" + + def test_cli_multi_scalar(caplog): class App(Application): aliases = {"opt": "App.opt"} diff --git a/traitlets/traitlets.py b/traitlets/traitlets.py index 8bc4e1fd..2666b3f6 100644 --- a/traitlets/traitlets.py +++ b/traitlets/traitlets.py @@ -720,12 +720,29 @@ def tag(self, **metadata): >>> Int(0).tag(config=True, sync=True) """ - maybe_constructor_keywords = set(metadata.keys()).intersection({'help','allow_none', 'read_only', 'default_value'}) + maybe_constructor_keywords = set(metadata.keys()).intersection( + {"help", "allow_none", "read_only", "default_value"} + ) if maybe_constructor_keywords: warn('The following attributes are set in using `tag`, but seem to be constructor keywords arguments: %s '% maybe_constructor_keywords, UserWarning, stacklevel=2) self.metadata.update(metadata) + + # allow from_string to be overridden via metadata + if self.metadata.get("from_string"): + self.from_string = self.metadata["from_string"] + if (not self.metadata.get("from_string_list")) and getattr( + self, "from_string_list", None + ): + self.from_string_list = None + # from_string overridden, from_string_list inherited and not overridden + # ensure inherited from_string_list does not take priority over our new from_string + # by removing it + + if self.metadata.get("from_string_list"): + self.from_string_list = self.metadata["from_string_list"] + return self def default_value_repr(self):