Skip to content

Commit

Permalink
Start struct handling rewrite (#191)
Browse files Browse the repository at this point in the history
* Custom constructors + container type resolution cleanup, docs

* Simpler rule API

* Global namespace cleanup

* fix for older Python versions

* Ignore docs for pyright

* Bump typing-extensions for typing.ReadOnly

* Docs sync

* Fix 3.7 typing-extension dep

* Fix issubclass error for new pydantic

* Add pydantic + generics test

* Start refactor

* More refactor

* Passing tests

* pyright

* ruff

* exports

* fix frozendict for Python 3.8

* dict fix

* mypy

* coverage
  • Loading branch information
brentyi authored Nov 4, 2024
1 parent dd11c77 commit 6552337
Show file tree
Hide file tree
Showing 41 changed files with 1,516 additions and 1,487 deletions.
1 change: 0 additions & 1 deletion .github/workflows/pyright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ jobs:
run: |
pip install uv
uv pip install --system -e ".[dev]"
uv pip install --system -r docs/requirements.txt
- name: Run pyright
run: |
pyright .
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@
</p>

<p align="center">
<img alt="build" src="https://github.com/brentyi/tyro/actions/workflows/build.yml/badge.svg" />
<!-- <img alt="build" src="https://github.com/brentyi/tyro/actions/workflows/build.yml/badge.svg" /> -->
<img alt="mypy" src="https://github.com/brentyi/tyro/actions/workflows/mypy.yml/badge.svg" />
<img alt="pyright" src="https://github.com/brentyi/tyro/actions/workflows/pyright.yml/badge.svg" />
<img alt="ruff" src="https://github.com/brentyi/tyro/actions/workflows/ruff.yml/badge.svg" />
<!-- <img alt="ruff" src="https://github.com/brentyi/tyro/actions/workflows/ruff.yml/badge.svg" /> -->
<a href="https://codecov.io/gh/brentyi/tyro">
<img alt="codecov" src="https://codecov.io/gh/brentyi/tyro/branch/main/graph/badge.svg" />
</a>
Expand Down
4 changes: 2 additions & 2 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
sphinx==7.2.6
furo==2023.9.10
sphinx==8.0.2
furo==2024.8.6
docutils==0.20.1
sphinx-autoapi==3.0.0
m2r2==0.3.3.post2
Expand Down
71 changes: 30 additions & 41 deletions docs/source/examples/04_additional/11_custom_constructors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,59 +4,32 @@
Custom Constructors
==========================================

For additional flexibility, :func:`tyro.conf.arg()` accepts a
:code:`constructor` argument, which makes it easier to load complex objects.

.. warning::
Custom constructors are permitted wherever :func:`tyro.conf.arg()` is. This
annotation corresponds to a single argument, so we expect that it is placed
placed at the root of the field corresponding to that argument. For
example:

.. code-block:: python
# Great!
x: Annotated[int, tyro.conf.arg(custom_constructor=...)]
However, nesting :func:`tyro.conf.arg()` within other types is generally
not supported. For example:

.. code-block:: python
# Not supported.
x: tuple[Annotated[int, tyro.conf.arg(custom_constructor=...)], ...]
This example can be rewritten with a custom constructor that directly
returns a tuple of integers:

.. code-block:: python
# Workaround for above.
x: Annotated[tuple[int, ...], tyro.conf.arg(custom_constructor=...)]
For additional flexibility, :module:`tyro.constructors` exposes
tyro's API for defining behavior for different types. This is the same
API that tyro relies on for the built-in types.


.. code-block:: python
:linenos:
import json as json_
import json
from typing_extensions import Annotated
import tyro
def dict_json_constructor(json: str) -> dict:
"""Construct a dictionary from a JSON string. Raises a ValueError if the result is
not a dictionary."""
out = json_.loads(json)
if not isinstance(out, dict):
raise ValueError(f"{json} is not a dictionary!")
return out
# A dictionary type, but `tyro` will expect a JSON string from the CLI.
JsonDict = Annotated[dict, tyro.conf.arg(constructor=dict_json_constructor)]
JsonDict = Annotated[
dict,
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(
Expand Down Expand Up @@ -88,6 +61,22 @@ For additional flexibility, :func:`tyro.conf.arg()` accepts a

------------

.. raw:: html

<kbd>python 04_additional/11_custom_constructors.py --dict1.json '{"hello": "world"}'</kbd>

.. program-output:: python ../../examples/04_additional/11_custom_constructors.py --dict1.json '{"hello": "world"}'

------------

.. raw:: html

<kbd>python 04_additional/11_custom_constructors.py --dict1.json '{"hello": "world"}' --dict2.json '{"hello": "world"}'</kbd>

.. program-output:: python ../../examples/04_additional/11_custom_constructors.py --dict1.json '{"hello": "world"}' --dict2.json '{"hello": "world"}'

------------

.. raw:: html

<kbd>python 04_additional/11_custom_constructors.py --dict1.json '{"hello": "world"}' --dict2.json '{"hello": "world"}'</kbd>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
.. Comment: this file is automatically generated by `update_example_docs.py`.
It should not be modified manually.
Custom Constructors (Registry)
==========================================

For additional flexibility, :module:`tyro.constructors` exposes
tyro's API for defining behavior for different types. This is the same
API that tyro relies on for the built-in types.


.. code-block:: python
:linenos:
import json
from typing import Any
import tyro
custom_registry = tyro.constructors.PrimitiveConstructorRegistry()
@custom_registry.define_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:
print(f"{dict1=}")
print(f"{dict2=}")
if __name__ == "__main__":
with custom_registry:
tyro.cli(main)
------------

.. raw:: html

<kbd>python 04_additional/12_custom_constructors_registry.py --help</kbd>

.. program-output:: python ../../examples/04_additional/12_custom_constructors_registry.py --help

------------

.. raw:: html

<kbd>python 04_additional/12_custom_constructors_registry.py --dict1.json '{"hello": "world"}'</kbd>

.. program-output:: python ../../examples/04_additional/12_custom_constructors_registry.py --dict1.json '{"hello": "world"}'

------------

.. raw:: html

<kbd>python 04_additional/12_custom_constructors_registry.py --dict1.json '{"hello": "world"}'</kbd>

.. program-output:: python ../../examples/04_additional/12_custom_constructors_registry.py --dict1.json '{"hello": "world"}'

------------

.. raw:: html

<kbd>python 04_additional/12_custom_constructors_registry.py --dict1.json '{"hello": "world"}' --dict2.json '{"hello": "world"}'</kbd>

.. program-output:: python ../../examples/04_additional/12_custom_constructors_registry.py --dict1.json '{"hello": "world"}' --dict2.json '{"hello": "world"}'

------------

.. raw:: html

<kbd>python 04_additional/12_custom_constructors_registry.py --dict1.json '{"hello": "world"}' --dict2.json '{"hello": "world"}'</kbd>

.. program-output:: python ../../examples/04_additional/12_custom_constructors_registry.py --dict1.json '{"hello": "world"}' --dict2.json '{"hello": "world"}'
Original file line number Diff line number Diff line change
Expand Up @@ -43,54 +43,54 @@ Argument Aliases

.. raw:: html

<kbd>python 04_additional/12_aliases.py --help</kbd>
<kbd>python 04_additional/17_aliases.py --help</kbd>

.. program-output:: python ../../examples/04_additional/12_aliases.py --help
.. program-output:: python ../../examples/04_additional/17_aliases.py --help

------------

.. raw:: html

<kbd>python 04_additional/12_aliases.py commit --help</kbd>
<kbd>python 04_additional/17_aliases.py commit --help</kbd>

.. program-output:: python ../../examples/04_additional/12_aliases.py commit --help
.. program-output:: python ../../examples/04_additional/17_aliases.py commit --help

------------

.. raw:: html

<kbd>python 04_additional/12_aliases.py commit --message hello --all</kbd>
<kbd>python 04_additional/17_aliases.py commit --message hello --all</kbd>

.. program-output:: python ../../examples/04_additional/12_aliases.py commit --message hello --all
.. program-output:: python ../../examples/04_additional/17_aliases.py commit --message hello --all

------------

.. raw:: html

<kbd>python 04_additional/12_aliases.py commit -m hello -a</kbd>
<kbd>python 04_additional/17_aliases.py commit -m hello -a</kbd>

.. program-output:: python ../../examples/04_additional/12_aliases.py commit -m hello -a
.. program-output:: python ../../examples/04_additional/17_aliases.py commit -m hello -a

------------

.. raw:: html

<kbd>python 04_additional/12_aliases.py checkout --help</kbd>
<kbd>python 04_additional/17_aliases.py checkout --help</kbd>

.. program-output:: python ../../examples/04_additional/12_aliases.py checkout --help
.. program-output:: python ../../examples/04_additional/17_aliases.py checkout --help

------------

.. raw:: html

<kbd>python 04_additional/12_aliases.py checkout --branch main</kbd>
<kbd>python 04_additional/17_aliases.py checkout --branch main</kbd>

.. program-output:: python ../../examples/04_additional/12_aliases.py checkout --branch main
.. program-output:: python ../../examples/04_additional/17_aliases.py checkout --branch main

------------

.. raw:: html

<kbd>python 04_additional/12_aliases.py checkout -b main</kbd>
<kbd>python 04_additional/17_aliases.py checkout -b main</kbd>

.. program-output:: python ../../examples/04_additional/12_aliases.py checkout -b main
.. program-output:: python ../../examples/04_additional/17_aliases.py checkout -b main
58 changes: 16 additions & 42 deletions examples/04_additional/11_custom_constructors.py
Original file line number Diff line number Diff line change
@@ -1,60 +1,34 @@
"""Custom Constructors
For additional flexibility, :func:`tyro.conf.arg()` accepts a
:code:`constructor` argument, which makes it easier to load complex objects.
.. warning::
Custom constructors are permitted wherever :func:`tyro.conf.arg()` is. This
annotation corresponds to a single argument, so we expect that it is placed
placed at the root of the field corresponding to that argument. For
example:
.. code-block:: python
# Great!
x: Annotated[int, tyro.conf.arg(custom_constructor=...)]
However, nesting :func:`tyro.conf.arg()` within other types is generally
not supported. For example:
.. code-block:: python
# Not supported.
x: tuple[Annotated[int, tyro.conf.arg(custom_constructor=...)], ...]
This example can be rewritten with a custom constructor that directly
returns a tuple of integers:
.. code-block:: python
# Workaround for above.
x: Annotated[tuple[int, ...], tyro.conf.arg(custom_constructor=...)]
For additional flexibility, :module:`tyro.constructors` exposes
tyro's API for defining behavior for different types. This is the same
API that tyro relies on for the built-in types.
Usage:
`python ./10_custom_constructors.py --help`
`python ./10_custom_constructors.py --dict1.json '{"hello": "world"}'`
`python ./10_custom_constructors.py --dict1.json "{\"hello\": \"world\"}"`
`python ./10_custom_constructors.py --dict1.json '{"hello": "world"}' --dict2.json '{"hello": "world"}'`
`python ./10_custom_constructors.py --dict1.json "{\"hello\": \"world\"}" --dict2.json "{\"hello\": \"world\"}"`
"""

import json as json_
import json

from typing_extensions import Annotated

import tyro


def dict_json_constructor(json: str) -> dict:
"""Construct a dictionary from a JSON string. Raises a ValueError if the result is
not a dictionary."""
out = json_.loads(json)
if not isinstance(out, dict):
raise ValueError(f"{json} is not a dictionary!")
return out


# A dictionary type, but `tyro` will expect a JSON string from the CLI.
JsonDict = Annotated[dict, tyro.conf.arg(constructor=dict_json_constructor)]
JsonDict = Annotated[
dict,
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(
Expand Down
Loading

0 comments on commit 6552337

Please sign in to comment.