Skip to content

Commit

Permalink
Patch underscore prefix edge case
Browse files Browse the repository at this point in the history
  • Loading branch information
brentyi committed Oct 5, 2023
1 parent 7a8c6f7 commit b773b90
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 53 deletions.
55 changes: 32 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,32 @@

<br />

<strong><code>tyro</code></strong> is a tool for building command-line
interfaces and configuration objects in Python.

Our core interface, `tyro.cli()`, generates command-line interfaces from
type-annotated callables.

---
<strong><code>tyro</code></strong> is a tool for generating command-line
interfaces and configuration objects from type-annotated Python. We:

- Introduce a single-function core API, `tyro.cli()`, which is minimal enough to
use in throwaway scripts but flexible enough to be hardened in larger
projects.
- Support a broad range of Python type constructs, including basics (`int`,
`str`, `bool`, `float`, `pathlib.Path`, ...), both fixed- and variable-length
containers (`list[T]`, `tuple[T1, T2, ...]`, `set[T]`, `dict[K, V]`), unions
(`X | Y`, `Union[X, Y]`), literals (`Literal`), and generics (`TypeVar`).
- Understand hierarchy, nesting, and tools you may already use, like
`dataclasses`, `pydantic`, and `attrs`.
- Generate helptext automatically from defaults, annotations, and docstrings.
- Provide flexible support for subcommands, as well as choosing between and
overriding values in configuration objects.
- Enable tab completion in both your IDE and terminal.
- Expose fine-grained configuration via PEP 529 runtime annotations
(`tyro.conf.*`).

`tyro`'s use cases overlaps significantly with many other tools. The differences
are a result of several API goals:

- Focusing on a single, uninvasive function.
- Prioritizing compatibility with language servers and type checkers.
- Avoiding assumptions on serialization formats (like YAML or JSON) for
configuration objects.

### Brief walkthrough

Expand All @@ -65,10 +84,10 @@ print(total)
```

This pattern is dramatically cleaner than manually parsing `sys.argv`, but has
several issues: it lacks type checking and IDE support (consider: jumping to
several issues: it requires a significant amount of parsing-specific
boilerplate, lacks type checking and IDE support (consider: jumping to
definitions, finding references, docstrings, refactoring and renaming tools),
requires a significant amount of parsing-specific boilerplate, and becomes
difficult to manage for larger projects.
and becomes difficult to manage for larger projects.

The basic goal of `tyro.cli()` is to provide a wrapper for `argparse` that
solves these issues.
Expand Down Expand Up @@ -122,17 +141,6 @@ args = tyro.cli(Args)
print(args.a + args.b)
```

Unlike directly using `argparse`, both the function-based and dataclass-based
approaches are compatible with static analysis; tab completion and type checking
will work out-of-the-box.

**(3) Additional features.**

These examples only scratch the surface of what's possible. `tyro` aims to
support all reasonable type annotations, which can help us define things like
hierarchical structures, enums, unions, variable-length inputs, and subcommands.
See [documentation](https://brentyi.github.io/tyro) for examples.

### In the wild

`tyro` is still a new library, but being stress tested in several projects!
Expand All @@ -148,7 +156,8 @@ See [documentation](https://brentyi.github.io/tyro) for examples.
implementation of [instant-ngp](https://nvlabs.github.io/instant-ngp/),
implemented in JAX.
- [NVIDIAGameWorks/kaolin-wisp](https://github.com/NVIDIAGameWorks/kaolin-wisp)
combines `tyro` with [`hydra-zen`](https://github.com/mit-ll-responsible-ai/hydra-zen)
for neural fields in PyTorch.
combines `tyro` with
[`hydra-zen`](https://github.com/mit-ll-responsible-ai/hydra-zen) for neural
fields in PyTorch.
- [openrlbenchmark/openrlbenchmark](https://github.com/openrlbenchmark/openrlbenchmark)
is a collection of tracked experiments for reinforcement learning.
7 changes: 0 additions & 7 deletions docs/requirements.txt

This file was deleted.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ name = "tyro"
authors = [
{name = "brentyi", email = "[email protected]"},
]
version = "0.5.9"
version = "0.5.10"
description = "Strongly typed, zero-effort CLI interfaces"
readme = "README.md"
license = { text="MIT" }
Expand Down
28 changes: 28 additions & 0 deletions tests/test_nested.py
Original file line number Diff line number Diff line change
Expand Up @@ -989,3 +989,31 @@ def train(
container=DatasetContainer(ImageNet(50))
)
assert tyro.cli(train, args=["sgd"]) == Sgd(container=DatasetContainer(Mnist()))


def test_underscore_prefix() -> None:
"""https://github.com/brentyi/tyro/issues/77"""

@dataclasses.dataclass
class PrivateConfig:
pass

@dataclasses.dataclass
class BaseConfig:
_private: PrivateConfig = dataclasses.field(
default_factory=lambda: PrivateConfig()
)

@dataclasses.dataclass
class Level1(BaseConfig):
pass

@dataclasses.dataclass
class Level2(BaseConfig):
child: Level1 = dataclasses.field(default_factory=lambda: Level1())

@dataclasses.dataclass
class Level3(BaseConfig):
child: Level2 = dataclasses.field(default_factory=lambda: Level2())

tyro.cli(Level3, args=[])
1 change: 0 additions & 1 deletion tyro/_calling.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ def get_value_from_arg(prefixed_field_name: str) -> Any:

# Resolve field type.
field_type = field.typ

if prefixed_field_name in arg_from_prefixed_field_name:
assert prefixed_field_name not in consumed_keywords

Expand Down
41 changes: 20 additions & 21 deletions tyro/_strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,33 +34,32 @@ def get_delimeter() -> Literal["-", "_"]:
return DELIMETER


def replace_delimeter_in_part(p: str) -> str:
"""Replace hyphens with underscores (or vice versa) except when at the start."""
if get_delimeter() == "-":
num_underscore_prefix = 0
for i in range(len(p)):
if p[i] == "_":
num_underscore_prefix += 1
else:
break
p = "_" * num_underscore_prefix + (p[num_underscore_prefix:].replace("_", "-"))
else:
p = p.replace("-", "_")
return p


def make_field_name(parts: Sequence[str]) -> str:
"""Join parts of a field name together. Used for nesting.
('parent_1', 'child') => 'parent-1.child'
('parents', '1', '_child_node') => 'parents.1._child-node'
('parents', '1', 'middle._child_node') => 'parents.1.middle._child-node'
"""
out: List[str] = []
for i, p in enumerate(_strip_dummy_field_names(parts)):
if i > 0:
out.append(".")

# Replace all underscores with hyphens, except ones at the start of a string.
if get_delimeter() == "-":
num_underscore_prefix = 0
for i in range(len(p)):
if p[i] == "_":
num_underscore_prefix += 1
else:
break
p = "_" * num_underscore_prefix + (
p[num_underscore_prefix:].replace("_", "-")
)
else:
p = p.replace("-", "_")
out.append(p)

return "".join(out)
for p in _strip_dummy_field_names(parts):
out.extend(map(replace_delimeter_in_part, p.split(".")))
return ".".join(out)


def make_subparser_dest(name: str) -> str:
Expand Down Expand Up @@ -138,7 +137,7 @@ def subparser_name_from_type(prefix: str, cls: Type) -> str:
return suffix

if get_delimeter() == "-":
return f"{prefix}:{suffix}".replace("_", "-")
return f"{prefix}:{make_field_name(suffix.split('.'))}"
else:
assert get_delimeter() == "_"
return f"{prefix}:{suffix}"
Expand Down

0 comments on commit b773b90

Please sign in to comment.