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.
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
Exec Middleware¶
Execution middlewares are registered with commands specifically.
Suspending Execution¶
Middlewares may suspend their execution, and resume it after the command has executed by yielding.
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:
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.
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()
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.