msgspec-click
msgspec-click is a Python library that generates Click options from msgspec types, facilitating the creation of command-line interfaces with robust data validation and serialization. It is currently at version 0.2.1 and maintains an active development pace with regular updates.
Common errors
-
TypeError: 'list' object is not subscriptable
cause Attempting to use new-style type annotations (e.g., `list[str]`, `dict[str, int]`) on Python versions older than 3.9. While `from __future__ import annotations` delays evaluation, it doesn't change runtime support.fixUpgrade your Python interpreter to version 3.9 or newer. Alternatively, use old-style type annotations like `typing.List[str]` or `typing.Dict[str, int]` for compatibility with Python 3.8. -
msgspec.ValidationError: Expected `int`, got `str` - at `$.field_name`
cause The input provided via the command line does not conform to the expected type defined in your `msgspec.Struct`. `msgspec` performs strict validation by default.fixEnsure the command-line arguments match the types defined in your `msgspec.Struct`. For example, if a field is `int`, `--field 'abc'` will fail. Provide `--field 123` instead. Consider `strict=False` in `msgspec.convert` for laxer conversion if appropriate, but this is generally not recommended for robust CLI input. -
Error: Got unexpected extra argument (some-value)
cause This Click error usually means an argument was passed that doesn't correspond to any defined option or argument. This can happen if a `msgspec.Meta` annotation incorrectly configures a Click option, especially if `params` are missing or wrong for a field.fixReview the `msgspec.Meta(extra={'params': [...]})` definitions for the relevant `msgspec.Struct` fields. Ensure that the short (`-x`) and long (`--xyz`) parameters you expect are correctly listed in the `params` list. If `params` is not set, the field name becomes the `--field-name` option. -
Error: Missing option '--required-field'
cause A field in your `msgspec.Struct` is defined without a default value, making it a required field. `msgspec-click` then generates a Click option with `required=True`, but the user did not provide the option.fixProvide the missing option on the command line. If the field should be optional, define a default value in your `msgspec.Struct` (e.g., `field: str = ""` or `field: str | None = None`).
Warnings
- gotcha Type annotations used within `msgspec.Struct` (e.g., `list[str]`, `str | None`) must be compatible with your Python runtime version. Using modern syntax like `list[str]` (Python 3.9+) or `str | None` (Python 3.10+) on older Python versions will result in runtime errors, even with `from __future__ import annotations`.
- breaking msgspec versions (the underlying library) may introduce breaking changes to `msgspec.Struct` definitions (e.g., changes to `UNSET`/`NODEFAULT`, parameter ordering rules, or inheritance). These changes, while not directly in `msgspec-click`, can cause errors in your `msgspec.Struct` definitions which `msgspec-click` depends on.
- gotcha Customizing Click options requires understanding how `msgspec.Meta(extra={...})` maps to `click.Option` keyword arguments. Misconfigurations, especially with `params`, can lead to unexpected CLI behavior or unrecognized options.
- deprecated The `v0.1.0` release was the initial public release. While `dict` and `typing.TypedDict` support were added in `v0.2.0`, older versions do not provide this functionality, leading to unsupported type errors if used with these types.
Install
-
pip install msgspec-click
Imports
- generate_options
from msgspec_click import generate_options
- Struct
from msgspec import Struct
- Meta
from msgspec import Meta
Quickstart
from __future__ import annotations
from typing import Annotated
import click
from msgspec import Meta, Struct, convert
from msgspec_click import generate_options
class Connection(Struct):
user: Annotated[
str,
Meta(extra={'help': 'The user\'s name', 'params': ['-u', '--user']})
] = ""
password: Annotated[
str,
Meta(extra={
'help': 'The user\'s password',
'params': ['-p', '--pass'],
'prompt': True,
'hide_input': True,
'confirmation_prompt': True,
})
] = ""
headers: Annotated[list[str], Meta(extra={'params': ['-H']})] = []
timeout: float = 10.0
allow_insecure: bool = False
@click.command()
def command(**kwargs) -> None:
connection = convert(kwargs, Connection)
print(connection)
command.params.extend(generate_options(Connection))
if __name__ == "__main__":
# To run this, save as script.py and then execute:
# python script.py --user alice -H "Key: Value" --pass
# (the --pass will prompt for password)
command()