Skip to content

Commit 0a7852c

Browse files
committed
feat: add support for python 3.10+ optional types
1 parent b153a1b commit 0a7852c

2 files changed

Lines changed: 52 additions & 2 deletions

File tree

src/py_app_dev/core/cmd_line.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import dataclasses
2+
import types
23
from abc import ABC, abstractmethod
34
from argparse import ArgumentError, ArgumentParser, Namespace
45
from typing import Any, Union, get_args
@@ -87,7 +88,13 @@ def add_command(self, command: Command) -> "CommandLineHandlerBuilder":
8788

8889

8990
def is_type_optional(some_type: Any) -> bool:
90-
return hasattr(some_type, "__origin__") and some_type.__origin__ is Union and type(None) in some_type.__args__
91+
# Handle old typing.Union syntax (Optional[T] or Union[T, None])
92+
if hasattr(some_type, "__origin__") and some_type.__origin__ is Union and type(None) in some_type.__args__:
93+
return True
94+
# Handle new union syntax (T | None) - Python 3.10+
95+
if isinstance(some_type, types.UnionType) and type(None) in some_type.__args__:
96+
return True
97+
return False
9198

9299

93100
def is_type_list(some_type: Any) -> bool:

tests/test_cmd_line.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def test_duplicate_commands():
7474
class MyConfigDataclass:
7575
my_first_arg: Path = field(metadata={"help": "Some help for arg1."})
7676
arg: str = field(default="value1", metadata={"help": "Some help for arg1."})
77-
opt_arg: str | None = field(default=None, metadata={"help": "Some help for arg1."})
77+
opt_arg: Optional[str] = field(default=None, metadata={"help": "Some help for arg1."})
7878
opt_arg_bool: bool | None = field(
7979
default=False,
8080
metadata={
@@ -88,6 +88,10 @@ def test_is_type_optional():
8888
assert is_type_optional(Optional[str])
8989
assert not is_type_optional(str)
9090
assert is_type_optional(Union[Path | None, str])
91+
# Test modern union syntax (Python 3.10+)
92+
assert is_type_optional(Path | None)
93+
assert is_type_optional(str | None)
94+
assert not is_type_optional(Path | str) # Union without None should not be optional
9195
assert not is_type_optional(Union[Path, str])
9296

9397

@@ -138,6 +142,45 @@ def test_register_optional_path_arguments():
138142
}
139143

140144

145+
@dataclass
146+
class ClassWithModernUnionTypes:
147+
"""Test class with various modern union syntax optional fields."""
148+
149+
required_path: Path = field(metadata={"help": "Required path."})
150+
optional_path: Path | None = field(default=None, metadata={"help": "Optional path."})
151+
optional_str: str | None = field(default=None, metadata={"help": "Optional string."})
152+
optional_int: int | None = field(default=None, metadata={"help": "Optional integer."})
153+
optional_bool: bool | None = field(default=None, metadata={"help": "Optional boolean."})
154+
155+
156+
def test_register_modern_union_optional_arguments():
157+
"""Test that modern union syntax (T | None) works correctly."""
158+
parser = ArgumentParser()
159+
register_arguments_for_config_dataclass(parser, ClassWithModernUnionTypes)
160+
161+
# Test with all arguments provided
162+
args = parser.parse_args(
163+
["--required-path", "required/path", "--optional-path", "optional/path", "--optional-str", "test_string", "--optional-int", "42", "--optional-bool", "true"]
164+
)
165+
assert vars(args) == {
166+
"required_path": Path("required/path"),
167+
"optional_path": Path("optional/path"),
168+
"optional_str": "test_string",
169+
"optional_int": 42,
170+
"optional_bool": True,
171+
}
172+
173+
# Test with only required arguments
174+
args = parser.parse_args(["--required-path", "required/path"])
175+
assert vars(args) == {
176+
"required_path": Path("required/path"),
177+
"optional_path": None,
178+
"optional_str": None,
179+
"optional_int": None,
180+
"optional_bool": None,
181+
}
182+
183+
141184
@dataclass
142185
class ClassWithListArgument:
143186
my_paths: list[Path] = field(metadata={"help": "List of paths"})

0 commit comments

Comments
 (0)