Introduction
coma
makes it easy to build configurable command-based programs in Python.
Commands
Let’s dive in with a classic Hello World!
program:
import coma
if __name__ == "__main__":
coma.register("greet", lambda: print("Hello World!"))
coma.wake()
Now, let’s run this program:
$ python main.py greet
Hello World!
Note
The meat of working with coma
is the register()
function. It has two required parameters:
- name
the name of a command (
greet
in this example)- command
the command itself (an anonymous function in this example)
Note
The wake()
function should always follow the last call
to register()
. Calling wake()
tells coma
that all commands have been register()
ed.
Coma will then attempt to invoke whichever one was specified on the command
line. In this example, greet
was specified on the command line and so
the register()
ed command with that name was invoked.
In addition to anonymous functions, command
can be any Python function:
import coma
def cmd():
print("Hello World!")
if __name__ == "__main__":
coma.register("greet", cmd)
coma.wake()
or any Python class with a no-parameter run()
method:
import coma
class Cmd:
def run(self):
print("Hello World!")
if __name__ == "__main__":
coma.register("greet", Cmd)
coma.wake()
Multiple Commands
coma
is intended to manage multiple commands as part of building complex programs.
Let’s extend our previous example:
import coma
if __name__ == "__main__":
coma.register("greet", lambda: print("Hello World!"))
coma.register("leave", lambda: print("Goodbye!"))
coma.wake()
This register()
s two commands. By calling each in
turn, we induce different program behavior:
$ python main.py greet
Hello World!
$ python main.py leave
Goodbye!
Configurations
Commands alone are great, but omegaconf
integration is what makes coma
truly powerful. The simplest way to create an omegaconf
config object is
with a plain dictionary:
import coma
if __name__ == "__main__":
coma.register("greet", lambda cfg: print(cfg.message), {"message": "Hello World!"})
coma.wake()
Note
The command now takes one positional argument (cfg
in this example).
It will be bound to the config object if the command is invoked.
Note
If the command is a Python class, it is the constructor that should have
a positional config argument, not the run()
method:
import coma
class Cmd:
def __init__(self, cfg):
self.cfg = cfg
def run(self):
print(self.cfg.message)
if __name__ == "__main__":
coma.register("greet", Cmd, {"message": "Hello World!"})
coma.wake()
This separation between initialization and execution is done so that
stateful commands can be initialized based on config attributes, which is
typically more straightforward than delaying part of the initialization
until run()
is called.
The program essentially runs as before:
$ python main.py greet
Hello World!
The only difference is that, by default, coma
serializes the config object
to a YAML file in the current working directory:
$ ls
dict.yaml
main.py
By default, coma
uses the config object’s type
’s name (dict
in
this example) to create an identifier for the config, and this identifier is
then used derive a default file name. The default identifier can be overridden
by explicitly identifying the config object using a keyword argument:
import coma
if __name__ == "__main__":
coma.register("greet", lambda cfg: print(cfg.message),
greet={"message": "Hello World!"})
coma.wake()
Now the config will be serialized to greet.yaml
:
$ rm dict.yaml
$ python main.py greet
Hello World!
$ ls
greet.yaml
main.py
Config files can be used to hardcode attribute values that override the default
config attribute values. For example, changing greet.yaml
to:
message: hardcoded message
leads to the following program execution:
$ python main.py greet
hardcoded message
Note
See here for full details on configuration files.
Config attribute values can also be overridden on the command line using omegaconf
’s
dot-list notation:
$ python main.py greet message="New Message"
New Message
Note
See here for full details on command line overrides.
Note
Serialized configs override default configs and command line-based configs override
both serialized and default configs: default < serialized < command line
.
coma
supports any valid omegaconf
config object. In particular,
structured configs
are useful for enabling runtime validation:
from dataclasses import dataclass
import coma
@dataclass
class Config:
message: str = "Hello World!"
if __name__ == "__main__":
coma.register("greet", lambda cfg: print(cfg.message), Config)
coma.wake()
Note
Because Config
has type
name config
, it will be
serialized to config.yaml
.
Multiple Configurations
coma
enables commands to take an arbitrary number of independent configs:
from dataclasses import dataclass
import coma
@dataclass
class Greeting:
message: str = "Hello"
@dataclass
class Receiver:
entity: str = "World!"
if __name__ == "__main__":
coma.register("greet", lambda g, r: print(g.message, r.entity), Greeting, Receiver)
coma.wake()
Note
In this example, the command now takes two positional arguments. Each will be bound (in the given order) to the supplied config objects if the command is invoked.
$ python main.py greet
Hello World!
Multiple configs are often useful in practice to separate otherwise-large configs into smaller components, especially if some components are shared between commands:
from dataclasses import dataclass
import coma
@dataclass
class Greeting:
message: str = "Hello"
@dataclass
class Receiver:
entity: str = "World!"
if __name__ == "__main__":
coma.register("greet", lambda g, r: print(g.message, r.entity), Greeting, Receiver)
coma.register("leave", lambda r: print("Goodbye", r.entity), Receiver)
coma.wake()
Note
Configs need to be uniquely identified per-command, but not across commands,
so it is perfectly acceptable for both greet
and leave
to
share the Receiver
config. To disable this sharing (so that each
command has its own serialized copy of the config), use unique identifiers:
coma.register("greet", ..., Greeting, greet=Receiver)
coma.register("leave", ..., leave=Receiver)
We invoke both commands in turn as before:
$ python main.py greet
Hello World!
$ python main.py leave
Goodbye World!
Next Steps
🎉 You now have a solid foundation for writing Python programs with configurable commands! 🎉
For more advanced use cases, coma
offers many additional features, including:
Customizing the underlying
argparse
objects.Adding command line arguments and flags to your program.
Registering global configurations that are applied to every command.
Using hooks to tweak, replace, or extend
coma
’s default behavior.And more!
Check out the other tutorials to learn more.