User Classes
Cyclopts supports classically defined user classes, as well as classes defined by the following dataclass-like libraries:
Basic Example
As an example, let's consider using the builtin dataclass to make a CLI that manages a movie collection.
from cyclopts import App
from dataclasses import dataclass
app = App(name="movie-maintainer")
@dataclass
class Movie:
title: str
year: int
@app.command
def add(movie: Movie):
print(f"Adding movie: {movie}")
app()
$ movie-maintainer add --help
Usage: movie-maintainer add [ARGS] [OPTIONS]
╭─ Parameters ────────────────────────────────────────────────╮
│ * MOVIE.TITLE [required] │
│ --movie.title │
│ * MOVIE.YEAR --movie.year [required] │
╰─────────────────────────────────────────────────────────────╯
$ movie-maintainer add 'Mad Max: Fury Road' 2015
Adding movie: Movie(title='Mad Max: Fury Road', year=2015)
$ movie-maintainer add --movie.title 'Furiosa: A Mad Max Saga' --movie.year 2024
Adding movie: Movie(title='Furiosa: A Mad Max Saga', year=2024)
In most circumstances, Cyclopts will also parse a json-string for a dataclass-like parameter:
$ movie-maintainer add --movie='{"title": "Mad Max: Fury Road", "year": 2024}'
Adding movie: Movie(title='Mad Max: Fury Road', year=2024)
JSON Dict Parsing
JSON dict parsing will be performed when:
The parameter is specified as a keyword option; e.g.
--movie.The referenced parameter type has various sub-arguments (is dataclass-like).
The referenced parameter is not union'd with a
str.The first character is a
{.
This behavior can be configured via Parameter.json_dict.
from cyclopts import App
from dataclasses import dataclass
app = App(name="movie-manager")
@dataclass
class Movie:
title: str
year: int
rating: float = 8.0
@app.command
def add(movie: Movie):
print(f"Adding: {movie}")
app()
$ movie-manager add --movie '{"title": "Mad Max: Fury Road", "year": 2015, "rating": 8.1}'
Adding: Movie(title='Mad Max: Fury Road', year=2015, rating=8.1)
$ movie-manager add --movie '{"title": "Furiosa", "year": 2024}'
Adding: Movie(title='Furiosa', year=2024, rating=8.0)
Note that JSON parsing only works when using the keyword option format (--movie). The traditional positional argument format still works with individual fields:
$ movie-manager add --movie.title "Dune" --movie.year 2021 --movie.rating 8.5
Adding: Movie(title='Dune', year=2021, rating=8.5)
JSON List Parsing
Cyclopts also supports JSON parsing for lists of dataclasses. This allows you to pass multiple structured objects via JSON:
from cyclopts import App
from dataclasses import dataclass
app = App(name="movie-collection")
@dataclass
class Movie:
title: str
year: int
@app.command
def add_batch(movies: list[Movie]):
for movie in movies:
print(f"Adding: {movie}")
app()
You can provide the list in several ways:
JSON Array - Multiple objects in a single argument:
$ movie-collection add-batch --movies '[{"title": "Mad Max", "year": 2015}, {"title": "Furiosa", "year": 2024}]' Adding: Movie(title='Mad Max', year=2015) Adding: Movie(title='Furiosa', year=2024)
Individual JSON - Each object as a separate argument:
$ movie-collection add-batch --movies '{"title": "Mad Max", "year": 2015}' --movies '{"title": "Furiosa", "year": 2024}' Adding: Movie(title='Mad Max', year=2015) Adding: Movie(title='Furiosa', year=2024)
Mixed - Combining arrays and individual objects:
$ movie-collection add-batch --movies '{"title": "Mad Max", "year": 2015}' --movies '[{"title": "Furiosa", "year": 2024}, {"title": "Dune", "year": 2021}]' Adding: Movie(title='Mad Max', year=2015) Adding: Movie(title='Furiosa', year=2024) Adding: Movie(title='Dune', year=2021)
JSON list parsing is automatically enabled for list types containing dataclasses. The same rules apply as for dict parsing:
The element type cannot be union'd with
strJSON objects must start with
{or be arrays starting with[
This behavior can be configured via Parameter.json_list.
Namespace Flattening
It is likely that the actual movie class/object is not important to the CLI user, and the parameter names like --movie.title are unnecessarily verbose. We can remove movie from the name by giving the Movie type annotation the special name "*".
from cyclopts import App, Parameter
from dataclasses import dataclass
from typing import Annotated
app = App(name="movie-maintainer")
@dataclass
class Movie:
title: str
year: int
@app.command
def add(movie: Annotated[Movie, Parameter(name="*")]):
print(f"Adding movie: {movie}")
app()
$ movie-maintainer add --help
Usage: movie-maintainer add [ARGS] [OPTIONS]
╭─ Parameters ────────────────────────────────────────────────╮
│ * TITLE --title [required] │
│ * YEAR --year [required] │
╰─────────────────────────────────────────────────────────────╯
An alternative way of supplying the Parameter configuration is via a decorator.
This way can be cleaner and terser in many scenarios.
The Parameter configuration will also be inherited by subclasses.
from cyclopts import App, Parameter
from dataclasses import dataclass
app = App(name="movie-maintainer")
@Parameter(name="*")
@dataclass
class Movie:
title: str
year: int
@app.command
def add(movie: Movie):
print(f"Adding movie: {movie}")
app()
Config File
Having the user specify --user every single call is a bit cumbersome, especially if they're always going to provide the same value.
We can have Cyclopts fallback to a toml configuration file.
Consider the following toml data saved to config.toml:
# config.toml
user = "Guido"
We can update our app to fill in missing CLI parameters from this file:
from cyclopts import App, Parameter, config
from dataclasses import dataclass
from typing import Annotated
app = App(
name="movie-maintainer",
config=config.Toml("config.toml", use_commands_as_keys=False),
)
@Parameter(name="*")
@dataclass
class Config:
user: str
server: str = "media.sqlite"
@dataclass
class Movie:
title: str
year: int
@app.command
def add(movie: Movie, *, config: Config):
print(f"Config: {config}")
print(f"Adding movie: {movie}")
app()
$ movie-maintainer add 'Mad Max: Fury Road' 2015
Config: Config(user='Guido', server='media.sqlite')
Adding movie: Movie(title='Mad Max: Fury Road', year=2015)