diff --git a/dcargs/_fields.py b/dcargs/_fields.py index 896e8f4f..4032f2f6 100644 --- a/dcargs/_fields.py +++ b/dcargs/_fields.py @@ -198,7 +198,7 @@ def _try_field_list_from_callable( container_fields = _try_field_list_from_sequence( contained_type, default_instance ) - elif f_origin is dict: + elif f_origin is dict or cls is dict: container_fields = _try_field_list_from_dict(f, default_instance) # Check if one of the container types matched. diff --git a/dcargs/_resolver.py b/dcargs/_resolver.py index 8d7e69ad..f6598cf9 100644 --- a/dcargs/_resolver.py +++ b/dcargs/_resolver.py @@ -103,7 +103,7 @@ def narrow_type(typ: TypeT, default_instance: Any) -> TypeT: try: potential_subclass = type(default_instance) superclass = typ - if issubclass(potential_subclass, superclass): # type: ignore + if superclass is Any or issubclass(potential_subclass, superclass): # type: ignore return cast(TypeT, potential_subclass) except TypeError: pass diff --git a/pyproject.toml b/pyproject.toml index dbb57d57..4626b66f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "dcargs" -version = "0.2.2" +version = "0.2.3" description = "Strongly typed, zero-effort CLI interfaces" authors = ["brentyi "] include = ["./dcargs/**/*"] diff --git a/tests/test_collections.py b/tests/test_collections.py index c60c47d4..b6970cbf 100644 --- a/tests/test_collections.py +++ b/tests/test_collections.py @@ -1,7 +1,18 @@ import collections import dataclasses import enum -from typing import Any, Deque, FrozenSet, List, Optional, Sequence, Set, Tuple, Union +from typing import ( + Any, + Deque, + Dict, + FrozenSet, + List, + Optional, + Sequence, + Set, + Tuple, + Union, +) import pytest from typing_extensions import Literal @@ -382,3 +393,31 @@ def main( ] with pytest.raises(SystemExit): dcargs.cli(main, args=["--help"]) + + +def test_dict_no_annotation(): + def main(x: Dict[str, Any] = {"int": 5, "str": "5"}): + return x + + assert dcargs.cli(main, args=[]) == {"int": 5, "str": "5"} + assert dcargs.cli(main, args="--x.int 3 --x.str 7".split(" ")) == { + "int": 3, + "str": "7", + } + + +def test_double_dict_no_annotation(): + def main( + x: Dict[str, Any] = { + "wow": {"int": 5, "str": "5"}, + } + ): + return x + + assert dcargs.cli(main, args=[]) == {"wow": {"int": 5, "str": "5"}} + assert dcargs.cli(main, args="--x.wow.int 3 --x.wow.str 7".split(" ")) == { + "wow": { + "int": 3, + "str": "7", + } + } diff --git a/tests/test_helptext.py b/tests/test_helptext.py index 929e2c9a..2160c40b 100644 --- a/tests/test_helptext.py +++ b/tests/test_helptext.py @@ -515,7 +515,6 @@ class Something( def test_unparsable(): - @dataclasses.dataclass class Struct: a: int = 5 b: str = "7" diff --git a/tests/test_is_nested_type.py b/tests/test_is_nested_type.py new file mode 100644 index 00000000..6960d9a9 --- /dev/null +++ b/tests/test_is_nested_type.py @@ -0,0 +1,52 @@ +import dataclasses +import pathlib +from typing import Any, Dict, List, Tuple + +from dcargs._fields import MISSING_NONPROP, is_nested_type + + +def test_is_nested_type_simple(): + assert not is_nested_type(int, MISSING_NONPROP) + assert not is_nested_type(bool, MISSING_NONPROP) + assert not is_nested_type(str, MISSING_NONPROP) + assert not is_nested_type(pathlib.Path, MISSING_NONPROP) + + +def test_is_nested_type_containers(): + assert not is_nested_type(List[int], MISSING_NONPROP) + assert not is_nested_type(List[bool], MISSING_NONPROP) + assert not is_nested_type(List[str], MISSING_NONPROP) + assert not is_nested_type(List[pathlib.Path], MISSING_NONPROP) + + +@dataclasses.dataclass +class Color: + r: int + g: int + b: int + + +def test_is_nested_type_actually_nested(): + assert is_nested_type(Color, Color(255, 0, 0)) + + +def test_is_nested_type_actually_nested_narrowing(): + assert is_nested_type(Any, Color(255, 0, 0)) + assert is_nested_type(object, Color(255, 0, 0)) + assert not is_nested_type(int, Color(255, 0, 0)) + + +def test_is_nested_type_actually_nested_in_container(): + assert is_nested_type(Tuple[Color, Color], MISSING_NONPROP) + assert is_nested_type(Tuple[object, ...], (Color(255, 0, 0),)) + assert is_nested_type(Tuple[Any, ...], (Color(255, 0, 0),)) + assert is_nested_type(tuple, (Color(255, 0, 0),)) + assert not is_nested_type(tuple, (1, 2, 3)) + assert is_nested_type(tuple, (1, Color(255, 0, 0), 3)) + assert is_nested_type(List[Any], [Color(255, 0, 0)]) + + +def test_nested_dict(): + assert is_nested_type(Dict[str, int], {"x": 5}) + assert is_nested_type(dict, {"x": 5}) + assert is_nested_type(Any, {"x": 5}) diff --git a/tests/test_nested_in_containers.py b/tests/test_nested_in_containers.py index c914ef35..56c3dfde 100644 --- a/tests/test_nested_in_containers.py +++ b/tests/test_nested_in_containers.py @@ -125,6 +125,14 @@ def main(x: List[object] = [Color(255, 0, 0)]) -> Any: assert dcargs.cli(main, args="--x.0.r 127".split(" ")) == [Color(127, 0, 0)] +def test_list_any(): + def main(x: List[Any] = [Color(255, 0, 0)]) -> Any: + return x + + assert dcargs.cli(main, args=[]) == [Color(255, 0, 0)] + assert dcargs.cli(main, args="--x.0.r 127".split(" ")) == [Color(127, 0, 0)] + + def test_tuple_in_list(): def main(x: List[Tuple[Color]] = [(Color(255, 0, 0),)]) -> Any: return x @@ -320,3 +328,46 @@ def main( main, args="--x.int.g 0".split(" "), ) == {"float": GenericColor(0.5, 0.2, 0.3), "int": GenericColor(25, 0, 3)} + + +def test_generic_in_double_nested_dict_with_default(): + ScalarType = TypeVar("ScalarType", int, float) + + @dataclasses.dataclass + class GenericColor(Generic[ScalarType]): + r: ScalarType + g: ScalarType + b: ScalarType + + def main( + x: Dict[str, Dict[str, GenericColor]] = { + "hello": { + "float": GenericColor(0.5, 0.2, 0.3), + "int": GenericColor[int](25, 2, 3), + } + } + ) -> Any: + return x + + assert dcargs.cli(main, args="--x.hello.float.g 0.1".split(" "),)["hello"][ + "float" + ] == GenericColor(0.5, 0.1, 0.3) + assert dcargs.cli(main, args="--x.hello.int.g 0".split(" "),) == { + "hello": {"float": GenericColor(0.5, 0.2, 0.3), "int": GenericColor(25, 0, 3)} + } + + +def test_double_nested_dict_with_inferred_type(): + def main( + x: Dict[str, Any] = { + "hello": { + "a": Color(5, 2, 3), + "b": Color(25, 2, 3), + } + } + ) -> Any: + return x + + assert dcargs.cli(main, args="--x.hello.a.g 1".split(" "),)["hello"][ + "a" + ] == Color(5, 1, 3)