Command Line Arguments

Program-Level Arguments

Using program-level command line arguments is as an easy way to inject additional behavior into a program. This example is similar to the one seen here. The main difference is using global hooks instead of local hooks to avoid repeating the hook registration for the new leave command:

main.py
import coma

parser_hook = coma.hooks.parser_hook.factory("--dry-run", action="store_true")

@coma.hooks.hook
def pre_run_hook(known_args):
    if known_args.dry_run:
        print("Early exit!")
        quit()

if __name__ == "__main__":
    coma.initiate(parser_hook=parser_hook, pre_run_hook=pre_run_hook)
    coma.register("greet", lambda: print("Hello World!"))
    coma.register("leave", lambda: print("Goodbye World!"))
    coma.wake()

In this example, the parser_hook adds a new --dry-run flag to the command line. This flag is used by the pre_run_hook to exit the program early (before a command is actually executed) if the flag is given on the command line. Because these are global hooks, this behavior is present regardless of the command that is invoked:

$ python main.py greet
Hello World!
$ python main.py leave
Goodbye World!
$ python main.py greet --dry-run
Early exit!
$ python main.py leave --dry-run
Early exit!

Command-Level Arguments

Using command-level command line arguments is as an easy way to give a command additional data or modifiers that, for whatever reason, don’t belong in a dedicated config object:

main.py
import coma

parser_hook = coma.hooks.sequence(
    coma.hooks.parser_hook.factory("a", type=int),
    coma.hooks.parser_hook.factory("-b", default=coma.SENTINEL),
)

@coma.hooks.hook
def init_hook(known_args, command):
    if known_args.b is coma.SENTINEL:
        return command(known_args.a)
    else:
        return command(known_args.a, known_args.b)

if __name__ == "__main__":
    with coma.forget(init_hook=True):
        coma.register("numbers", lambda a, b=456: print(a, b),
                      parser_hook=parser_hook, init_hook=init_hook)
    coma.register("greet", lambda: print("Hello World!"))
    coma.wake()

Here, greet acts in accordance with coma’s default behaviour, whereas numbers is defined quite differently. First, we define a sequence() for the parser_hook made up of factory() calls, each of which simply passes its arguments to the underlying parser object. Next, we define a custom init_hook that is aware of how to instantiate this non-standard command object. Finally, we forget() the default init_hook, which doesn’t know how to handle non-standard commands.

With these definitions, we can invoke the program’s commands as follows:

$ python main.py greet
Hello World!
$ python main.py numbers 123
123 456
$ python main.py numbers 123 -b 321
123 321

Using coma.SENTINEL

In the previous example, we used coma’s convenience sentinel object, coma.SENTINEL. Another way to implement the same functionality would be:

main.py
import coma

parser_hook = coma.hooks.sequence(
    coma.hooks.parser_hook.factory("a", type=int),
    coma.hooks.parser_hook.factory("-b", default=456),
)

@coma.hooks.hook
def init_hook(known_args, command):
    return command(known_args.a, known_args.b)

if __name__ == "__main__":
    with coma.forget(init_hook=True):
        coma.register("numbers", lambda a, b=456: print(a, b),
                      parser_hook=parser_hook, init_hook=init_hook)
    coma.register("greet", lambda: print("Hello World!"))
    coma.wake()

In terms of final program behavior, these two versions of the program are essentially identical, yet the version without the sentinel is shorter. The tradeoff is that the sentinel allows the default value of b to be defined only once, rather than twice, which can be less error-prone.

Note

It would also be possible to define the default value of b only once (in the parser_hook):

coma.hooks.parser_hook.factory("-b", default=456)
...
coma.register(..., lambda a, b: print(a, b), ...)

The leads to another tradeoff: The full command definition is now spread out in the code, which can obscure the fact that b has a default value.

On-the-Fly Hook Redefinition

Command line arguments can also be used to redefine hooks on the fly. In this example, we have two configs, both of which define the same x attribute. We then define a new -e flag, which is used to toggle the exclusive parameter of override_factory(). In short, the presence of this flag prevents any command line override involving x from overriding more than one config attribute:

main.py
from dataclasses import dataclass

import coma

@dataclass
class Config1:
    x: int

@dataclass
class Config2:
    x: int

excl = coma.hooks.parser_hook.factory("-e", dest="excl", action="store_true")

@coma.hooks.hook
def post_config_hook(known_args, unknown_args, configs):
    override = coma.config.cli.override_factory(exclusive=known_args.excl)
    multi_cli = coma.hooks.post_config_hook.multi_cli_override_factory(override)
    return multi_cli(unknown_args=unknown_args, configs=configs)

if __name__ == "__main__":
    coma.initiate(Config1, Config2, post_config_hook=post_config_hook)
    coma.register("multiply", lambda c1, c2: print(c1.x * c2.x), parser_hook=excl)
    coma.wake()

Without the -e flag, we can use x on the command line to override both configs at once:

$ python main.py multiply x=3
9

This lets multiply is essentially act as square. We can prevent this by setting the -e flag:

$ python main.py multiply x=3
...
ValueError: Non-exclusive override: override: x=3 ; matched configs (possibly others too): ['config1', 'config2']

Note

See here for additional details on this example.