# User Classes

Cyclopts supports classically defined user classes, as well as classes defined by the following dataclass-like libraries:

- attrs

- dataclass

- pydantic

- NamedTuple

- TypedDict

## 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:

1. The parameter is specified as a keyword option; e.g. `--movie`.

2. The referenced parameter type has various sub-arguments (is dataclass-like).

3. The referenced parameter is not union'd with a `str`.

4. 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:

1. 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)

2. 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)

3. 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 `str`

- JSON 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()

## Sharing Parameters

A flattened dataclass provides a natural way of easily sharing a set of parameters between commands.

    from cyclopts import App, Parameter
    from dataclasses import dataclass

    app = App(name="movie-maintainer")

    @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.command
    def remove(movie: Movie, *, config: Config):
       print(f"Config: {config}")
       print(f"Removing movie: {movie}")

    app()

    $ movie-maintainer remove --help
    Usage: movie-maintainer remove [ARGS] [OPTIONS]

    ╭─ Parameters ────────────────────────────────────────────────╮
    │ *  MOVIE.TITLE              [required]                      │
    │      --movie.title                                          │
    │ *  MOVIE.YEAR --movie.year  [required]                      │
    │ *  --user                   [required]                      │
    │    --server                 [default: media.sqlite]         │
    ╰─────────────────────────────────────────────────────────────╯

    $ movie-maintainer remove 'Mad Max: Fury Road' 2015 --user Guido
    Config: Config(user='Guido', server='media.sqlite')
    Removing movie: Movie(title='Mad Max: Fury Road', year=2015)

## 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)