Validation

Typer has builtin argument validation for certain type annotations.

typer_app = typer.Typer()


@typer_app.command()
def foo(age: Annotated[int, typer.Argument(min=0)]):
    pass

This works for a select few builtins, but the Typer solution doesn't abstract out validation properly. Why does the generic typer.Argument have fields that only have meaning if the annotated type is a number? The typer.Argument signature has a ridiculous number of fields that only apply for certain types.

def Argument(
    # Parameter
    default: Optional[Any] = ...,
    *,
    callback: Optional[Callable[..., Any]] = None,
    metavar: Optional[str] = None,
    expose_value: bool = True,
    is_eager: bool = False,
    envvar: Optional[Union[str, List[str]]] = None,
    shell_complete: Optional[
        Callable[
            [click.Context, click.Parameter, str],
            Union[List["click.shell_completion.CompletionItem"], List[str]],
        ]
    ] = None,
    autocompletion: Optional[Callable[..., Any]] = None,
    # Custom type
    parser: Optional[Callable[[str], Any]] = None,
    # TyperArgument
    show_default: Union[bool, str] = True,
    show_choices: bool = True,
    show_envvar: bool = True,
    help: Optional[str] = None,
    hidden: bool = False,
    # Choice
    case_sensitive: bool = True,
    # Numbers
    min: Optional[Union[int, float]] = None,
    max: Optional[Union[int, float]] = None,
    clamp: bool = False,
    # DateTime
    formats: Optional[List[str]] = None,
    # File
    mode: Optional[str] = None,
    encoding: Optional[str] = None,
    errors: Optional[str] = "strict",
    lazy: Optional[bool] = None,
    atomic: bool = False,
    # Path
    exists: bool = False,
    file_okay: bool = True,
    dir_okay: bool = True,
    writable: bool = False,
    readable: bool = True,
    resolve_path: bool = False,
    allow_dash: bool = False,
    path_type: Union[None, Type[str], Type[bytes]] = None,
    # Rich settings
    rich_help_panel: Union[str, None] = None,
) -> Any:
    ...

Cyclopts has an explicit validator field that accepts a function:

cyclopts_app = cyclopts.App()


def age_validator(type_, value: int):
    if value < 0:
        raise ValueError


@cyclopts_app.command()
def foo(age: Annotated[int, Parameter(validator=age_validator)]):
    pass

This solution is similar to how other libraries, like Attrs or Pydantic, perform validation.

Cyclopts has builtin validators for common use-cases.

# Typer
typer.Argument(file_okay=True, exists=True)

# Cyclopts
cyclopts.Parameter(validator=cyclopts.validators.Path(file_okay=True, exists=True))