.. 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 will be generated by parsing the signature of the constructor function. In this example, we 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__": # Pass the registry directly to tyro.cli(). tyro.cli(main, registry=custom_registry) .. 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 struct type. Each struct type is broken down into multiple fields, which themselves can be either primitive types or nested structs. .. 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]) 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) # 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], helptext="Lower bound.", ), tyro.constructors.StructFieldSpec( name="upper", type=int, default=default[1], 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__": # Pass the registry directly to tyro.cli(). tyro.cli(main, registry=custom_registry) .. 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)