From 4d7504601b9db9eb6f31dfc102b9c469c8517496 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 27 Nov 2023 02:56:27 -0800 Subject: [PATCH] Add option to load traitlets values from environement. (#856) --- examples/myapp.py | 49 +++++++++++++++++++++++++++++++-- traitlets/config/application.py | 24 ++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/examples/myapp.py b/examples/myapp.py index 009f9209..e498146b 100755 --- a/examples/myapp.py +++ b/examples/myapp.py @@ -36,6 +36,14 @@ from traitlets.config.configurable import Configurable +class SubConfigurable(Configurable): + subvalue = Int(0, help="The integer subvalue.").tag(config=True) + + def describe(self): + print("I am SubConfigurable with:") + print(" subvalue =", self.subvalue) + + class Foo(Configurable): """A class that has configurable, typed attributes.""" @@ -44,9 +52,39 @@ class Foo(Configurable): name = Unicode("Brian", help="First name.").tag(config=True, shortname="B") mode = Enum(values=["on", "off", "other"], default_value="on").tag(config=True) + def __init__(self, **kwargs): + super().__init__(**kwargs) + # using parent=self allows configuration in the form c.Foo.SubConfigurable.subvalue=1 + # while c.SubConfigurable.subvalue=1 will still work, this allow to + # target specific instances of SubConfigurables + self.subconf = SubConfigurable(parent=self) + + def describe(self): + print("I am Foo with:") + print(" i =", self.i) + print(" j =", self.j) + print(" name =", self.name) + print(" mode =", self.mode) + self.subconf.describe() + class Bar(Configurable): enabled = Bool(True, help="Enable bar.").tag(config=True) + mylist = List([1, 2, 3], help="Just a list.").tag(config=True) + + def describe(self): + print("I am Bar with:") + print(" enabled = ", self.enabled) + print(" mylist = ", self.mylist) + self.subconf.describe() + + def __init__(self, **kwargs): + super().__init__(**kwargs) + # here we do not use parent=self, so configuration in the form + # c.Bar.SubConfigurable.subvalue=1 will not work. Only + # c.SubConfigurable.subvalue=1 will work and affect all instances of + # SubConfigurable + self.subconf = SubConfigurable(config=self.config) class MyApp(Application): @@ -76,8 +114,8 @@ class MyApp(Application): ) def init_foo(self): - # Pass config to other classes for them to inherit the config. - self.foo = Foo(config=self.config) + # You can pass self as parent to automatically propagate config. + self.foo = Foo(parent=self) def init_bar(self): # Pass config to other classes for them to inherit the config. @@ -87,12 +125,14 @@ def initialize(self, argv=None): self.parse_command_line(argv) if self.config_file: self.load_config_file(self.config_file) + self.load_config_environ() self.init_foo() self.init_bar() def start(self): print("app.config:") print(self.config) + self.describe() print("try running with --help-all to see all available flags") assert self.log is not None self.log.debug("Debug Message") @@ -100,6 +140,11 @@ def start(self): self.log.warning("Warning Message") self.log.critical("Critical Message") + def describe(self): + print("I am MyApp with", self.name, self.running, "and 2 sub configurables Foo and bar:") + self.foo.describe() + self.bar.describe() + def main(): app = MyApp() diff --git a/traitlets/config/application.py b/traitlets/config/application.py index b8e0dfa1..9a3fcc94 100644 --- a/traitlets/config/application.py +++ b/traitlets/config/application.py @@ -23,6 +23,7 @@ ArgumentError, Config, ConfigFileNotFound, + DeferredConfigString, JSONFileConfigLoader, KVArgParseConfigLoader, PyFileConfigLoader, @@ -970,6 +971,29 @@ def load_config_file( new_config.merge(self.cli_config) self.update_config(new_config) + @catch_config_error + def load_config_environ(self) -> None: + """Load config files by environment.""" + + PREFIX = self.name.upper() + new_config = Config() + + self.log.debug('Looping through config variables with prefix "%s"', PREFIX) + + for k, v in os.environ.items(): + if k.startswith(PREFIX): + self.log.debug('Seeing environ "%s"="%s"', k, v) + # use __ instead of . as separator in env variable. + # Warning, case sensitive ! + _, *path, key = k.split("__") + section = new_config + for p in path: + section = section[p] + setattr(section, key, DeferredConfigString(v)) + + new_config.merge(self.cli_config) + self.update_config(new_config) + def _classes_with_config_traits( self, classes: ClassesType | None = None ) -> t.Generator[type[Configurable], None, None]: