.. Comment: this file is automatically generated by `update_example_docs.py`. It should not be modified manually. .. _example-category-custom_constructors: Custom constructors =================== :func:`tyro.cli` aims for comprehensive support of standard Python type constructs. It can still, however, be useful to extend the set of suported types. We provide two complementary approaches for doing so: - :mod:`tyro.conf` provides a simple API for specifying custom constructor functions. - :mod:`tyro.constructors` provides a more flexible API for defining behavior for different types. There are two categories of types: *primitive* types are instantiated from a single commandline argument, while *struct* types are broken down into multiple arguments. .. warning:: Custom constructors are useful, but can be verbose and require care. We recommend using them sparingly. .. _example-01_simple_constructors: Simple Constructors ------------------- For simple custom constructors, we can pass a constructor function into :func:`tyro.conf.arg` or :func:`tyro.conf.subcommand`. Arguments for will be generated by parsing the signature of the constructor function. In this example, we use this pattern to define custom behavior for instantiating a NumPy array. .. code-block:: python :linenos: # 01_simple_constructors.py from typing import Literal import numpy as np from typing_extensions import Annotated import tyro def construct_array( values: tuple[float, ...], dtype: Literal["float32", "float64"] = "float64" ) -> np.ndarray: """A custom constructor for 1D NumPy arrays.""" return np.array( values, dtype={"float32": np.float32, "float64": np.float64}[dtype], ) def main( # We can specify a custom constructor for an argument in `tyro.conf.arg()`. array: Annotated[np.ndarray, tyro.conf.arg(constructor=construct_array)], ) -> None: print(f"{array=}") if __name__ == "__main__": tyro.cli(main) .. raw:: html
$ python ./01_simple_constructors.py --help usage: 01_simple_constructors.py [-h] --array.values [FLOAT [FLOAT ...]] [--array.dtype {float32,float64}] ╭─ options ─────────────────────────────────────────╮ │ -h, --help show this help message and exit │ ╰───────────────────────────────────────────────────╯ ╭─ array options ───────────────────────────────────╮ │ A custom constructor for 1D NumPy arrays. │ │ ───────────────────────────────────────── │ │ --array.values [FLOAT [FLOAT ...]] │ │ (required) │ │ --array.dtype {float32,float64} │ │ (default: float64) │ ╰───────────────────────────────────────────────────╯.. raw:: html
$ python ./01_simple_constructors.py --array.values 1 2 3
array=array([1., 2., 3.])
.. raw:: html
$ python ./01_simple_constructors.py --array.values 1 2 3 4 5 --array.dtype float32
array=array([1., 2., 3., 4., 5.], dtype=float32)
.. _example-02_primitive_annotation:
Custom Primitive
----------------
In this example, we use :mod:`tyro.constructors` to attach a primitive
constructor via a runtime annotation.
.. code-block:: python
:linenos:
# 02_primitive_annotation.py
import json
from typing_extensions import Annotated
import tyro
# A dictionary type, but `tyro` will expect a JSON string from the CLI.
JsonDict = Annotated[
dict,
tyro.constructors.PrimitiveConstructorSpec(
# Number of arguments to consume.
nargs=1,
# Argument name in usage messages.
metavar="JSON",
# Convert a list of strings to an instance. The length of the list
# should match `nargs`.
instance_from_str=lambda args: json.loads(args[0]),
# Check if an instance is of the expected type. This is only used for
# helptext formatting in the presence of union types.
is_instance=lambda instance: isinstance(instance, dict),
# Convert an instance to a list of strings. This is used for handling
# default values that are set in Python. The length of the list should
# match `nargs`.
str_from_instance=lambda instance: [json.dumps(instance)],
),
]
def main(
dict1: JsonDict,
dict2: JsonDict = {"default": None},
) -> None:
print(f"{dict1=}")
print(f"{dict2=}")
if __name__ == "__main__":
tyro.cli(main)
.. raw:: html
$ python ./02_primitive_annotation.py --help usage: 02_primitive_annotation.py [-h] --dict1 JSON [--dict2 JSON] ╭─ options ───────────────────────────────────────────╮ │ -h, --help show this help message and exit │ │ --dict1 JSON (required) │ │ --dict2 JSON (default: '{"default": null}') │ ╰─────────────────────────────────────────────────────╯.. raw:: html
$ python ./02_primitive_annotation.py --dict1 '{"hello": "world"}'
dict1={'hello': 'world'}
dict2={'default': None}
.. raw:: html
$ python ./02_primitive_annotation.py --dict1 '{"hello": "world"}' --dict2 '{"hello": "world"}'
dict1={'hello': 'world'}
dict2={'hello': 'world'}
.. _example-03_primitive_registry:
Custom Primitive (Registry)
---------------------------
In this example, we use a :class:`tyro.constructors.ConstructorRegistry` to
define a rule that applies to all types that match ``dict[str, Any]``.
.. code-block:: python
:linenos:
# 03_primitive_registry.py
import json
from typing import Any
import tyro
# Create a custom registry, which stores constructor rules.
custom_registry = tyro.constructors.ConstructorRegistry()
# Define a rule that applies to all types that match `dict[str, Any]`.
@custom_registry.primitive_rule
def _(
type_info: tyro.constructors.PrimitiveTypeInfo,
) -> tyro.constructors.PrimitiveConstructorSpec | None:
# We return `None` if the rule does not apply.
if type_info.type != dict[str, Any]:
return None
# If the rule applies, we return the constructor spec.
return tyro.constructors.PrimitiveConstructorSpec(
nargs=1,
metavar="JSON",
instance_from_str=lambda args: json.loads(args[0]),
is_instance=lambda instance: isinstance(instance, dict),
str_from_instance=lambda instance: [json.dumps(instance)],
)
def main(
dict1: dict[str, Any],
dict2: dict[str, Any] = {"default": None},
) -> None:
"""A function with two arguments, which can be populated from the CLI via JSON."""
print(f"{dict1=}")
print(f"{dict2=}")
if __name__ == "__main__":
# To activate a custom registry, we should use it as a context manager.
with custom_registry:
tyro.cli(main)
.. raw:: html
$ python ./03_primitive_registry.py --help usage: 03_primitive_registry.py [-h] --dict1 JSON [--dict2 JSON] A function with two arguments, which can be populated from the CLI via JSON. ╭─ options ───────────────────────────────────────────╮ │ -h, --help show this help message and exit │ │ --dict1 JSON (required) │ │ --dict2 JSON (default: '{"default": null}') │ ╰─────────────────────────────────────────────────────╯.. raw:: html
$ python ./03_primitive_registry.py --dict1 '{"hello": "world"}'
dict1={'hello': 'world'}
dict2={'default': None}
.. raw:: html
$ python ./03_primitive_registry.py --dict1 '{"hello": "world"}' --dict2 '{"hello": "world"}'
dict1={'hello': 'world'}
dict2={'hello': 'world'}
.. _example-04_struct_registry:
Custom Structs (Registry)
-------------------------
In this example, we use a :class:`tyro.constructors.ConstructorRegistry` to
add support for a custom type.
.. warning::
This will be complicated!
.. code-block:: python
:linenos:
# 04_struct_registry.py
import tyro
# A custom type that we'll add support for to tyro.
class Bounds:
def __init__(self, lower: int, upper: int):
self.bounds = (lower, upper)
def __repr__(self) -> str:
return f"(lower={self.bounds[0]}, upper={self.bounds[1]})"
# Create a custom registry, which stores constructor rules.
custom_registry = tyro.constructors.ConstructorRegistry()
# Define a rule that applies to all types that match `Bounds`.
@custom_registry.struct_rule
def _(
type_info: tyro.constructors.StructTypeInfo,
) -> tyro.constructors.StructConstructorSpec | None:
# We return `None` if the rule does not apply.
if type_info.type != Bounds:
return None
# We can extract the default value of the field from `type_info`.
if isinstance(type_info.default, Bounds):
# If the default value is a `Bounds` instance, we don't need to generate a constructor.
default = (type_info.default.bounds[0], type_info.default.bounds[1])
is_default_overridden = True
else:
# Otherwise, the default value is missing. We'll mark the child defaults as missing as well.
assert type_info.default in (
tyro.constructors.MISSING,
tyro.constructors.MISSING_NONPROP,
)
default = (tyro.MISSING, tyro.MISSING)
is_default_overridden = False
# If the rule applies, we return the constructor spec.
return tyro.constructors.StructConstructorSpec(
# The instantiate function will be called with the fields as keyword arguments.
instantiate=Bounds,
fields=(
tyro.constructors.StructFieldSpec(
name="lower",
type=int,
default=default[0],
is_default_overridden=is_default_overridden,
helptext="Lower bound." "",
),
tyro.constructors.StructFieldSpec(
name="upper",
type=int,
default=default[1],
is_default_overridden=is_default_overridden,
helptext="Upper bound." "",
),
),
)
def main(
bounds: Bounds,
bounds_with_default: Bounds = Bounds(0, 100),
) -> None:
"""A function with two `Bounds` instances as input."""
print(f"{bounds=}")
print(f"{bounds_with_default=}")
if __name__ == "__main__":
# To activate a custom registry, we should use it as a context manager.
with custom_registry:
tyro.cli(main)
.. raw:: html
$ python ./04_struct_registry.py --help usage: 04_struct_registry.py [-h] [OPTIONS] A function with two `Bounds` instances as input. ╭─ options ───────────────────────────────────────────────╮ │ -h, --help show this help message and exit │ ╰─────────────────────────────────────────────────────────╯ ╭─ bounds options ────────────────────────────────────────╮ │ --bounds.lower INT Lower bound. (required) │ │ --bounds.upper INT Upper bound. (required) │ ╰─────────────────────────────────────────────────────────╯ ╭─ bounds-with-default options ───────────────────────────╮ │ --bounds-with-default.lower INT │ │ Lower bound. (default: 0) │ │ --bounds-with-default.upper INT │ │ Upper bound. (default: 100) │ ╰─────────────────────────────────────────────────────────╯.. raw:: html
$ python ./04_struct_registry.py --bounds.lower 5 --bounds.upper 10
bounds=(lower=5, upper=10)
bounds_with_default=(lower=0, upper=100)