diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 99a48452..dcb418c3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,6 +12,15 @@ The semantic versioning only considers the public API as described in paths are considered internals and can change in minor and patch releases. +v4.32.0 (2024-07-??) +-------------------- + +Added +^^^^^ +- Support for ``MappingProxyType`` as a type and as default for mapping types + (`#540 `__). + + v4.31.0 (2024-06-27) -------------------- diff --git a/DOCUMENTATION.rst b/DOCUMENTATION.rst index cc56c138..c9e7699c 100644 --- a/DOCUMENTATION.rst +++ b/DOCUMENTATION.rst @@ -400,8 +400,9 @@ Some notes about this support are: :ref:`restricted-strings` and paths and URLs as explained in sections :ref:`parsing-paths` and :ref:`parsing-urls`. -- ``Dict``, ``Mapping``, ``MutableMapping``, and ``TypedDict`` are supported but - only with ``str`` or ``int`` keys. For more details see :ref:`dict-items`. +- ``Dict``, ``Mapping``, ``MutableMapping``, ``MappingProxyType``, and + ``TypedDict`` are supported but only with ``str`` or ``int`` keys. 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 diff --git a/jsonargparse/_typehints.py b/jsonargparse/_typehints.py index 5d257099..60163820 100644 --- a/jsonargparse/_typehints.py +++ b/jsonargparse/_typehints.py @@ -10,7 +10,7 @@ from copy import deepcopy from enum import Enum from functools import partial -from types import FunctionType +from types import FunctionType, MappingProxyType from typing import ( Any, Callable, @@ -145,7 +145,7 @@ abc.Sequence, abc.MutableSequence, } -mapping_origin_types = {Dict, dict, Mapping, MutableMapping, abc.Mapping, abc.MutableMapping} +mapping_origin_types = {Dict, dict, Mapping, MappingProxyType, MutableMapping, abc.Mapping, abc.MutableMapping} callable_origin_types = {Callable, abc.Callable} literal_types = {Literal} @@ -872,6 +872,8 @@ def adapt_typehints( val = {**prev_val, val.key: val.val} else: val = {val.key: val.val} + elif isinstance(val, MappingProxyType): + val = dict(val) elif not isinstance(val, dict): raise_unexpected_value(f"Expected a {typehint_origin}", val) if subtypehints is not None: @@ -902,6 +904,8 @@ def adapt_typehints( raise_unexpected_value(f"Unexpected keys: {extra_keys}", val) for k, v in val.items(): val[k] = adapt_typehints(v, typehint.__annotations__[k], **adapt_kwargs) + if typehint_origin == MappingProxyType and not serialize: + val = MappingProxyType(val) # Callable elif typehint_origin in callable_origin_types or typehint in callable_origin_types: diff --git a/jsonargparse_tests/test_typehints.py b/jsonargparse_tests/test_typehints.py index 42100836..dd107a3c 100644 --- a/jsonargparse_tests/test_typehints.py +++ b/jsonargparse_tests/test_typehints.py @@ -8,6 +8,7 @@ from calendar import Calendar, TextCalendar from enum import Enum from pathlib import Path +from types import MappingProxyType from typing import ( Any, Callable, @@ -545,6 +546,23 @@ def test_typeddict_with_args_ntotal(parser): ctx.match("Expected a ") +def test_mapping_proxy_type(parser): + parser.add_argument("--mapping", type=MappingProxyType) + cfg = parser.parse_args(['--mapping={"x":1}']) + assert isinstance(cfg.mapping, MappingProxyType) + assert cfg.mapping == {"x": 1} + assert parser.dump(cfg, format="json") == '{"mapping":{"x":1}}' + + +def test_mapping_default_mapping_proxy_type(parser): + mapping_proxy = MappingProxyType({"x": 1}) + parser.add_argument("--mapping", type=Mapping[str, int], default=mapping_proxy) + cfg = parser.parse_args([]) + assert isinstance(cfg.mapping, Mapping) + assert mapping_proxy == cfg.mapping + assert parser.dump(cfg, format="json") == '{"mapping":{"x":1}}' + + # union tests