Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Added
^^^^^
- Support for ``MappingProxyType`` as a type and as default for mapping types
(`#540 <https://github.com/omni-us/jsonargparse/pull/540>`__).
- Support for ``OrderedDict`` as a type.
- New function ``get_loader`` to get the current loader for a given parser mode
(`#479 comment
<https://github.com/omni-us/jsonargparse/issues/479#issuecomment-2022596544>`__,
Expand Down
6 changes: 3 additions & 3 deletions DOCUMENTATION.rst
Original file line number Diff line number Diff line change
Expand Up @@ -400,9 +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``, ``MappingProxyType``, and
``TypedDict`` are supported but only with ``str`` or ``int`` keys. For more
details see :ref:`dict-items`.
- ``Dict``, ``Mapping``, ``MutableMapping``, ``MappingProxyType``,
``OrderedDict`` 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
Expand Down
3 changes: 2 additions & 1 deletion jsonargparse/_namespace.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Classes and functions related to namespace objects."""

import argparse
from collections import OrderedDict
from contextlib import contextmanager
from typing import (
Any,
Expand Down Expand Up @@ -72,7 +73,7 @@ def strip_meta(cfg):

def recreate_branches(data, skip_keys=None):
new_data = data
if isinstance(data, (Namespace, dict)):
if isinstance(data, (Namespace, dict)) and not isinstance(data, OrderedDict):
new_data = type(data)()
for key, val in getattr(data, "__dict__", data).items():
if skip_keys is None or key not in skip_keys:
Expand Down
18 changes: 15 additions & 3 deletions jsonargparse/_typehints.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import os
import re
from argparse import ArgumentError
from collections import abc, defaultdict
from collections import OrderedDict, abc, defaultdict
from contextlib import contextmanager, suppress
from contextvars import ContextVar
from copy import deepcopy
Expand Down Expand Up @@ -120,6 +120,7 @@
MutableMapping,
abc.Mapping,
abc.MutableMapping,
OrderedDict,
Callable,
abc.Callable,
}
Expand All @@ -145,7 +146,16 @@
abc.Sequence,
abc.MutableSequence,
}
mapping_origin_types = {Dict, dict, Mapping, MappingProxyType, MutableMapping, abc.Mapping, abc.MutableMapping}
mapping_origin_types = {
Dict,
dict,
Mapping,
MappingProxyType,
MutableMapping,
abc.Mapping,
abc.MutableMapping,
OrderedDict,
}
callable_origin_types = {Callable, abc.Callable}

literal_types = {Literal}
Expand Down Expand Up @@ -904,8 +914,10 @@ 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:
if typehint_origin is MappingProxyType and not serialize:
val = MappingProxyType(val)
elif typehint_origin is OrderedDict:
val = dict(val) if serialize else OrderedDict(val)

# Callable
elif typehint_origin in callable_origin_types or typehint in callable_origin_types:
Expand Down
21 changes: 21 additions & 0 deletions jsonargparse_tests/test_typehints.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import time
import uuid
from calendar import Calendar, TextCalendar
from collections import OrderedDict
from enum import Enum
from pathlib import Path
from types import MappingProxyType
Expand Down Expand Up @@ -563,6 +564,26 @@ def test_mapping_default_mapping_proxy_type(parser):
assert parser.dump(cfg, format="json") == '{"mapping":{"x":1}}'


@pytest.mark.skipif(sys.version_info < (3, 9), reason="OrderedDict subscriptable since python 3.9")
def test_ordered_dict(parser):
parser.add_argument("--odict", type=eval("OrderedDict[str, int]"))
cfg = parser.parse_args(['--odict={"a":1, "b":2}'])
assert isinstance(cfg.odict, OrderedDict)
assert OrderedDict([("a", 1), ("b", 2)]) == cfg.odict
with pytest.raises(ArgumentError) as ctx:
parser.parse_args(['--odict={"x":"-"}'])
ctx.match("Expected a <class 'int'>")
assert parser.dump(cfg, format="json") == '{"odict":{"a":1,"b":2}}'
assert parser.dump(cfg, format="yaml") == "odict:\n a: 1\n b: 2\n"


def test_dict_default_ordered_dict(parser):
parser.add_argument("--dict", type=dict, default=OrderedDict({"a": 1}))
defaults = parser.get_defaults()
assert isinstance(defaults.dict, OrderedDict)
assert defaults.dict == {"a": 1}


# union tests


Expand Down