Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
9b6081d
add pydantic as requirement
svlandeg Jun 9, 2026
b54b308
use TypeAdapter for int and float validation
svlandeg Jun 9, 2026
c8640ae
Range adaptor and Pydantic error messages
svlandeg Jun 9, 2026
10746de
UUID adapter
svlandeg Jun 9, 2026
394e7f2
DateTime adapter
svlandeg Jun 9, 2026
27f5880
Choice adapter
svlandeg Jun 9, 2026
570d2be
introduce PydanticParamType
svlandeg Jun 9, 2026
c8f6dff
build_type_adapter
svlandeg Jun 10, 2026
15a0e9a
consolidate enum validation
svlandeg Jun 10, 2026
94cf21b
type adapter for bool parsing
svlandeg Jun 10, 2026
d0fe927
🎨 Auto format
pre-commit-ci-lite[bot] Jun 10, 2026
b749fc2
fix types
svlandeg Jun 10, 2026
48ff1f4
param_type_from_annotation in new module param_types
svlandeg Jun 10, 2026
e0fd56d
move str and File to new module
svlandeg Jun 10, 2026
5eb5ade
🎨 Auto format
pre-commit-ci-lite[bot] Jun 10, 2026
4495762
clean up converter code
svlandeg Jun 10, 2026
31432df
remove support for click_type and open bounds
svlandeg Jun 10, 2026
eaa68dd
datetime factory
svlandeg Jun 10, 2026
a0e429e
🎨 Auto format
pre-commit-ci-lite[bot] Jun 10, 2026
b79b401
move min/max info into TyperOption and TyperArgument and clamp using …
svlandeg Jun 10, 2026
58bb823
Merge branch 'feat/pydantic' of https://github.com/svlandeg/typer int…
svlandeg Jun 10, 2026
3953649
🎨 Auto format
pre-commit-ci-lite[bot] Jun 10, 2026
943b335
repr cleanup
svlandeg Jun 10, 2026
5b5085e
move _types.py stuff to param_types.py
svlandeg Jun 11, 2026
7c201ec
move TyperPath to param_types.py as well
svlandeg Jun 11, 2026
621280b
remove unused ignore statement
svlandeg Jun 11, 2026
b6165b7
centralize Pydantic functionality to adapaters.py
svlandeg Jun 11, 2026
4332d34
resolve_param_type instead of convert_type
svlandeg Jun 12, 2026
43f71ad
fix type
svlandeg Jun 12, 2026
d6bc20d
fix import
svlandeg Jun 12, 2026
fdf2973
few more test cases for default type inference
svlandeg Jun 12, 2026
0336edf
move FuncParamType
svlandeg Jun 12, 2026
98e200b
coercion through RuntimeParam schema, slim down PydanticParamType to …
svlandeg Jun 14, 2026
12df977
move out File to param_types and extend RuntimeParam
svlandeg Jun 14, 2026
4972630
some cleanup and refactor
svlandeg Jun 15, 2026
9d97184
cleanup inline imports
svlandeg Jun 15, 2026
bab9056
TyperTuple into param_types
svlandeg Jun 15, 2026
0809456
ChoiceRuntimeParam
svlandeg Jun 15, 2026
80495f9
ChoiceRuntimeParam
svlandeg Jun 15, 2026
199b28b
clean up ParamType methods and introduce TyperParameter
svlandeg Jun 15, 2026
39a2c15
Merge branch 'master' into feat/pydantic
svlandeg Jun 16, 2026
78a1c37
refactors and simplifications
svlandeg Jun 16, 2026
cfccba8
fix test
svlandeg Jun 16, 2026
9a9bee7
some more cleanup in RuntimeParam/DeclaredParam/Parameter
svlandeg Jun 16, 2026
b7531f5
remove ParamType's repr functionality
svlandeg Jun 16, 2026
f98d311
consolidate metavar functionality and discard the short-lived Display…
svlandeg Jun 16, 2026
ab3863e
Merge branch 'master' into feat/pydantic
svlandeg Jun 17, 2026
46e3fec
return metavar in <> and lowercased
svlandeg Jun 17, 2026
1e6ff44
removing CommandSchema for now and make RuntimeParam required
svlandeg Jun 17, 2026
b157ea9
use TyperParameter more often and further unify metavar printing (sti…
svlandeg Jun 17, 2026
f3679c7
fix metavar for list
svlandeg Jun 17, 2026
3b3459a
clean out param_types and move metavar functions to TyperParameter
svlandeg Jun 18, 2026
0264bca
fix fixture and isolate temp dir
svlandeg Jun 18, 2026
de4e031
clean up default inference
svlandeg Jun 19, 2026
0c8b6a8
PassThroughRuntimeParam instead of falling back to str
svlandeg Jun 19, 2026
042d16c
small refactors for diff readability
svlandeg Jun 19, 2026
8600249
Merge branch 'master' into feat/pydantic
svlandeg Jun 22, 2026
b448af6
formatting
svlandeg Jun 22, 2026
0bf9b2f
remove intermediate layer DeclaredParam
svlandeg Jun 22, 2026
9d778d4
🎨 Auto format
pre-commit-ci-lite[bot] Jun 22, 2026
66f57a8
TypeDescriptor to solidify type coercion
svlandeg Jun 22, 2026
f462dab
Merge remote-tracking branch 'origin/feat/pydantic' into feat/pydantic
svlandeg Jun 22, 2026
65dd995
🎨 Auto format
pre-commit-ci-lite[bot] Jun 22, 2026
68c3a7f
remove unnecessary helper functions
svlandeg Jun 23, 2026
9c08b7c
🎨 Auto format
pre-commit-ci-lite[bot] Jun 23, 2026
0a0365a
fix types
svlandeg Jun 23, 2026
a1e10b6
Merge branch 'feat/pydantic' of https://github.com/svlandeg/typer int…
svlandeg Jun 23, 2026
1f058cc
fix types (again)
svlandeg Jun 23, 2026
07c724a
keep fixing types
svlandeg Jun 23, 2026
291e4e9
remove _click/types and ParamType entirely
svlandeg Jun 23, 2026
c5f30ac
Merge branch 'master' into feat/pydantic
svlandeg Jun 24, 2026
9b84173
better test for str resolved as path
svlandeg Jun 24, 2026
6198cd6
even better test for str resolve
svlandeg Jun 24, 2026
6230efc
avoid Path coercion for str-typed parameters
svlandeg Jun 24, 2026
9c3ddd8
extend metavar test with defaults (will fail for now due to related i…
svlandeg Jun 24, 2026
3948629
Merge branch 'master' into feat/pydantic
svlandeg Jun 26, 2026
53a6cf0
fix metavar type display
svlandeg Jun 26, 2026
166b22d
solidify metavar printing (part 1: brackets)
svlandeg Jun 26, 2026
f1cb56e
🎨 Auto format
pre-commit-ci-lite[bot] Jun 26, 2026
e7e8432
part 2 of metavar printing: use {} to denote arg/opt names otherwise …
svlandeg Jun 26, 2026
27c7840
🎨 Auto format
pre-commit-ci-lite[bot] Jun 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions docs/tutorial/parameter-types/enum.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ Check it:
```console
$ python main.py --help

// Notice the predefined values [simple|conv|lstm]
// Notice the predefined values <simple|conv|lstm>
Usage: main.py [OPTIONS]

Options:
--network [simple|conv|lstm] [default: simple]
--network <simple|conv|lstm> [default: simple]
--help Show this message and exit.

// Try it
Expand Down Expand Up @@ -91,8 +91,8 @@ $ python main.py --help
Usage: main.py [OPTIONS]

Options:
--groceries [Eggs|Bacon|Cheese] [default: Eggs, Cheese]
--help Show this message and exit.
--groceries <list[Eggs|Bacon|Cheese]> [default: Eggs, Cheese]
--help Show this message and exit.

// Try it with the default values
$ python main.py
Expand Down Expand Up @@ -123,11 +123,11 @@ You can also use `Literal` to represent a set of possible predefined choices, wi
```console
$ python main.py --help

// Notice the predefined values [simple|conv|lstm]
// Notice the predefined values <simple|conv|lstm>
Usage: main.py [OPTIONS]

Options:
--network [simple|conv|lstm] [default: simple]
--network <simple|conv|lstm> [default: simple]
--help Show this message and exit.

// Try it
Expand Down
4 changes: 2 additions & 2 deletions docs_src/parameter_types/number/tutorial001_an_py310.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@

@app.command()
def main(
id: Annotated[int, typer.Argument(min=0, max=1000)],
ID: Annotated[int, typer.Argument(min=0, max=1000)],
age: Annotated[int, typer.Option(min=18)] = 20,
score: Annotated[float, typer.Option(max=100)] = 0,
):
print(f"ID is {id}")
print(f"ID is {ID}")
print(f"--age is {age}")
print(f"--score is {score}")

Expand Down
4 changes: 2 additions & 2 deletions docs_src/parameter_types/number/tutorial001_py310.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@

@app.command()
def main(
id: int = typer.Argument(..., min=0, max=1000),
ID: int = typer.Argument(..., min=0, max=1000),
age: int = typer.Option(20, min=18),
score: float = typer.Option(0, max=100),
):
print(f"ID is {id}")
print(f"ID is {ID}")
print(f"--age is {age}")
print(f"--score is {score}")

Expand Down
4 changes: 2 additions & 2 deletions docs_src/parameter_types/number/tutorial002_an_py310.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@

@app.command()
def main(
id: Annotated[int, typer.Argument(min=0, max=1000)],
ID: Annotated[int, typer.Argument(min=0, max=1000)],
rank: Annotated[int, typer.Option(max=10, clamp=True)] = 0,
score: Annotated[float, typer.Option(min=0, max=100, clamp=True)] = 0,
):
print(f"ID is {id}")
print(f"ID is {ID}")
print(f"--rank is {rank}")
print(f"--score is {score}")

Expand Down
4 changes: 2 additions & 2 deletions docs_src/parameter_types/number/tutorial002_py310.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@

@app.command()
def main(
id: int = typer.Argument(..., min=0, max=1000),
ID: int = typer.Argument(..., min=0, max=1000),
rank: int = typer.Option(0, max=10, clamp=True),
score: float = typer.Option(0, min=0, max=100, clamp=True),
):
print(f"ID is {id}")
print(f"ID is {ID}")
print(f"--rank is {rank}")
print(f"--score is {score}")

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ classifiers = [
"Programming Language :: Python :: 3.14",
]
dependencies = [
"pydantic >=2.5.3",
"shellingham >=1.3.0",
"rich >=13.8.0",
"annotated-doc >=0.0.2",
Expand Down
8 changes: 4 additions & 4 deletions tests/assets/cli/multiapp-docs-title.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ $ multiapp sub hello [OPTIONS]

**Options**:

* `--name TEXT`: [default: World]
* `--age INTEGER`: The age of the user [default: 0]
* `--name <str>`: [default: World]
* `--age <int>`: The age of the user [default: 0]
* `--help`: Show this message and exit.

### `multiapp sub hi`
Expand All @@ -76,12 +76,12 @@ Say Hi
**Usage**:

```console
$ multiapp sub hi [OPTIONS] [USER]
$ multiapp sub hi [OPTIONS] [user]
```

**Arguments**:

* `[USER]`: The name of the user to greet [default: World]
* `[user]`: The name of the user to greet [default: World]

**Options**:

Expand Down
8 changes: 4 additions & 4 deletions tests/assets/cli/multiapp-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ $ multiapp sub hello [OPTIONS]

**Options**:

* `--name TEXT`: [default: World]
* `--age INTEGER`: The age of the user [default: 0]
* `--name <str>`: [default: World]
* `--age <int>`: The age of the user [default: 0]
* `--help`: Show this message and exit.

### `multiapp sub hi`
Expand All @@ -76,12 +76,12 @@ Say Hi
**Usage**:

```console
$ multiapp sub hi [OPTIONS] [USER]
$ multiapp sub hi [OPTIONS] [user]
```

**Arguments**:

* `[USER]`: The name of the user to greet [default: World]
* `[user]`: The name of the user to greet [default: World]

**Options**:

Expand Down
6 changes: 3 additions & 3 deletions tests/assets/cli/richformattedapp-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ Say <span style="color: #800000; text-decoration-color: #800000; font-weight: bo
**Usage**:

```console
$ hello [OPTIONS] USER_1 [USER_2]
$ hello [OPTIONS] {user_1} [user_2]
```

**Arguments**:

* `USER_1`: The <span style="font-weight: bold">cool</span> name of the <span style="color: #008000; text-decoration-color: #008000">user</span> [required]
* `[USER_2]`: The world [default: The World]
* `user_1`: The <span style="font-weight: bold">cool</span> name of the <span style="color: #008000; text-decoration-color: #008000">user</span> [required]
* `[user_2]`: The world [default: The World]

**Options**:

Expand Down
3 changes: 2 additions & 1 deletion tests/assets/completion_argument.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import typer
from typer import _click
from typer.core import TyperParameter

app = typer.Typer()


def shell_complete(ctx: _click.Context, param: _click.Parameter, incomplete: str):
def shell_complete(ctx: _click.Context, param: TyperParameter, incomplete: str):
typer.echo(f"ctx: {ctx.info_name}", err=True)
typer.echo(f"arg is: {param.name}", err=True)
typer.echo(f"incomplete is: {incomplete}", err=True)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_cli/test_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def cmd(value: str) -> None:
output_lines = result.output.splitlines()
usage_idx = output_lines.index("Usage: very-long-program-name-that-forces-wrap ")
args_line = output_lines[usage_idx + 1]
assert args_line.lstrip() == "[OPTIONS] VALUE"
assert args_line.lstrip() == "[OPTIONS] {value}"
assert args_line.startswith(" ")


Expand Down
129 changes: 129 additions & 0 deletions tests/test_coercion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
from pathlib import Path

import typer
from typer.testing import CliRunner

runner = CliRunner()


def test_coercion() -> None:
app = typer.Typer()
seen: dict[str, object] = {}

@app.command()
def main(items: list[int], active: bool = False, val=42):
seen["items"] = items
seen["active"] = active
seen["val"] = val

result = runner.invoke(app, ["1", "2", "--active", "--val", "7"])
assert result.exit_code == 0, result.output
assert seen == {"items": [1, 2], "active": True, "val": 7}


def test_coercion_invalid() -> None:
app = typer.Typer()

@app.command()
def main(age: int):
pass

result = runner.invoke(app, ["not-an-int"])
assert "Input should be a valid integer" in result.stderr
assert result.exit_code == 2


def test_coercion_path(tmp_path: Path) -> None:
target = tmp_path / "config.txt"
target.write_text("hello\n", encoding="utf-8")
app = typer.Typer()
seen: list[Path] = []

@app.command()
def main(config: Path = typer.Option(..., exists=True)):
seen.append(config)

result = runner.invoke(app, ["--config", str(target)])
assert result.exit_code == 0
assert seen == [target]


def test_coercion_tuple_files(tmp_path: Path) -> None:
first = tmp_path / "first.txt"
second = tmp_path / "second.txt"
first.write_text("first-content\n", encoding="utf-8")
second.write_text("second-content\n", encoding="utf-8")
app = typer.Typer()
seen: list[str] = []

@app.command()
def main(files: tuple[typer.FileText, typer.FileText]):
seen.append(files[0].read())
seen.append(files[1].read())

result = runner.invoke(app, [str(first), str(second)])
assert result.exit_code == 0, result.output
assert seen == ["first-content\n", "second-content\n"]


def test_passthrough_runtime_param_default() -> None:
class Widget:
def __init__(self, value: int) -> None:
self.value = value

def __repr__(self) -> str:
return f"Widget({self.value})"

app = typer.Typer()
seen: dict[str, Widget] = {}

@app.command()
def main(val=Widget(42)):
seen["val"] = val

param = next(p for p in typer.main.get_command(app).params if p.name == "val")
assert param.runtime_param is not None
assert param.runtime_param.annotation is Widget

result = runner.invoke(app)
assert result.exit_code == 0
assert isinstance(seen["val"], Widget)
assert seen["val"].value == 42

result = runner.invoke(app, ["--val", "666"])
assert result.exit_code == 2
# This doesn't work because there's no parser
assert "is not a valid Widget" in result.output


def test_widget_parsed_from_cli_with_parser() -> None:
class Widget:
def __init__(self, value: int) -> None:
self.value = value

def __repr__(self) -> str:
return f"Widget({self.value})"

def parse_widget(value: str) -> Widget:
return Widget(int(value))

app = typer.Typer()
seen: dict[str, Widget] = {}

@app.command()
def main(val: Widget = typer.Option("42", parser=parse_widget)):
seen["val"] = val

param = next(p for p in typer.main.get_command(app).params if p.name == "val")
assert param.runtime_param is not None
assert param.runtime_param.annotation is Widget

result = runner.invoke(app)
assert result.exit_code == 0
assert isinstance(seen["val"], Widget)
assert seen["val"].value == 42

result = runner.invoke(app, ["--val", "666"])
assert result.exit_code == 0
assert isinstance(seen["val"], Widget)
assert seen["val"].value == 666
Loading
Loading