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 (
greetin 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()
For simple use cases, commands can also be register()ed at
declaration time using the @command (link: command())
convenience decorator:
from coma import command
import coma
@command("greet")
def cmd():
print("Hello World!")
if __name__ == "__main__":
# Removed call to coma.register()
coma.wake()
Note
Most tutorials in this documentation stick to using explicit calls to
register() for simplicity. See here
for full details on the @command convenience decorator.
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
argparseobjects.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.