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
6 changes: 4 additions & 2 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ Fixed
^^^^^
- ``set_parsing_settings(validate_defaults=True)`` fails when the parser has a
config action (`#718 <https://github.com/omni-us/jsonargparse/pull/718>`__).
- Regression causing dump/save to fail when ``skip_link_targets=True`` and target
being an entire required dataclass (`#717
- Regression causing dump/save to fail when ``skip_link_targets=True`` and
target being an entire required dataclass (`#717
<https://github.com/omni-us/jsonargparse/pull/717>`__).
- ``TypedDict`` values not validated when types are forward references (`#722
<https://github.com/omni-us/jsonargparse/pull/722>`__).

Changed
^^^^^^^
Expand Down
19 changes: 18 additions & 1 deletion jsonargparse/_typehints.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@
from copy import deepcopy
from enum import Enum
from functools import partial
from importlib import import_module
from types import FunctionType, MappingProxyType
from typing import (
Any,
Callable,
Dict,
ForwardRef,
Iterable,
List,
Literal,
Expand Down Expand Up @@ -719,6 +721,15 @@ def raise_union_unexpected_value(subtypes, val: Any, exceptions: List[Exception]
) from exceptions[0]


def resolve_forward_ref(ref):
if not isinstance(ref, ForwardRef) or not ref.__forward_module__:
return ref

aliases = __builtins__.copy()
aliases.update(vars(import_module(ref.__forward_module__)))
return aliases.get(ref.__forward_arg__, ref)


def adapt_typehints(
val,
typehint,
Expand Down Expand Up @@ -954,7 +965,8 @@ def adapt_typehints(
if extra_keys:
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)
subtypehint = resolve_forward_ref(typehint.__annotations__[k])
val[k] = adapt_typehints(v, subtypehint, **adapt_kwargs)
if typehint_origin is MappingProxyType and not serialize:
val = MappingProxyType(val)
elif typehint_origin is OrderedDict:
Expand Down Expand Up @@ -1100,6 +1112,11 @@ def adapt_typehints(
elif is_alias_type(typehint):
return adapt_typehints(val, get_alias_target(typehint), **adapt_kwargs)

else:
if str(typehint) == "+VT_co":
return val # required for typing.Mapping in python 3.8
raise RuntimeError(f"The code should never reach here: typehint={typehint}") # pragma: no cover

return val


Expand Down
6 changes: 4 additions & 2 deletions jsonargparse_tests/test_typehints.py
Original file line number Diff line number Diff line change
Expand Up @@ -753,8 +753,7 @@ def test_invalid_inherited_unpack_typeddict(parser, init_args):
parser.parse_args([f"--testclass={json.dumps(test_config)}"])


@pytest.mark.skipif(sys.version_info < (3, 9), reason="Python 3.8 lacked runtime inspection of TypedDict required keys")
def test_typeddict_totality_inheritance(parser):
if sys.version_info >= (3, 9):

class BottomDict(TypedDict, total=True):
a: int
Expand All @@ -765,6 +764,9 @@ class MiddleDict(BottomDict, total=False):
class TopDict(MiddleDict, total=True):
c: int


@pytest.mark.skipif(sys.version_info < (3, 9), reason="Python 3.8 lacked runtime inspection of TypedDict required keys")
def test_typeddict_totality_inheritance(parser):
parser.add_argument("--middledict", type=MiddleDict, required=False)
parser.add_argument("--topdict", type=TopDict, required=False)
assert {"a": 1} == parser.parse_args(['--middledict={"a": 1}'])["middledict"]
Expand Down