diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 823c915b..5e56ce29 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,6 +15,11 @@ paths are considered internals and can change in minor and patch releases. v4.49.0 (unreleased) -------------------- +Added +^^^^^ +- Support ``Deque`` and ``FrozenSet`` in type hints (`#905 + `__). + Changed ^^^^^^^ - Docs now reference methods via the public ``ArgumentParser`` class instead of diff --git a/DOCUMENTATION.rst b/DOCUMENTATION.rst index 85ab55ef..b4404870 100644 --- a/DOCUMENTATION.rst +++ b/DOCUMENTATION.rst @@ -455,11 +455,11 @@ Some notes about this support are: - Fully supported types are: ``str``, ``bool`` (more details in :ref:`boolean-arguments`), ``int``, ``float``, ``Decimal``, ``complex``, ``bytes``/``bytearray`` (Base64 encoding), ``range``, ``list`` (more details - in :ref:`list-append`), ``Iterable``, ``Sequence``, ``Any``, ``Union``, - ``Optional``, ``Type``, ``Enum``, ``PathLike``, ``UUID``, ``timedelta``, - restricted types as explained in sections :ref:`restricted-numbers` and - :ref:`restricted-strings` and path and URL types as explained in sections - :ref:`parsing-paths` and :ref:`parsing-urls`. + in :ref:`list-append`), ``Deque``, ``Iterable``, ``Sequence``, ``Any``, + ``Union``, ``Optional``, ``Type``, ``Enum``, ``PathLike``, ``UUID``, + ``timedelta``, restricted types as explained in sections + :ref:`restricted-numbers` and :ref:`restricted-strings` and path and URL types + as explained in sections :ref:`parsing-paths` and :ref:`parsing-urls`. - ``dict``, ``Mapping``, ``MutableMapping``, ``MappingProxyType``, ``OrderedDict``, and ``TypedDict`` are supported but only with ``str`` or @@ -469,12 +469,12 @@ Some notes about this support are: typing as described in PEP `692 `__. For more details see :ref:`dict-items`. -- ``tuple``, ``set`` and ``MutableSet`` are supported even though they can't be - represented in JSON distinguishable from a list. Each ``tuple`` element - position can have its own type and will be validated as such. ``tuple`` with - ellipsis (``tuple[type, ...]``) is also supported. In command line arguments, - config files and environment variables, tuples and sets are represented as an - array. +- ``tuple``, ``set``, ``frozenset`` and ``MutableSet`` are supported even though + they can't be represented in JSON distinguishable from a list. Each ``tuple`` + element position can have its own type and will be validated as such. + ``tuple`` with ellipsis (``tuple[type, ...]``) is also supported. In command + line arguments, config files and environment variables, tuples and sets are + represented as an array. - To set a value to ``None`` it is required to use ``null`` since this is how JSON/YAML defines it. To avoid confusion in the help, ``NoneType`` is diff --git a/jsonargparse/_typehints.py b/jsonargparse/_typehints.py index e886f73b..7b4bc2d6 100644 --- a/jsonargparse/_typehints.py +++ b/jsonargparse/_typehints.py @@ -5,7 +5,7 @@ import re import sys from argparse import ArgumentError -from collections import OrderedDict, abc, defaultdict +from collections import OrderedDict, abc, defaultdict, deque from contextlib import contextmanager, suppress from contextvars import ContextVar from copy import deepcopy @@ -16,8 +16,10 @@ from typing import ( Any, Callable, + Deque, Dict, ForwardRef, + FrozenSet, Iterable, List, Literal, @@ -119,6 +121,9 @@ def _capture_typing_extension_shadows(name: str, *collections) -> None: Union, List, list, + FrozenSet, + Deque, + deque, Iterable, Sequence, MutableSequence, @@ -128,6 +133,7 @@ def _capture_typing_extension_shadows(name: str, *collections) -> None: Tuple, tuple, Set, + FrozenSet, set, frozenset, MutableSet, @@ -160,6 +166,8 @@ def _capture_typing_extension_shadows(name: str, *collections) -> None: sequence_origin_types = { List, list, + Deque, + deque, Iterable, Sequence, MutableSequence, @@ -899,7 +907,7 @@ def adapt_typehints( # Tuple or Set elif typehint_origin in tuple_set_origin_types: - if not isinstance(val, (list, tuple, set)): + if not isinstance(val, (list, tuple, set, frozenset)): raise_unexpected_value(f"Expected a {typehint_origin}", val) val = list(val) if subtypehints is not None: @@ -911,7 +919,12 @@ def adapt_typehints( subtypehint = subtypehints[0 if is_ellipsis or not is_tuple else n] val[n] = adapt_typehints(v, subtypehint, **adapt_kwargs) if not serialize: - val = tuple(val) if typehint_origin in {Tuple, tuple} else set(val) + if typehint_origin in {Tuple, tuple}: + val = tuple(val) + elif typehint_origin is frozenset: + val = frozenset(val) + else: + val = set(val) # List, Iterable or Sequence elif typehint_origin in sequence_origin_types: @@ -950,6 +963,8 @@ def adapt_typehints( adapt_kwargs_n = deepcopy(adapt_kwargs) with change_to_path_dir(list_path): val[n] = adapt_typehints(v, subtypehints[0], **adapt_kwargs_n) + if typehint_origin is deque: + val = list(val) if serialize else deque(val) # Dict, Mapping elif typehint_origin in mapping_origin_types: diff --git a/jsonargparse_tests/test_typehints.py b/jsonargparse_tests/test_typehints.py index 1fcccc18..505f6f3d 100644 --- a/jsonargparse_tests/test_typehints.py +++ b/jsonargparse_tests/test_typehints.py @@ -7,7 +7,7 @@ import time import uuid from calendar import Calendar, TextCalendar -from collections import OrderedDict +from collections import OrderedDict, deque from dataclasses import dataclass, field from enum import Enum from pathlib import Path @@ -15,7 +15,9 @@ from typing import ( Any, Callable, + Deque, Dict, + FrozenSet, Iterable, List, Literal, @@ -312,6 +314,16 @@ def test_set(parser): ctx.match("Expected a ") +def test_frozenset(parser): + parser.add_argument("--frozen", type=FrozenSet[int]) + cfg = parser.parse_args(["--frozen=[1, 2]"]) + assert frozenset([1, 2]) == cfg.frozen + assert parser.dump(cfg, format="json") == '{"frozen":[1,2]}' + with pytest.raises(ArgumentError) as ctx: + parser.parse_args(['--frozen=["a", "b"]']) + ctx.match("Expected a ") + + # tuple tests @@ -373,6 +385,14 @@ def test_list_variants(parser, list_type): assert [1, 2] == cfg.list +def test_deque(parser): + parser.add_argument("--deque", type=Deque[int]) + cfg = parser.parse_args(["--deque=[1, 2]"]) + assert isinstance(cfg.deque, deque) + assert deque([1, 2]) == cfg.deque + assert parser.dump(cfg, format="json") == '{"deque":[1,2]}' + + def test_list_dump(parser): parser.add_argument("--list", type=Union[PositiveInt, List[PositiveInt]]) dump = json_or_yaml_load(parser.dump(Namespace(list=[1, 2])))