Introduction

coma makes it easy to build configurable command-based programs in Python.

Commands

Let’s dive in with a classic Hello World! program:

main.py
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:

main.py
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:

main.py
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:

main.py
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:

greet.yaml
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:

main.py
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:

main.py
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:

main.py
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.