Intuitive, easy CLIs based on type hints.
—
Comprehensive type conversion system with pre-built validated types and custom validation support for robust CLI input handling.
Central function for converting command-line tokens to Python types.
def convert(type_: type, tokens: list[Token]) -> Any:
"""
Convert command-line tokens to target Python type.
Parameters
----------
type_
Target Python type for conversion
tokens
List of Token objects containing user input
Returns
-------
Any
Converted value of the target type
Raises
------
CoercionError
If conversion fails or tokens are invalid for the type
"""Parse environment variables into appropriate types.
def env_var_split(value: str, type_: type) -> list[str]:
"""
Split environment variable value based on target type.
Parameters
----------
value
Environment variable string value
type_
Target type that determines splitting behavior
Returns
-------
list[str]
List of string tokens for conversion
"""Validated Path types with existence and type checking.
# Basic path types
ExistingPath = Annotated[Path, ...]
"""Path that must exist."""
NonExistentPath = Annotated[Path, ...]
"""Path that must not exist."""
ExistingFile = Annotated[Path, ...]
"""File path that must exist."""
NonExistentFile = Annotated[Path, ...]
"""File path that must not exist."""
ExistingDirectory = Annotated[Path, ...]
"""Directory path that must exist."""
NonExistentDirectory = Annotated[Path, ...]
"""Directory path that must not exist."""
File = Annotated[Path, ...]
"""Generic file path type."""
Directory = Annotated[Path, ...]
"""Generic directory path type."""
# Resolved path types
ResolvedPath = Annotated[Path, ...]
"""Path resolved to absolute form."""
ResolvedExistingPath = Annotated[Path, ...]
"""Existing path resolved to absolute form."""
ResolvedExistingFile = Annotated[Path, ...]
"""Existing file path resolved to absolute form."""
ResolvedExistingDirectory = Annotated[Path, ...]
"""Existing directory path resolved to absolute form."""
ResolvedDirectory = Annotated[Path, ...]
"""Directory path resolved to absolute form."""
ResolvedFile = Annotated[Path, ...]
"""File path resolved to absolute form."""
# Extension-specific path types
BinPath = Annotated[Path, ...]
"""Binary file path."""
CsvPath = Annotated[Path, ...]
"""CSV file path."""
ImagePath = Annotated[Path, ...]
"""Image file path."""
JsonPath = Annotated[Path, ...]
"""JSON file path."""
Mp4Path = Annotated[Path, ...]
"""MP4 video file path."""
TomlPath = Annotated[Path, ...]
"""TOML configuration file path."""
TxtPath = Annotated[Path, ...]
"""Text file path."""
YamlPath = Annotated[Path, ...]
"""YAML configuration file path."""
# Extension-specific path types with existence constraints
ExistingBinPath = Annotated[Path, ...]
"""Binary file path that must exist."""
NonExistentBinPath = Annotated[Path, ...]
"""Binary file path that must not exist."""
ExistingCsvPath = Annotated[Path, ...]
"""CSV file path that must exist."""
NonExistentCsvPath = Annotated[Path, ...]
"""CSV file path that must not exist."""
ExistingImagePath = Annotated[Path, ...]
"""Image file path that must exist."""
NonExistentImagePath = Annotated[Path, ...]
"""Image file path that must not exist."""
ExistingJsonPath = Annotated[Path, ...]
"""JSON file path that must exist."""
NonExistentJsonPath = Annotated[Path, ...]
"""JSON file path that must not exist."""
ExistingMp4Path = Annotated[Path, ...]
"""MP4 file path that must exist."""
NonExistentMp4Path = Annotated[Path, ...]
"""MP4 file path that must not exist."""
ExistingTomlPath = Annotated[Path, ...]
"""TOML file path that must exist."""
NonExistentTomlPath = Annotated[Path, ...]
"""TOML file path that must not exist."""
ExistingTxtPath = Annotated[Path, ...]
"""Text file path that must exist."""
NonExistentTxtPath = Annotated[Path, ...]
"""Text file path that must not exist."""
ExistingYamlPath = Annotated[Path, ...]
"""YAML file path that must exist."""
NonExistentYamlPath = Annotated[Path, ...]
"""YAML file path that must not exist."""Validated numeric types with range constraints.
# Float range types
PositiveFloat = Annotated[float, ...]
"""Float greater than 0."""
NonNegativeFloat = Annotated[float, ...]
"""Float greater than or equal to 0."""
NegativeFloat = Annotated[float, ...]
"""Float less than 0."""
NonPositiveFloat = Annotated[float, ...]
"""Float less than or equal to 0."""
# Integer range types
PositiveInt = Annotated[int, ...]
"""Integer greater than 0."""
NonNegativeInt = Annotated[int, ...]
"""Integer greater than or equal to 0."""
NegativeInt = Annotated[int, ...]
"""Integer less than 0."""
NonPositiveInt = Annotated[int, ...]
"""Integer less than or equal to 0."""
# Fixed-width integer types
UInt8 = Annotated[int, ...]
"""8-bit unsigned integer (0-255)."""
Int8 = Annotated[int, ...]
"""8-bit signed integer (-128 to 127)."""
UInt16 = Annotated[int, ...]
"""16-bit unsigned integer (0-65535)."""
Int16 = Annotated[int, ...]
"""16-bit signed integer (-32768 to 32767)."""
UInt32 = Annotated[int, ...]
"""32-bit unsigned integer."""
Int32 = Annotated[int, ...]
"""32-bit signed integer."""
UInt64 = Annotated[int, ...]
"""64-bit unsigned integer."""
Int64 = Annotated[int, ...]
"""64-bit signed integer."""
# Hexadecimal integer types
HexUInt = Annotated[int, ...]
"""Unsigned integer from hexadecimal string."""
HexUInt8 = Annotated[int, ...]
"""8-bit unsigned integer from hexadecimal string."""
HexUInt16 = Annotated[int, ...]
"""16-bit unsigned integer from hexadecimal string."""
HexUInt32 = Annotated[int, ...]
"""32-bit unsigned integer from hexadecimal string."""
HexUInt64 = Annotated[int, ...]
"""64-bit unsigned integer from hexadecimal string."""Additional pre-built types for common use cases.
Json = Annotated[Any, ...]
"""JSON string parsed to Python object."""
Email = Annotated[str, ...]
"""Email address with validation."""
Port = Annotated[int, ...]
"""Network port number (1-65535)."""
URL = Annotated[str, ...]
"""URL with validation."""Built-in validators for custom validation logic.
class Number:
def __init__(
self,
min: float | None = None,
max: float | None = None,
min_inclusive: bool = True,
max_inclusive: bool = True
):
"""
Numeric range validator.
Parameters
----------
min
Minimum allowed value
max
Maximum allowed value
min_inclusive
Whether minimum is inclusive
max_inclusive
Whether maximum is inclusive
"""
def __call__(self, value: float) -> float:
"""Validate numeric value against range constraints."""
class Path:
def __init__(
self,
exists: bool | None = None,
file_okay: bool = True,
dir_okay: bool = True,
resolve: bool = False
):
"""
Path validator with existence and type checking.
Parameters
----------
exists
Whether path must exist (True), not exist (False), or either (None)
file_okay
Whether files are allowed
dir_okay
Whether directories are allowed
resolve
Whether to resolve path to absolute form
"""
def __call__(self, value: Path) -> Path:
"""Validate path against constraints."""
class LimitedChoice:
def __init__(self, *choices: Any, case_sensitive: bool = True):
"""
Limit value to specific choices.
Parameters
----------
choices
Valid choice values
case_sensitive
Whether string comparison is case sensitive
"""
def __call__(self, value: Any) -> Any:
"""Validate value is in allowed choices."""
class MutuallyExclusive:
def __init__(self, *groups: str):
"""
Ensure mutual exclusivity between parameter groups.
Parameters
----------
groups
Parameter group names that are mutually exclusive
"""
def __call__(self, namespace: dict) -> dict:
"""Validate mutual exclusivity constraints."""Standalone validator functions for common patterns.
def mutually_exclusive(*groups: str) -> Callable:
"""
Create validator for mutually exclusive parameter groups.
Parameters
----------
groups
Parameter group names that are mutually exclusive
Returns
-------
Callable
Validator function
"""
def all_or_none(*parameters: str) -> Callable:
"""
Create validator requiring all or none of the specified parameters.
Parameters
----------
parameters
Parameter names with all-or-none constraint
Returns
-------
Callable
Validator function
"""from cyclopts import App, Parameter
from cyclopts.validators import Number
from typing import Annotated
# Custom type with range validation
Percentage = Annotated[float, Number(min=0.0, max=100.0)]
app = App()
@app.command
def set_threshold(value: Percentage):
"""Set threshold as a percentage."""
print(f"Threshold set to {value}%")from cyclopts import App
from cyclopts.types import ExistingFile, PositiveInt, Email
app = App()
@app.command
def process_user_data(
input_file: ExistingFile,
max_records: PositiveInt,
notify_email: Email
):
"""Process user data file."""
print(f"Processing {input_file} (max {max_records} records)")
print(f"Will notify {notify_email}")from cyclopts import App, Parameter
from datetime import datetime
def parse_date(value: str) -> datetime:
"""Custom date converter."""
return datetime.strptime(value, "%Y-%m-%d")
app = App()
@app.command
def schedule_task(
name: str,
due_date: datetime = Parameter(converter=parse_date, help="Date in YYYY-MM-DD format")
):
"""Schedule a task with due date."""
print(f"Task '{name}' scheduled for {due_date.strftime('%Y-%m-%d')}")from cyclopts import App, Parameter
from cyclopts.validators import Number, LimitedChoice
app = App()
@app.command
def configure_server(
port: int = Parameter(
validator=[
Number(min=1024, max=65535),
LimitedChoice(8080, 8443, 9000, 9443)
],
help="Server port (must be 1024-65535 and one of: 8080, 8443, 9000, 9443)"
)
):
"""Configure server with validated port."""
print(f"Server configured on port {port}")Install with Tessl CLI
npx tessl i tessl/pypi-cyclopts