Skip to content

Callbacks

In arc, callbacks are a way to encapsulate shared functionality across multiple commands.

In short, callbacks are:

  • Reusable
  • Composable
  • Modular

Creating Callbacks

Callbacks are created using the arc.callback.create() function. This will wrap your provided function into a callback, which you then decorate a command with to register that callback to that command.

examples/callback_create.py
import arc
from arc import callback


@callback.create()
def cb(args, ctx):
    print("before execution")
    yield
    print("after execution")


@cb
@arc.command()
def command():
    print("command execution")


command()
$ python callback_create.py 
before execution
command execution
after execution

You can think of callbacks as wrapping the execution of a command. They can be used to perform any shared setup-code and any shared teardown code needed.

Tip

The examples used in this document will generally yield to show that callbacks wrap the execution of a command. However, they are not required to do this and can just be simple functions if they only need to perform setup before the command runs.

Callback Creation Shorthand

All Command objects have a callback() method, so they above can be modified to:

examples/callback_example.py
import arc


@arc.command()
def command():
    print("command execution")


@command.callback()
def callback(args: dict, ctx: arc.Context):
    print("before execution")
    yield
    print("after execution")


command()

$ python callback_example.py 
before execution
command execution
after execution
In essence, all this method does is create the callback, and register it to the Command being referred to.

Callback Inheritance

By default, When using arc.CLI(), callbacks are inherited by any subcommand of the command it is added to. For Example:

examples/callback_inherit.py
from arc import CLI

cli = CLI()


@cli.callback()
def global_callback(args, ctx):
    print("global callback -- start")
    yield
    print("global callback -- end")


@cli.command()
def c1():
    print("c1")


@c1.callback()
def c1_callback(args, ctx):
    print("c1 callback -- start")
    yield
    print("c1 callback -- end")


@c1.subcommand()
def sub():
    print("c1:sub")


@cli.command()
def c2():
    print("c2")


cli()
$ python callback_inherit.py c2
global callback -- start
c2
global callback -- end

$ python callback_inherit.py c1
global callback -- start
c1 callback -- start
c1
c1 callback -- end
global callback -- end

$ python callback_inherit.py c1:sub
global callback -- start
c1 callback -- start
c1:sub
c1 callback -- end
global callback -- end
  • c2 Executed the global callback because it inherited it from the CLI
  • c1 executed the global callback because it inherited it from the CLI and it executed it's own callback
  • c1:sub executed the global callback because it inherited it from the CLI and it executed it's parent's (c1) callback.

Callback inheritance can be stopped in two ways.

  • Set inherit = False when creating the callback. When a callback is non-inhertiable, it can still be added to any command by deocrating the command with it.
  • Use @callback.remove() or @arc.callback.remove(*callbacks) to remove a callback from a particular command and any of it's subcommands.

Let's modify the above example to remove all callbacks from c1:sub

examples/callback_inherit2.py
from arc import CLI

cli = CLI()


@cli.callback()
def global_callback(args, ctx):
    print("global callback -- start")
    yield
    print("global callback -- end")


@cli.command()
def c1():
    print("c1")


@c1.callback(inherit=False)
def c1_callback(args, ctx):
    print("c1 callback -- start")
    yield
    print("c1 callback -- end")


@global_callback.remove
@c1.subcommand()
def sub():
    print("c1:sub")


@cli.command()
def c2():
    print("c2")


cli()
$ python callback_inherit2.py c2
global callback -- start
c2
global callback -- end

$ python callback_inherit2.py c1
global callback -- start
c1
global callback -- end

$ python callback_inherit2.py c1:sub
c1:sub
  • c2 executed the global callback because it inherited it from the CLI
  • c1 executed the global callback because it inherited it from the CLI and it executed it's own callback
  • c1:sub executed neither callback because it removed the CLI callback with @global_callback.remove and c1_callback is non-inhertiable.

Error Handling

If you're going to be performing cleanup within a callback, it's generally advisable to wrap it in a finally block. This makes sure that the code is ran, regardless of whether or not the executed command succeeds.

You can also catch and handle exception within callbacks, but that is primarily the responsibilty of error handlers