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