Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ feat: json and bytes field support in options #985

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a3ee10a
✨ feat: json and bytes field suppport in options
mhkarimi1383 Sep 9, 2024
1dd8d71
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Sep 9, 2024
1f3d1cf
✏️ typo: fix linter issues
mhkarimi1383 Sep 9, 2024
c24f1a7
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Sep 9, 2024
11292ff
📝 docs: added json arg type in docs_src
mhkarimi1383 Sep 10, 2024
137e1bc
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Sep 10, 2024
0f5a1b3
🐛 fix: docs_src python version support
mhkarimi1383 Sep 10, 2024
432a48c
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Sep 10, 2024
31e7623
📝 docs: added json arg type docs
mhkarimi1383 Sep 10, 2024
f83e798
📝 docs: just a typo
mhkarimi1383 Sep 10, 2024
2b480ad
:white_check_mark: DictParamType tests added
mhkarimi1383 Sep 17, 2024
fdb7bb4
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Sep 17, 2024
114b761
:bug: DictParamType repl test fix
mhkarimi1383 Sep 17, 2024
4b75b07
Merge branch 'master' into feat-bytes-json-options
svlandeg Jan 8, 2025
38163f0
rewrite example code to focus on dict functionality
svlandeg Jan 10, 2025
71ca8e3
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Jan 10, 2025
240d614
rename to dict instead of json
svlandeg Jan 10, 2025
966206d
specific annotated and non-annotated version of the tutorial and test…
svlandeg Jan 10, 2025
7caf522
remove default
svlandeg Jan 10, 2025
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
28 changes: 28 additions & 0 deletions docs/tutorial/parameter-types/dict.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Dict

You can declare a *CLI parameter* to be a standard Python `dict`:

{* docs_src/parameter_types/dict/tutorial001_an.py hl[5] *}

Check it:

<div class="termy">

```console
// Run your program
$ python main.py --user-info '{"name": "Camila", "age": 15, "height": 1.7, "female": true}'

Name: Camila
User attributes: ['age', 'female', 'height', 'name']

```

</div>

This can be particularly useful when you want to include JSON input:

```python
import json

data = json.loads(user_input)
```
Empty file.
10 changes: 10 additions & 0 deletions docs_src/parameter_types/dict/tutorial001.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import typer


def main(user_info: dict = typer.Option()):
print(f"Name: {user_info.get('name', 'Unknown')}")
print(f"User attributes: {sorted(user_info.keys())}")


if __name__ == "__main__":
typer.run(main)
11 changes: 11 additions & 0 deletions docs_src/parameter_types/dict/tutorial001_an.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import typer
from typing_extensions import Annotated


def main(user_info: Annotated[dict, typer.Option()]):
print(f"Name: {user_info.get('name', 'Unknown')}")
print(f"User attributes: {sorted(user_info.keys())}")


if __name__ == "__main__":
typer.run(main)
19 changes: 18 additions & 1 deletion tests/test_others.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import os
import subprocess
import sys
Expand All @@ -12,7 +13,7 @@
import typer.completion
from typer.core import _split_opt
from typer.main import solve_typer_info_defaults, solve_typer_info_help
from typer.models import ParameterInfo, TyperInfo
from typer.models import DictParamType, ParameterInfo, TyperInfo
from typer.testing import CliRunner

from .utils import requires_completion_permission
Expand Down Expand Up @@ -278,3 +279,19 @@ def test_split_opt():
prefix, opt = _split_opt("verbose")
assert prefix == ""
assert opt == "verbose"


def test_json_param_type_convert():
data = {"name": "Camila", "age": 15, "height_meters": 1.7, "female": True}
converted = DictParamType().convert(json.dumps(data), None, None)
assert data == converted


def test_json_param_type_convert_dict_input():
data = {"name": "Camila", "age": 15, "height_meters": 1.7, "female": True}
converted = DictParamType().convert(data, None, None)
assert data == converted


def test_dict_param_tyoe_name():
assert repr(DictParamType()) == "DICT"
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import json
import subprocess
import sys

import typer
from typer.testing import CliRunner

from docs_src.parameter_types.dict import tutorial001 as mod

runner = CliRunner()

app = typer.Typer()
app.command()(mod.main)


def test_help():
result = runner.invoke(app, ["--help"])
assert result.exit_code == 0
assert "--user-info" in result.output
assert "DICT" in result.output


def test_params():
data = {"name": "Camila", "age": 15, "height": 1.7, "female": True}
result = runner.invoke(
app,
[
"--user-info",
json.dumps(data),
],
)
assert result.exit_code == 0
assert "Name: Camila" in result.output
assert "User attributes: ['age', 'female', 'height', 'name']" in result.output


def test_invalid():
result = runner.invoke(app, ["--user-info", "Camila"])
assert result.exit_code != 0
assert "Expecting value: line 1 column 1 (char 0)" in result.exc_info[1].args[0]


def test_script():
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__, "--help"],
capture_output=True,
encoding="utf-8",
)
assert "Usage" in result.stdout
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import json
import subprocess
import sys

import typer
from typer.testing import CliRunner

from docs_src.parameter_types.dict import tutorial001_an as mod

runner = CliRunner()

app = typer.Typer()
app.command()(mod.main)


def test_help():
result = runner.invoke(app, ["--help"])
assert result.exit_code == 0
assert "--user-info" in result.output
assert "DICT" in result.output


def test_params():
data = {"name": "Camila", "age": 15, "height": 1.7, "female": True}
result = runner.invoke(
app,
[
"--user-info",
json.dumps(data),
],
)
assert result.exit_code == 0
assert "Name: Camila" in result.output
assert "User attributes: ['age', 'female', 'height', 'name']" in result.output


def test_invalid():
result = runner.invoke(app, ["--user-info", "Camila"])
assert result.exit_code != 0
assert "Expecting value: line 1 column 1 (char 0)" in result.exc_info[1].args[0]


def test_script():
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__, "--help"],
capture_output=True,
encoding="utf-8",
)
assert "Usage" in result.stdout
5 changes: 4 additions & 1 deletion typer/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
Default,
DefaultPlaceholder,
DeveloperExceptionConfig,
DictParamType,
FileBinaryRead,
FileBinaryWrite,
FileText,
Expand Down Expand Up @@ -710,8 +711,10 @@ def get_click_type(
elif parameter_info.parser is not None:
return click.types.FuncParamType(parameter_info.parser)

elif annotation is str:
elif annotation in [str, bytes]:
return click.STRING
elif annotation is dict:
return DictParamType()
elif annotation is int:
if parameter_info.min is not None or parameter_info.max is not None:
min_ = None
Expand Down
19 changes: 18 additions & 1 deletion typer/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import inspect
import io
import json
from typing import (
TYPE_CHECKING,
Any,
Expand All @@ -20,7 +21,6 @@
from .core import TyperCommand, TyperGroup
from .main import Typer


NoneType = type(None)

AnyType = Type[Any]
Expand Down Expand Up @@ -52,6 +52,23 @@ class CallbackParam(click.Parameter):
pass


class DictParamType(click.ParamType):
name = "dict"

def convert(
self,
value: Any,
param: Optional["click.Parameter"],
ctx: Optional["click.Context"],
) -> Any:
if isinstance(value, dict):
return value
return json.loads(value)

def __repr__(self) -> str:
return "DICT"


class DefaultPlaceholder:
"""
You shouldn't use this class directly.
Expand Down
Loading