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

Support Python 3.10 optional union types #522

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/tutorial/arguments/optional.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ Because we are using `typer.Argument()` **Typer** will know that this is a *CLI
And because the first parameter passed to `typer.Argument(None)` (the new "default" value) is `None`, **Typer** knows that this is an **optional** *CLI argument*, if no value is provided when calling it in the command line, it will have that default value of `None`.

!!! tip
By using `Optional` your editor will be able to know that the value *could* be `None`, and will be able to warn you if you do something assuming it is a `str` that would break if it was `None`.
By using `Optional` your editor will be able to know that the value *could* be `None`, and will be able to warn you if you do something assuming it is a `str` that would break if it was `None`. If you are using Python 3.10 or newer, you can also use `T | None` instead of `Optional[T]` (e.g., `str | None`).

Check the help:

Expand Down
23 changes: 23 additions & 0 deletions tests/test_type_conversion.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import sys
from enum import Enum
from pathlib import Path
from typing import List, Optional, Tuple
Expand Down Expand Up @@ -28,6 +29,28 @@ def opt(user: Optional[str] = None):
assert "User: Camila" in result.output


def test_union_none():
if sys.version_info < (3, 10):
pytest.skip("SomeType | None is only available in Python 3.10+")

app = typer.Typer()

@app.command()
def union_none(user: "str | None" = None):
if user:
print(f"User: {user}")
else:
print("No user")

result = runner.invoke(app)
assert result.exit_code == 0
assert "No user" in result.output

result = runner.invoke(app, ["--user", "Camila"])
assert result.exit_code == 0
assert "User: Camila" in result.output


def test_no_type():
app = typer.Typer()

Expand Down
57 changes: 29 additions & 28 deletions typer/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -811,35 +811,36 @@ def get_click_param(
parameter_type: Any = None
is_flag = None
origin = getattr(main_type, "__origin__", None)
if origin is not None:
# Handle Optional[SomeType]
if origin is Union:
types = []
for type_ in main_type.__args__:
if type_ is NoneType:
continue
types.append(type_)
assert len(types) == 1, "Typer Currently doesn't support Union types"
main_type = types[0]
origin = getattr(main_type, "__origin__", None)
# Handle Tuples and Lists
if lenient_issubclass(origin, List):
main_type = main_type.__args__[0]
contains_args = hasattr(main_type, "__args__")

# Handle Optional[SomeType] and SomeType | None
if origin is Union or (origin is None and contains_args):
types = []
for type_ in main_type.__args__:
if type_ is NoneType:
continue
types.append(type_)
assert len(types) == 1, "Typer Currently doesn't support Union types"
main_type = types[0]
origin = getattr(main_type, "__origin__", None)
# Handle Tuples and Lists
if lenient_issubclass(origin, List):
main_type = main_type.__args__[0]
assert not getattr(
main_type, "__origin__", None
), "List types with complex sub-types are not currently supported"
is_list = True
elif lenient_issubclass(origin, Tuple): # type: ignore
types = []
for type_ in main_type.__args__:
assert not getattr(
main_type, "__origin__", None
), "List types with complex sub-types are not currently supported"
is_list = True
elif lenient_issubclass(origin, Tuple): # type: ignore
types = []
for type_ in main_type.__args__:
assert not getattr(
type_, "__origin__", None
), "Tuple types with complex sub-types are not currently supported"
types.append(
get_click_type(annotation=type_, parameter_info=parameter_info)
)
parameter_type = tuple(types)
is_tuple = True
type_, "__origin__", None
), "Tuple types with complex sub-types are not currently supported"
types.append(
get_click_type(annotation=type_, parameter_info=parameter_info)
)
parameter_type = tuple(types)
is_tuple = True
if parameter_type is None:
parameter_type = get_click_type(
annotation=main_type, parameter_info=parameter_info
Expand Down