bundles / traitlets latest /docs
Doc
Configurable objects with traitlets.config
docs/config
This document describes traitlets.config, the traitlets-based configuration system used by IPython and Jupyter.
The main concepts
There are a number of abstractions that the IPython configuration system uses. Each of these abstractions is represented by a Python class.
Configuration object: Config
A configuration object is a simple dictionary-like class that holds configuration attributes and sub-configuration objects. These classes support dotted attribute style access (
cfg.Foo.bar) in addition to the regular dictionary style access (cfg['Foo']['bar']). The Config object is a wrapper around a simple dictionary with some convenience methods, such as merging and automatic section creation.Application: Application
An application is a process that does a specific job. The most obvious application is the
ipythoncommand line program. Each application may read configuration files and a single set of command line options and then produces a master configuration object for the application. This configuration object is then passed to the configurable objects that the application creates, usually either via the config orparentconstructor arguments. These configurable objects implement the actual logic of the application and know how to configure themselves given the configuration object.Applications always have a
logattribute that is a configured Logger. This allows centralized logging configuration per-application.Configurable: Configurable
A configurable is a regular Python class that serves as a base class for all main classes in an application. The Configurable base class is lightweight and only does one thing.
This Configurable is a subclass of HasTraits that knows how to configure itself. Class level traits with the metadata
config=Truebecome values that can be configured from the command line and configuration files.Developers create Configurable subclasses that implement all of the logic in the application. Each of these subclasses has its own configuration information that controls how instances are created. When constructing a Configurable, the config or
parentarguments can be passed to the constructor (respectively a Config and a Configurable object with a.config).Singletons: SingletonConfigurable
Any object for which there is a single canonical instance. These are just like Configurables, except they have a class method instance, that returns the current active instance (or creates one if it does not exist). Application is a singleton. This lets objects easily connect to the current running Application without passing objects around everywhere. For instance, to get the current running Application instance, simply do:
app = Application.instance().
Singleton scopes
By default instance resolves against a single, process-global registry. A SingletonScope provides an isolated registry that is active only for a dynamic extent (the current thread or asyncio task), leaving the global one untouched. This is useful when you need to run code that internally calls .instance() — code you do not control — against a fresh set of singletons, for example in a test or when serving concurrent requests.
Create a scope from the base class whose subtree it should cover with scope, then activate it as a context manager:
a = MyApp.instance() # process-global instance scope = MyApp.scope() # covers MyApp and its subclasses with scope(): third_party_function() # its internal MyApp.instance() calls... b = MyApp.instance() # ...resolve to this fresh instance assert b is not a assert MyApp.instance() is a # global untouched, exception-safe scope.get(MyApp) # -> b, retrieve what was created in-scope with scope(): # re-entering resumes the same registry assert MyApp.instance() is b
Key semantics:
Coverage is defined by the base class.
Foo.scope()only intercepts.instance()forFooand its subclasses; unrelated singletons keep resolving against the global registry. Pick a narrower base to limit the blast radius, orSingletonConfigurableitself for a complete blank slate (see below).No fallback to the global. Inside a scope, the first
.instance()call creates a fresh instance registered in the scope; instance never reads, creates, or mutates the process-global_instance, and initialized isFalseuntil that first in-scope creation.Pre-seeding. add registers an instance you built yourself, so that
.instance()returns that object inside the scope (see singleton-scope-injection).Nesting and propagation. Scopes nest; the innermost active scope covering a class wins, and blocks restore in LIFO order (even on exceptions). Because the active scope lives in a ContextVar, an asyncio task created inside a scope inherits it. A new threading.Thread started inside a scope inherits it only when sys.flags.thread_inherit_context is set (see the caveat below); otherwise the thread falls through to the global registry.
Direct construction is never intercepted.
MyApp()always builds a new, unregistered object, in or out of a scope.
Giving each worker thread its own isolated singleton works on every build: have every thread activate its own scope. Any .instance() calls made by that thread — directly or deep inside code it calls — then resolve to a per-thread instance, while the process-global singleton is left untouched:
import threading from traitlets.config import SingletonConfigurable class MyApp(SingletonConfigurable): pass seen = {} def worker(name): scope = MyApp.scope() # a fresh registry, private to this thread with scope(): # activate it for this thread's extent app = MyApp.instance() # created once, here... assert MyApp.instance() is app # ...and reused within the thread seen[name] = app threads = [threading.Thread(target=worker, args=(i,)) for i in range(3)] for t in threads: t.start() for t in threads: t.join() # each thread got its own distinct instance assert len({id(app) for app in seen.values()}) == 3 # and none of them is the process-global singleton assert all(app is not MyApp.instance() for app in seen.values())
A complete blank slate
Because coverage is just issubclass(cls, base), scoping the root class — SingletonConfigurable itself — covers every singleton in the process at once. This gives you a clean-room registry in which no singleton has been created yet, which is what you usually want when running a test, or any self-contained piece of work, in full isolation:
from traitlets.config import SingletonConfigurable class Foo(SingletonConfigurable): pass class Bar(SingletonConfigurable): pass foo, bar = Foo.instance(), Bar.instance() # process-global instances with SingletonConfigurable.scope()(): # covers every singleton # nothing has been created in here yet assert not Foo.initialized() assert not Bar.initialized() assert Foo.instance() is not foo # every subtree resolves fresh assert Bar.instance() is not bar # the process-global registry was never read, created, or mutated assert Foo.instance() is foo assert Bar.instance() is bar
Note the double call: SingletonConfigurable.scope() builds the scope and () activates it. Bind it to a name if you want to inspect the registry afterwards or re-enter it later
blank = SingletonConfigurable.scope() with blank(): ... blank.get(Foo) # what was created inside
Since this covers everything, it is the widest possible blast radius: prefer a narrower base when you only need to isolate one subtree.
Injecting a specific instance
Inside a scope, the first .instance() call constructs a fresh object of the class it was called on. When you need .instance() to hand back a particular object instead — a test double, a subclass, or something built by a factory — pre-seed the registry with add. This is the only way to control which object uncontrolled code receives:
class App(SingletonConfigurable): pass class FakeApp(App): # a stand-in used only by the test pass fake = FakeApp() scope = App.scope() scope.add(fake) # register before anything resolves with scope(): third_party_function() # its internal App.instance() returns `fake` assert App.instance() is fake
add writes through to the singleton ancestors of the instance's class, the same way the classic global path does, so the object is also returned when resolving via a parent class (App.instance() above, not just FakeApp.instance()).
Having described these main concepts, we can now state the main idea in our configuration system: "configuration" allows the default values of class attributes to be controlled on a class by class basis. Thus all instances of a given class are configured in the same way. Furthermore, if two instances need to be configured differently, they need to be instances of two different classes. While this model may seem a bit restrictive, we have found that it expresses most things that need to be configured extremely well. However, it is possible to create two instances of the same class that have different trait values. This is done by overriding the configuration.
Now, we show what our configuration objects and files look like.
Configuration objects and files
A configuration object is little more than a wrapper around a dictionary. A configuration file is simply a mechanism for producing that object. Configuration files currently can be a plain Python script or a JSON file. The former has the benefit that it can perform extensive logic to populate the config object, while the latter is just a direct JSON serialization of the config dictionary and can be easily processed by external software. When both Python and JSON configuration file are present, both will be loaded, with JSON configuration having higher priority.
The configuration files can be loaded by calling Application.load_config_file(), which takes the relative path to the config file (with or without file extension) and the directories in which to search for the config file. All found configuration files will be loaded in reverse order, so that configs in earlier directories will have higher priority.
Python configuration Files
A Python configuration file is a pure Python file that populates a configuration object. This configuration object is a Config instance. It is available inside the config file as c, and you simply set attributes on this. All you have to know is:
The name of the class to configure.
The name of the attribute.
The type of each attribute.
The answers to these questions are provided by the various Configurable subclasses that an application uses. Let's look at how this would work for a simple configurable subclass
# Sample configurable: from traitlets.config.configurable import Configurable from traitlets import Int, Float, Unicode, Bool class School(Configurable): name = Unicode("defaultname", help="the name of the object").tag(config=True) ranking = Integer(0, help="the class's ranking").tag(config=True) value = Float(99.0) # The rest of the class implementation would go here.. # Construct from config via School(config=..)
In this example, we see that School has three attributes, two of which (name, ranking) can be configured. All of the attributes are given types and default values. If a School is instantiated, but not configured, these default values will be used. But let's see how to configure this class in a configuration file
# Sample config file c.School.name = "coolname" c.School.ranking = 10
After this configuration file is loaded, the values set in it will override the class defaults anytime a School is created. Furthermore, these attributes will be type checked and validated anytime they are set. This type checking is handled by the traitlets module, which provides the Unicode, Integer and Float types; see Trait Types for the full list.
It should be very clear at this point what the naming convention is for configuration attributes
c.ClassName.attribute_name = attribute_valueHere, ClassName is the name of the class whose configuration attribute you want to set, attribute_name is the name of the attribute you want to set and attribute_value the value you want it to have. The ClassName attribute of c is not the actual class, but instead is another Config instance.
JSON configuration Files
A JSON configuration file is simply a file that contains a Config dictionary serialized to JSON. A JSON configuration file has the same base name as a Python configuration file, but with a .json extension.
Configuration described in previous section could be written as follows in a JSON configuration file:
{ "School": { "name": "coolname", "ranking": 10 } }
JSON configuration files can be more easily generated or processed by programs or other languages.
Configuration files inheritance
Let's say you want to have different configuration files for various purposes. Our configuration system makes it easy for one configuration file to inherit the information in another configuration file. The load_subconfig command can be used in a configuration file for this purpose. Here is a simple example that loads all of the values from the file base_config.py:
c = get_config() # noqa c.School.name = "Harvard" c.School.ranking = 100
into the configuration file main_config.py:
c = get_config() # noqa # Load everything from base_config.py load_subconfig("base_config.py") # noqa # Now override one of the values c.School.name = "bettername"
In a situation like this the load_subconfig makes sure that the search path for sub-configuration files is inherited from that of the parent. Thus, you can typically put the two in the same directory and everything will just work. An example app using these configuration files can be found at examples/docs/load_config_app.py.
Class based configuration inheritance
There is another aspect of configuration where inheritance comes into play. Sometimes, your classes will have an inheritance hierarchy that you want to be reflected in the configuration system. Here is a simple example:
from traitlets.config import Application, Configurable from traitlets import Integer, Float, Unicode, Bool class Foo(Configurable): name = Unicode("fooname", config=True) value = Float(100.0, config=True) class Bar(Foo): name = Unicode("barname", config=True) othervalue = Int(0, config=True) # construct Bar(config=..)
Now, we can create a configuration file to configure instances of Foo and Bar:
# config file c = get_config() # noqa c.Foo.name = "bestname" c.Bar.othervalue = 10
This class hierarchy and configuration file accomplishes the following:
The default value for
Foo.nameandBar.namewill be'bestname'. BecauseBaris aFoosubclass it also picks up the configuration information forFoo.The default value for
Foo.valueandBar.valuewill be100.0, which is the value specified as the class default.The default value for
Bar.othervaluewill be 10 as set in the configuration file. BecauseFoois the parent ofBarit doesn't know anything about theothervalueattribute.
Command-line arguments
All configurable options can also be supplied at the command line when launching the application. Internally, when Application.initialize() is called, a KVArgParseConfigLoader instance is constructed to load values into a Config object. (For advanced users, this can be overridden in the helper method Application._create_loader().)
Most command-line scripts simply need to call Application.launch_instance(), which will create the Application singleton, parse the command-line arguments, and start the application:
from traitlets.config import Application class MyApp(Application): def start(self): pass # app logic goes here if __name__ == "__main__": MyApp.launch_instance()
By default, config values are assigned from command-line arguments in much the same way as in a config file:
$ ipython --InteractiveShell.autoindent=False --BaseIPythonApplication.profile='myprofile'is the same as adding:
c.InteractiveShell.autoindent = False c.BaseIPythonApplication.profile = "myprofile"
to your configuration file. Command-line arguments take precedence over values read from configuration files. (This is done in Application.load_config_file() by merging Application.cli_config over values read from configuration files.)
Note that even though Application is a SingletonConfigurable, multiple applications could still be started and called from each other by constructing them as one would with any other Configurable:
from traitlets.config import Application class OtherApp(Application): def start(self): print("other") class MyApp(Application): classes = [OtherApp] def start(self): # similar to OtherApp.launch_instance(), but without singleton self.other_app = OtherApp(config=self.config) self.other_app.initialize(["--OtherApp.log_level", "INFO"]) self.other_app.start() if __name__ == "__main__": MyApp.launch_instance()
versionchanged 5.0
If a scalar (Unicode, Integer, etc.) is specified multiple times on the command-line, this will now raise. Prior to 5.0, all instances of the option before the last would be ignored.
versionchanged 5.0
In 5.0, positional extra arguments (typically a list of files) must be contiguous, for example
mycommand file1 file2 --flagor
mycommand --flag file1 file2whereas prior to 5.0, these "extra arguments" be distributed among other arguments
mycommand file1 --flag file2note
By default, an error in a configuration file will cause the configuration file to be ignored and a warning logged. Application subclasses may specify raise_config_file_errors = True to exit on failure to load config files instead.
versionadded 4.3
The environment variable TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR to '1' or 'true' to change the default value of raise_config_file_errors.
Common Arguments
Since the strictness and verbosity of the full --Class.trait=value form are not ideal for everyday use, common arguments can be specified as flags_ or aliases_.
In general, flags and aliases are prefixed by --, except for those that are single characters, in which case they can be specified with a single -, e.g.:
$ ipython -i -c "import numpy; x=numpy.linspace(0,1)" --profile testing --colors=lightbgFlags and aliases are declared by specifying flags and aliases attributes as dictionaries on subclasses of Application.
A key in both those dictionaries might be a string or tuple of strings. One-character strings are converted into "short" options (like -v); longer strings are "long" options (like --verbose).
Aliases
For convenience, applications have a mapping of commonly used traits, so you don't have to specify the whole class name:
$ ipython --profile myprofile # and $ ipython --profile='myprofile' # are equivalent to $ ipython --BaseIPythonApplication.profile='myprofile'
When specifying alias dictionary in code, the values might be the strings like 'Class.trait' or two-tuples like ('Class.trait', "Some help message"). For example:
from traitlets import Bool from traitlets.config import Application, Configurable class Foo(Configurable): enabled = Bool(False, help="whether enabled").tag(config=True) class App(Application): classes = [Foo] dry_run = Bool(False, help="dry run test").tag(config=True) aliases = { "dry-run": "App.dry_run", ("f", "foo-enabled"): ("Foo.enabled", "whether foo is enabled"), } if __name__ == "__main__": App.launch_instance()
By default, the --log-level alias will be set up for Application.log_level.
Flags
Applications can also be passed flags. Flags are options that take no arguments. They are simply wrappers for setting one or more configurables with predefined values, often True/False.
For instance:
$ ipcontroller --debug # is equivalent to $ ipcontroller --Application.log_level=DEBUG # and $ ipython --matplotlib # is equivalent to $ ipython --matplotlib auto # or $ ipython --no-banner # is equivalent to $ ipython --TerminalIPythonApp.display_banner=False
And a runnable code example:
from traitlets import Bool from traitlets.config import Application, Configurable class Foo(Configurable): enabled = Bool(False, help="whether enabled").tag(config=True) class App(Application): classes = [Foo] dry_run = Bool(False, help="dry run test").tag(config=True) flags = { "dry-run": ({"App": {"dry_run": True}}, dry_run.help), ("f", "enable-foo"): ( { "Foo": {"enabled": True}, }, "Enable foo", ), ("disable-foo"): ( { "Foo": {"enabled": False}, }, "Disable foo", ), } if __name__ == "__main__": App.launch_instance()
Since flags are a bit more complicated to set up, there are a couple of common patterns implemented in helper methods. For example, traitlets.config.boolean_flag() sets up the flags --x and --no-x. By default, the following few flags are set up: --debug (setting log_level=DEBUG), --show-config, and --show-config-json (print config to stdout and exit).
Subcommands
Configurable applications can also have subcommands. Subcommands are modeled after git, and are called with the form command subcommand [...args]. For instance, the QtConsole is a subcommand of terminal IPython:
$ jupyter qtconsole --profile myprofileSubcommands are specified as a dictionary assigned to a subcommands class member of Application instances. This dictionary maps subcommand names to two-tuples containing these:
A subclass of Application to handle the subcommand. This can be specified as:
simply as a class, where its SingletonConfigurable.instance() will be invoked (straight-forward, but loads subclasses on import time);
as a string which can be imported to produce the above class;
as a factory function accepting a single argument like that
app_factory(parent_app: Application) -> Applicationnote
The return value of the factory above is an instance, not a class,
so the SingletonConfigurable.instance() is not invoked in this case.
In all cases, the instantiated app is stored in
Application.subappand its Application.initialize() is invoked.A short description of the subcommand for use in help output.
For example (refer to examples/subcommands_app.py for a more complete example):
from traitlets.config import Application class SubApp1(Application): pass class SubApp2(Application): @classmethod def get_subapp_instance(cls, app: Application) -> Application: app.clear_instance() # since Application is singleton, need to clear main app return cls.instance(parent=app) class MainApp(Application): subcommands = { "subapp1": (SubApp1, "First subapp"), "subapp2": (SubApp2.get_subapp_instance, "Second subapp"), } if __name__ == "__main__": MainApp.launch_instance()
To see a list of the available aliases, flags, and subcommands for a configurable application, simply pass -h or --help. To see the full list of configurable options (very long), pass --help-all.
For more complete examples of setting up Application, refer to the application examples.
Other Application members
The following are typically set as class variables of Application subclasses, but can also be set as instance variables.
.classes: A list of Configurable classes. Similar to configs, any class name can be used in--Class.trait=valuearguments, including classes that the Application might not know about. However, the--help-allmenu will only enumerateconfigtraits of classes inApplication.classes. Similarly,.classesis used in other places where an application wants to list all configurable traits; examples include Application.generate_config_file() and the argcomplete handling..name,.description,.option_description,.keyvalue_description,.subcommand_description,.examples,.version: Various strings used in the--helpmenu and other messages.log_level,.log_datefmt,.log_format,.logging_config: Configurable options to control application logging, which is emitted via the loggerApplication.log. For more information about these, refer to their respective traits'.help..show_config,.show_config_json: Configurable boolean options, which if set toTrue, will cause the application to print the config to stdout instead of calling Application.start()
Additionally, the following are set by Application:
.cli_config: The Config created from the command-line arguments. This is saved to override any config values loaded from configuration files called by Application.load_config_file()..extra_args: This is a list holding any positional arguments remaining from the command-line arguments parsed during Application.initialize(). As noted earlier, these must be contiguous in the command-line.
Interpreting command-line strings
versionadded 5.0
Prior to 5.0, we only had good support for Unicode or similar string types on the command-line. Other types were supported via ast.literal_eval, which meant that simple types such as integers were well supported, too.
The downside of this implementation was that the literal_eval happened before the type of the target trait was known, meaning that strings that could be interpreted as literals could end up with the wrong type, famously
$ ipython -c 1 ... [TerminalIPythonApp] CRITICAL | Bad config encountered during initialization: [TerminalIPythonApp] CRITICAL | The 'code_to_run' trait of a TerminalIPythonApp instance must be a unicode string, but a value of 1 <class 'int'> was specified.
This resulted in requiring redundant "double-quoting" of strings in many cases. That gets confusing when the shell also interprets quotes, so one had to
$ ipython -c "'1'"in order to set a string that looks like an integer.
traitlets 5.0 defers parsing of interpreting command-line strings to from_string, which is an arbitrary function that will be called with the string given on the command-line. This eliminates the need to 'guess' how to interpret strings before we know what they are configuring.
Backward compatibility
It is not feasible to be perfectly backward-compatible when fixing behavior as problematic as this. However, we are doing our best to ensure that folks who had workarounds for this funky behavior are disrupted as little as we can manage. That means that we have kept what look like literals working wherever we could, so if you were double-quoting strings to ensure the were interpreted as strings, that will continue to work with warnings for the foreseeable future.
If you have an example command-line call that used to work with traitlets 4 but does not any more with traitlets 5, please let us know.
Custom traits
versionadded 5.0
Custom trait types can override from_string to specify how strings should be interpreted. This could for example allow specifying hex-encoded bytes on the command-line:
from binascii import a2b_hex from traitlets.config import Application from traitlets import Bytes class HexBytes(Bytes): def from_string(self, s): return a2b_hex(s) class App(Application): aliases = {"key": "App.key"} key = HexBytes( help=""" Key to be used. Specify as hex on the command-line. """, config=True, ) def start(self): print(f"key={self.key}") if __name__ == "__main__": App.launch_instance()
$ examples/docs/from_string.py --key=a1b2 key=b'\xa2\xb2'
Container traits
In traitlets 5.0, items for container traits can be specified by passing the key multiple times, e.g.
myprogram -l a -l bto produce the list ["a", "b"]
or for dictionaries use key=value
myprogram -d a=5 -d b=10to produce the dict {"a": 5, "b": 10}.
In traitlets prior to 5.0, container traits (List, Dict) could technically be configured on the command-line by specifying a repr of a Python list or dict, e.g
ipython --ScriptMagics.script_paths='{"perl": "/usr/bin/perl"}'but that gets pretty tedious, especially with more than a couple of fields. This still works with a FutureWarning, but the new way allows container items to be specified by passing the argument multiple times
ipython \ --ScriptMagics.script_paths perl=/usr/bin/perl \ --ScriptMagics.script_paths ruby=/usr/local/opt/bin/ruby
This handling is good enough that we can recommend defining aliases for container traits for the first time! For example:
from traitlets.config import Application from traitlets import Dict, Integer, List, Unicode class App(Application): aliases = {"x": "App.x", "y": "App.y"} x = List(Unicode(), config=True) y = Dict(Integer(), config=True) def start(self): print(f"x={self.x}") print(f"y={self.y}") if __name__ == "__main__": App.launch_instance()
produces:
$ examples/docs/container.py -x a -x b -y a=10 -y b=5 x=['a', 'b'] y={'a': 10, 'b': 5}
note
Specifying the value trait of Dict was necessary to cast the values in y to integers. Otherwise, they values of y would have been the strings '10' and '5'.
For container types, .List.from_string_list is called with the list of all values specified on the command-line and is responsible for turning the list of strings into the appropriate type. Each item is then passed to .List.item_from_string which is responsible for handling the item, such as casting to integer or parsing key=value in the case of a Dict.
The deprecated ast.literal_eval handling is preserved for backward-compatibility in the event of a single item that 'looks like' a list or dict literal.
If you would prefer, you can also use custom container traits which define :meth`~.TraitType.from_string` to expand a single string into a list, for example:
class PathList(List): def from_string(self, s): return s.split(os.pathsep)
which would allow
myprogram --path /bin:/usr/local/bin:/opt/binto set a PathList trait with ["/bin", "/usr/local/bin", "/opt/bin"].
Command-line tab completion with argcomplete
versionadded 5.8
traitlets has limited support for command-line tab completion for scripts based on Application using argcomplete. To use this, follow the instructions for setting up argcomplete; you will likely want to activate global completion by doing something alone the lines of:
# pip install argcomplete mkdir -p ~/.bash_completion.d/ activate-global-python-argcomplete --dest=~/.bash_completion.d/argcomplete # source ~/.bash_completion.d/argcomplete from your ~/.bashrc
(Follow relevant instructions for your shell.) For any script you want tab-completion to work on, include the line:
# PYTHON_ARGCOMPLETE_OKin the first 1024 bytes of the script.
The following options can be tab-completed:
Flags and aliases
The classes in
Application.classes, which can be initially completed as--Class.Once a completion is narrows to a single class, the individual
configtraits of the class will be tab-completable, as--Class.trait.
The available values for traitlets.Bool and traitlets.Enum will be completable, as well as any other custom traitlets.TraitType which defines a
argcompleter()method returning a list of available string completions.Custom completer methods can be assigned to a trait by tagging an
argcompletermetadata tag. Refer to argcomplete's documentation for examples of creating custom completer methods.
Detailed examples of these can be found in the docstring of examples/argcomplete_app.py.
Caveats with argcomplete handling
The support for argcomplete is still relatively new and may not work with all ways in which an Application is used. Some known caveats:
argcompleteis called when anyApplicationfirst constructs and uses a KVArgParseConfigLoader instance, which constructs aargparse.ArgumentParserinstance. We assume that this is usually first done in scripts when parsing the command-line arguments, but technically a script can first callApplication.initialize(["other", "args"])for some other reason.traitletsdoes not actually add"--Class.trait"options to theArgumentParser, but instead directly parses them fromargv. In order to complete these, a custom CompletionFinder is subclassed fromargcomplete.CompletionFinder, which dynamically inserts the"--Class.""and"--Class.trait"completions when it thinks suitable. However, this handling may be a bit fragile.Because
traitletsinitializes configs fromargvand not fromArgumentParser, it may be more difficult to write custom completers which dynamically provide completions based on the state of other parsed arguments.Subcommand handling is especially tricky.
argcompletelibraries' strategy is to call the python script with no arguments e.g.len(sys.argv) == 1, run untilargcompleteis called on anArgumentParserand determine what completions are available. On the other hand, thetraitletsubcommand-handling strategy is to checksys.argv[1]and see if it matches a subcommand, and if so then dynamically load the subcommand app and initialize it withsys.argv[1:]. To reconcile these two different approaches, some hacking was done to gettraitletsto recognize the current command-line as seen byargcomplete, and to getargcompleteto start parsing command-line arguments after subcommands have been evaluated.Currently, completing subcommands themselves is not yet supported.
Some applications like
Jupyterhave custom ways of constructing subcommands or parsingargvwhich complicates matters even further.
More details about these caveats can be found in the original pull request.
Design requirements
Here are the main requirements we wanted our configuration system to have:
Support for hierarchical configuration information.
Full integration with command line option parsers. Often, you want to read a configuration file, but then override some of the values with command line options. Our configuration system automates this process and allows each command line option to be linked to a particular attribute in the configuration hierarchy that it will override.
Configuration files that are themselves valid Python code. This accomplishes many things. First, it becomes possible to put logic in your configuration files that sets attributes based on your operating system, network setup, Python version, etc. Second, Python has a super simple syntax for accessing hierarchical data structures, namely regular attribute access (
Foo.Bar.Bam.name). Third, using Python makes it easy for users to import configuration attributes from one configuration file to another. Fourth, even though Python is dynamically typed, it does have types that can be checked at runtime. Thus, a1in a config file is the integer '1', while a'1'is a string.A fully automated method for getting the configuration information to the classes that need it at runtime. Writing code that walks a configuration hierarchy to extract a particular attribute is painful. When you have complex configuration information with hundreds of attributes, this makes you want to cry.
Type checking and validation that doesn't require the entire configuration hierarchy to be specified statically before runtime. Python is a very dynamic language and you don't always know everything that needs to be configured when a program starts.