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 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
37 changes: 37 additions & 0 deletions docs/tutorial/parameter-types/json.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# JSON

To use JSON inputs use `dict` as Argument type

it will do something like

```python
import json

data = json.loads(user_input)
```

## Usage

You will get all the correct editor support, attributes, methods, etc for the dict object:

//// tab | Python 3.7+

```Python hl_lines="5"
{!> ../docs_src/parameter_types/json/tutorial001.py!}
```

////

Check it:

<div class="termy">

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

User Info: {"name": "Camila", "age": 15, "height_meters": 1.7, "female": true}

```

</div>
Empty file.
12 changes: 12 additions & 0 deletions docs_src/parameter_types/json/tutorial001.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import json

import typer
from typing_extensions import Annotated


def main(user_info: Annotated[dict, typer.Option()]):
print(f"User Info: {json.dumps(user_info)}")


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

runner = CliRunner()
Expand Down Expand Up @@ -275,3 +276,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,48 @@
import json
import subprocess
import sys

import typer
from typer.testing import CliRunner

from docs_src.parameter_types.json 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_meters": 1.7, "female": True}
result = runner.invoke(
app,
[
"--user-info",
json.dumps(data),
],
)
assert result.exit_code == 0
assert result.output.strip() == (f"User Info: {json.dumps(data)}")


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 @@ -32,6 +32,7 @@
Default,
DefaultPlaceholder,
DeveloperExceptionConfig,
DictParamType,
FileBinaryRead,
FileBinaryWrite,
FileText,
Expand Down Expand Up @@ -716,8 +717,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