Repository: omni-us/jsonargparse
Pull request #545 (2024-07-04 04:19:27)

Added support for OrderedDict as a type
## Before submitting

- [x] Did you read the [contributing guideline](https://github.com/omni-us/jsonargparse/blob/main/CONTRIBUTING.rst)?
- [x] Did you update **the documentation**? (readme and public docstrings)
- [x] Did you write **unit tests** such that there is 100% coverage on related code? (required for bug fixes and new features)
- [x] Did you verify that new and existing **tests pass locally**?
- [x] Did you make sure that all changes preserve **backward compatibility**?
- [x] Did you update **the CHANGELOG**? (not for typos, docs, test updates, or minor internal changes/refactors)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=545) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=545&resolved=false&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=545&resolutions=WONTFIX)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=545&resolved=false&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=545&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=545&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=545)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/omni-us/jsonargparse/pull/545?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 100.00%. Comparing base [(`9a64c5c`)](https://app.codecov.io/gh/omni-us/jsonargparse/commit/9a64c5cee3bab9f7040a81e662b565833ce2630a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) to head [(`7c01e5c`)](https://app.codecov.io/gh/omni-us/jsonargparse/commit/7c01e5c0edb3fd617822a67a811a29790cb78bdf?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##              main      #545   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           22        22           
  Lines         6422      6425    +3     
=========================================
+ Hits          6422      6425    +3     
```

| [Flag](https://app.codecov.io/gh/omni-us/jsonargparse/pull/545/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | Coverage Δ | |
|---|---|---|
| [py3.10](https://app.codecov.io/gh/omni-us/jsonargparse/pull/545/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `85.69% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.10_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/545/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.69% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.10_pydantic1](https://app.codecov.io/gh/omni-us/jsonargparse/pull/545/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `48.43% <85.71%> (+<0.01%)` | :arrow_up: |
| [py3.10_pydantic2](https://app.codecov.io/gh/omni-us/jsonargparse/pull/545/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `48.21% <85.71%> (+<0.01%)` | :arrow_up: |
| [py3.10_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/545/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.70% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.11](https://app.codecov.io/gh/omni-us/jsonargparse/pull/545/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `85.67% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.11_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/545/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.69% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.11_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/545/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.70% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.12](https://app.codecov.io/gh/omni-us/jsonargparse/pull/545/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `85.81% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.12_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/545/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.69% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.12_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/545/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.70% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.7](https://app.codecov.io/gh/omni-us/jsonargparse/pull/545/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `86.13% <85.71%> (-0.01%)` | :arrow_down: |
| [py3.7_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/545/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.19% <85.71%> (-0.02%)` | :arrow_down: |
| [py3.7_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/545/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.24% <85.71%> (-0.02%)` | :arrow_down: |
| [py3.8](https://app.codecov.io/gh/omni-us/jsonargparse/pull/545/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `86.32% <85.71%> (-0.01%)` | :arrow_down: |
| [py3.8_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/545/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.36% <85.71%> (-0.02%)` | :arrow_down: |
| [py3.8_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/545/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.39% <85.71%> (-0.02%)` | :arrow_down: |
| [py3.9](https://app.codecov.io/gh/omni-us/jsonargparse/pull/545/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `86.21% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.9_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/545/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.28% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.9_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/545/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.29% <100.00%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/omni-us/jsonargparse/pull/545?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us).


Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index dd90dc4..393b404 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -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>`__,
diff --git a/DOCUMENTATION.rst b/DOCUMENTATION.rst
index c9e7699..dfb1bde 100644
--- a/DOCUMENTATION.rst
+++ b/DOCUMENTATION.rst
@@ -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
diff --git a/jsonargparse/_namespace.py b/jsonargparse/_namespace.py
index a5b594b..a760dc5 100644
--- a/jsonargparse/_namespace.py
+++ b/jsonargparse/_namespace.py
@@ -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,
@@ -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:
diff --git a/jsonargparse/_typehints.py b/jsonargparse/_typehints.py
index 6016382..30c3ca6 100644
--- a/jsonargparse/_typehints.py
+++ b/jsonargparse/_typehints.py
@@ -4,7 +4,7 @@ import inspect
 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
@@ -120,6 +120,7 @@ root_types = {
     MutableMapping,
     abc.Mapping,
     abc.MutableMapping,
+    OrderedDict,
     Callable,
     abc.Callable,
 }
@@ -145,7 +146,16 @@ sequence_origin_types = {
     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}
@@ -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:


Test patch:
diff --git a/jsonargparse_tests/test_typehints.py b/jsonargparse_tests/test_typehints.py
index dd107a3..4183bf6 100644
--- a/jsonargparse_tests/test_typehints.py
+++ b/jsonargparse_tests/test_typehints.py
@@ -6,6 +6,7 @@ import sys
 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
@@ -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