Skip to content

Middleware

arc is based on a middleware design pattern. In arc there are three kinds:

  • Initialization middlewares - Middleware that run a single time on startup. These are expected to perform the necessary setup to get an arc application up and running. The default middleware stack does, among other things, the following:
    • Performs development mode checks
    • Normalizes the differences between input sources
    • Validates that input
    • Parses the input from the command line
    • Prepares the context object for execution
  • Execution Middlewares - Middleware that run every-time a command is executed. Each command will have associated middlewares and when a command is chosen all of it's parents middlewares will be ran, then it's middlewares, then the callback. The default middleware stack does, among other things, the following:
    • Associates parsed values with command parameters
    • Retrieves values from other parameter sources (enviroment variables, user input, etc...)
    • Performs type conversion on parsed values
    • Executes validations on parsed values (ensures that they meet the requirements of the parameter)
  • Type Middlewares - Middlewares that operate on the type level. These middlewares are unique and will not be covered on this page. But they are used to add validations / transformations to a type.

Middleware Example

A middleware is a callable object that receives and instance of arc.Context, which serves as a dict-like object that all middleware share. Each middleware can then perform manipulations to this context object to get their desired effect.

In this example, we add a middleware that simple prints out some information about the command that is about to be executed.

examples/middleware/middleware.py
import arc


@arc.command
def hello(name: str = "world"):
    arc.print(f"Hello, {name}!")


@hello.use
def middleware(ctx: arc.Context):
    arc.print("Hello from middleware!")
    arc.print(f"Command name: {ctx.command.name}")
    arc.print(f"Command args: {ctx['arc.args']}")


hello()
$ python middleware/middleware.py Joseph
Hello from middleware!
Command name: middleware.py
Command args: {'name': 'Joseph'}
Hello, Joseph!

Registering Middlewares

Custom middleware can be added using the @object.use interface

Init Middleware

The add init middlewares to your application, you are required to construct the App object explicitly

@arc.command
def command():
   ...

app = arc.App(command)

@app.use
def middleware(ctx):
    ...

app()

Exec Middleware

Execution middlewares are registered with commands specifically.

@arc.command
def command():
   ...

@command.use
def middleware(ctx):
    ...

Suspending Execution

Middlewares may suspend their execution, and resume it after the command has executed by yielding.

def middleware(ctx: Context):
    # Perform setup logic
    res = yield
    # Perform teardown logic

The generators will be resumed in reverse order after command execution. The generator will be sent() the returned result of the command (or previous middleware). You may then choose to return something else which will be used as the result sent to the next middleware.

Replace Context

If you'd like to completely replace the context object, you may do so by returning (or yielding) a different object from your middleware.

def middleware(ctx: Context):
    return {"arc.args": {...}}

# OR

def middleware(ctx: Context):
    yield {"arc.args": {...}}

Halting Execution

Stopping the rest of the pipeline from running should be accomplished by raising an exception

def middleware(ctx: Context):
    arc.exit(1, "Something bad occured") # raises a SystemExit
    # OR
    raise arc.ExecutionError("Something bad occured") # If you want an exception that other middlewares can catch
    # OR
    raise CustomException(...)

Catching Exceptions

Middlewares are capable of catching exceptions that occur further down the pipeline by yielding inside of a try-except block:

def middleware(ctx: Context):
    try:
        yield
    except Exception:
        # Handle the exception, or raise again

If the handler can't handle a particular error, you can raise the exception again (or even raise a new exception). This will cause it to continue down the list of middlewares until it finds one that does handle the exception. If none are found, the exception will be handled by arc's default error-handling behavior.

Alternative Error Handler Syntax

Because error handling in middlewares is a common pattern, arc supports an alternate syntax for defining them.

examples/errors/error_handlers.py
import arc


@arc.command
def command():
    arc.print("We're going to throw an error")
    raise RuntimeError("Something has gone wrong!")


@command.handle(RuntimeError)
def handle_exception(ctx: arc.Context, exc):
    arc.print("handled!")


command()

$ python errors/error_handlers.py 
We're going to throw an error
handled!
See Error Handling for more information

Modifying the Middleware Stack

By default use() adds the provided middleware to the end of the stack. Instead, you can provide various arguments to place it in a specific location

command.use(mid1, pos=4) # at index 4
command.use(mid2, after=mid1) # Inserted directly after mid1
command.use(mid3, before=mid1) # Inserted directly before mid1
command.use(mid4, replace=mid2) # Replaces mid2 with mid4

This can be used to override the default behavior of arc. For example, this could be used to replace arc's parsing middleware with your own.

import arc

@arc.command
def command():
    ...


app = arc.App()

@app.use(replace=arc.InitMiddleware.Parser)
def parsing(ctx):
    # Do custom parsing

app()

Be careful when replacing middlewares, as it may break the functionality of arc. Most middlewares expect certain data to be in the Context object and will fail if it is not present. For example, if you replace the arc.InitMiddleware.Parser middleware, you will need to ensure that the arc.parse.result key is present in the context object and contains the parsed arguments.

You can review the reference for both the init middlewares and execution middlewares to see what data they expect to be present in and what data they add to the context object.