Skip to content

Type Middleware

arc is based on a middleware design pattern. This also applies to arc's type system to give you further flexibility in defining your APIs.

What is a Type Middleware?

A Type Middleware is simply a callable object that recieves a value, and then returns a value of the same type. It can be used to modify that value, validate some property of that value, or simply analyze the value and return it.

Middlewares fall into a couple of rough categories, and so arc uses specific terms to refer to them.

  • Validator: A middleware that checks the value against some condition. It raises an exception if the condition is not met, and otherwise it returns the original value
  • Transformer: A middleware that modifies the value and returns it.
  • Observer: A middleware that just analyzes the type, but won't every raise an error / manipulate it

These terms will be used throughout the rest of this page.

Creating a Type Middleware

Because middleware are just callables, they are extremely simple to define. For example, we could create a transform middleware that rounds floats to 2 digits.

def round2(val: float):
    return round(val, 2)
Tip

arc already ships with a Round() transformer, so you wouldn't actually need to implement this one yourself.

Using a Type Middleware

Type Middleware are attached to a type using an Annotated Type

examples/round.py
from typing import Annotated
import arc

# Could use arc.types.middleware.Round() instead
# of implementing a custom middleware.
def round2(val: float):
    return round(val, 2)


@arc.command
def command(val: Annotated[float, round2]):
    arc.print(val)


command()
$ python round.py 1.123456789
1.12
Tip

Middlewares are actually why most custom arc types require you to use arc.convert() to convert them properly. A good majority of them are actually just an underlying type + some middleware to provide a validation / conversion step. For example arc.types.PositiveInt is actually just defined as

PositiveInt = Annotated[int, GreaterThan(0)]

Builtin Middlewares

arc ships with a set of general-use builtin middlewares

Examples

Middleware that ensure that a passed in version is greater than the current version

examples/new_version.py
import typing as t
import arc
from arc import types


def greater_than_previous(value: types.SemVer, ctx: arc.Context):
    current_version: types.SemVer | None = ctx.state.get("curr_version")
    if not current_version:
        return value

    if current_version >= value:
        raise arc.ValidationError(
            f"New version must be greater than current version ({current_version})"
        )

    return value


NewVersion = t.Annotated[types.SemVer, greater_than_previous]


@arc.command
def command(version: NewVersion):
    arc.print(version)


command(state={"curr_version": types.SemVer.parse("1.0.0")})
$ python new_version.py 0.1.1
USAGE
    new_version.py [-h] version

invalid value for version: New version must be greater than current version (1.0.0)
$ python new_version.py 1.2.1
1.2.1