Repository: omni-us/jsonargparse
Pull request #386 (2023-09-18 20:37:23)

Fix postponed annotations not working for dataclasses
## Before submitting

- [x] Did you read the [contributing guideline](https://github.com/omni-us/jsonargparse/blob/main/CONTRIBUTING.rst)?
- [n/a] 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]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=386)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=386&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=386&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=386&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=386&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=386&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=386&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=386&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=386&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=386&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=386&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=386&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=386&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=386) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=386&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=386&metric=new_duplicated_lines_density&view=list)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/omni-us/jsonargparse/pull/386?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) Report
Patch coverage: **`100.00%`** and no project coverage change.
> Comparison is base [(`1d1b917`)](https://app.codecov.io/gh/omni-us/jsonargparse/commit/1d1b917052697c1824651a36c27caa92024b3aee?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) 100.00% compared to head [(`d2c3d06`)](https://app.codecov.io/gh/omni-us/jsonargparse/pull/386?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) 100.00%.

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


```diff
@@            Coverage Diff            @@
##              main      #386   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           21        21           
  Lines         5729      5734    +5     
=========================================
+ Hits          5729      5734    +5     
```

| [Flag](https://app.codecov.io/gh/omni-us/jsonargparse/pull/386/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/386/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `87.68% <87.50%> (-0.03%)` | :arrow_down: |
| [py3.10_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/386/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.95% <87.50%> (-0.04%)` | :arrow_down: |
| [py3.10_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/386/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.95% <87.50%> (-0.04%)` | :arrow_down: |
| [py3.11](https://app.codecov.io/gh/omni-us/jsonargparse/pull/386/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `87.67% <87.50%> (-0.03%)` | :arrow_down: |
| [py3.11_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/386/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.93% <87.50%> (-0.04%)` | :arrow_down: |
| [py3.11_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/386/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.93% <87.50%> (-0.04%)` | :arrow_down: |
| [py3.7](https://app.codecov.io/gh/omni-us/jsonargparse/pull/386/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `88.26% <93.75%> (+0.01%)` | :arrow_up: |
| [py3.7_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/386/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.68% <93.75%> (+<0.01%)` | :arrow_up: |
| [py3.7_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/386/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.66% <93.75%> (+<0.01%)` | :arrow_up: |
| [py3.8](https://app.codecov.io/gh/omni-us/jsonargparse/pull/386/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `88.41% <100.00%> (+0.01%)` | :arrow_up: |
| [py3.8_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/386/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.73% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.8_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/386/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.72% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.9](https://app.codecov.io/gh/omni-us/jsonargparse/pull/386/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `88.26% <93.75%> (-0.01%)` | :arrow_down: |
| [py3.9_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/386/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.61% <93.75%> (-0.02%)` | :arrow_down: |
| [py3.9_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/386/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.61% <93.75%> (-0.02%)` | :arrow_down: |

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.

| [Files Changed](https://app.codecov.io/gh/omni-us/jsonargparse/pull/386?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | Coverage Δ | |
|---|---|---|
| [jsonargparse/\_parameter\_resolvers.py](https://app.codecov.io/gh/omni-us/jsonargparse/pull/386?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us#diff-anNvbmFyZ3BhcnNlL19wYXJhbWV0ZXJfcmVzb2x2ZXJzLnB5) | `100.00% <100.00%> (ø)` | |
| [jsonargparse/\_postponed\_annotations.py](https://app.codecov.io/gh/omni-us/jsonargparse/pull/386?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us#diff-anNvbmFyZ3BhcnNlL19wb3N0cG9uZWRfYW5ub3RhdGlvbnMucHk=) | `100.00% <100.00%> (ø)` | |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/omni-us/jsonargparse/pull/386?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 a57a2d1..3e4a477 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -20,6 +20,7 @@ Fixed
 - ``--print_config`` fails when parser has shallow links.
 - Argument links unnecessarily applied when ``--print_config`` used and parser
   has subcommands (`#311 <https://github.com/omni-us/jsonargparse/issue/311>`__).
+- Postponed annotations not working for dataclasses.
 
 Changed
 ^^^^^^^
diff --git a/jsonargparse/_parameter_resolvers.py b/jsonargparse/_parameter_resolvers.py
index 401138e..a2df528 100644
--- a/jsonargparse/_parameter_resolvers.py
+++ b/jsonargparse/_parameter_resolvers.py
@@ -281,7 +281,7 @@ def get_signature_parameters_and_indexes(component, parent, logger):
             component=component,
             **{a: getattr(param, a) for a in parameter_attributes},
         )
-    evaluate_postponed_annotations(params, signature_source, logger)
+    evaluate_postponed_annotations(params, signature_source, parent, logger)
     stubs = get_stub_types(params, signature_source, parent, logger)
     return params, args_idx, kwargs_idx, doc_params, stubs
 
@@ -831,7 +831,7 @@ def get_parameters_from_pydantic_or_attrs(
                 component=function_or_class,
             )
         )
-    evaluate_postponed_annotations(params, function_or_class, logger)
+    evaluate_postponed_annotations(params, function_or_class, None, logger)
     return params
 
 
diff --git a/jsonargparse/_postponed_annotations.py b/jsonargparse/_postponed_annotations.py
index 963a361..aff8924 100644
--- a/jsonargparse/_postponed_annotations.py
+++ b/jsonargparse/_postponed_annotations.py
@@ -5,6 +5,7 @@ import sys
 import textwrap
 from collections import namedtuple
 from copy import deepcopy
+from dataclasses import is_dataclass
 from importlib import import_module
 from typing import Any, Dict, ForwardRef, FrozenSet, List, Optional, Set, Tuple, Type, Union, get_type_hints
 
@@ -268,7 +269,7 @@ def get_types(obj: Any, logger: Optional[logging.Logger] = None) -> dict:
         tree = ast.parse(source)
         assert isinstance(tree, ast.Module) and len(tree.body) == 1
         node = tree.body[0]
-        assert isinstance(node, ast.FunctionDef)
+        assert isinstance(node, (ast.FunctionDef, ast.ClassDef))
     except Exception as ex2:
         if isinstance(types, Exception):
             if logger:
@@ -287,14 +288,18 @@ def get_types(obj: Any, logger: Optional[logging.Logger] = None) -> dict:
     if "TYPE_CHECKING" in module_source:
         TypeCheckingVisitor().update_aliases(module_source, obj.__module__, aliases, logger)
 
-    for arg_ast in node.args.args + node.args.kwonlyargs:
-        name = arg_ast.arg
-        if arg_ast.annotation and (name not in types or type_requires_eval(types[name])):
+    if isinstance(node, ast.FunctionDef):
+        arg_asts = [(a.arg, a.annotation) for a in node.args.args + node.args.kwonlyargs]
+    else:
+        arg_asts = [(a.target.id, a.annotation) for a in node.body if isinstance(a, ast.AnnAssign)]  # type: ignore
+
+    for name, annotation in arg_asts:
+        if annotation and (name not in types or type_requires_eval(types[name])):
             try:
-                if isinstance(arg_ast.annotation, ast.Constant) and arg_ast.annotation.value in aliases:
-                    types[name] = aliases[arg_ast.annotation.value]
+                if isinstance(annotation, ast.Constant) and annotation.value in aliases:
+                    types[name] = aliases[annotation.value]
                 else:
-                    arg_type = get_arg_type(arg_ast.annotation, aliases)
+                    arg_type = get_arg_type(annotation, aliases)
                     types[name] = resolve_forward_refs(arg_type, aliases, logger)
             except Exception as ex3:
                 types[name] = ex3
@@ -305,11 +310,18 @@ def get_types(obj: Any, logger: Optional[logging.Logger] = None) -> dict:
     return types
 
 
-def evaluate_postponed_annotations(params, component, logger):
+def evaluate_postponed_annotations(params, component, parent, logger):
     if not (params and any(type_requires_eval(p.annotation) for p in params)):
         return
     try:
-        types = get_types(component, logger)
+        if (
+            is_dataclass(parent)
+            and component.__name__ == "__init__"
+            and not component.__qualname__.startswith(parent.__name__ + ".")
+        ):
+            types = get_types(parent, logger)
+        else:
+            types = get_types(component, logger)
     except Exception as ex:
         logger.debug(f"Unable to evaluate types for {component}", exc_info=ex)
         return


Test patch:
diff --git a/jsonargparse_tests/test_postponed_annotations.py b/jsonargparse_tests/test_postponed_annotations.py
index fefc45d..f729c15 100644
--- a/jsonargparse_tests/test_postponed_annotations.py
+++ b/jsonargparse_tests/test_postponed_annotations.py
@@ -1,13 +1,17 @@
 from __future__ import annotations  # keep
 
+import dataclasses
+import os
 import sys
 from random import Random
 from typing import TYPE_CHECKING, Dict, FrozenSet, List, Optional, Set, Tuple, Type, Union
 
 import pytest
 
+from jsonargparse import Namespace
 from jsonargparse._parameter_resolvers import get_signature_parameters as get_params
 from jsonargparse._postponed_annotations import TypeCheckingVisitor, evaluate_postponed_annotations, get_types
+from jsonargparse.typing import Path_drw
 from jsonargparse_tests.conftest import capture_logs, source_unavailable
 
 
@@ -131,7 +135,7 @@ def test_get_types_all_types_fail():
 def test_evaluate_postponed_annotations_all_types_fail(logger):
     params = get_params(function_all_types_fail)
     with capture_logs(logger) as logs:
-        evaluate_postponed_annotations(params, function_all_types_fail, logger)
+        evaluate_postponed_annotations(params, function_all_types_fail, None, logger)
     assert "Unable to evaluate types for " in logs.getvalue()
 
 
@@ -295,3 +299,30 @@ def test_get_types_source_unavailable():
     with source_unavailable():
         types = get_types(function_source_unavailable)
     assert types == {"p1": List["TypeCheckingClass1"]}
+
+
+@dataclasses.dataclass
+class Data585:
+    a: list[int]
+    b: str = "x"
+
+
+def test_get_types_dataclass_pep585(parser):
+    types = get_types(Data585)
+    list_int = List[int] if sys.version_info < (3, 9) else list[int]
+    assert types == {"a": list_int, "b": str}
+    parser.add_dataclass_arguments(Data585, "data")
+    cfg = parser.parse_args(["--data.a=[1, 2]"])
+    assert cfg.data == Namespace(a=[1, 2], b="x")
+
+
+@dataclasses.dataclass
+class DataWithInit585(Data585):
+    def __init__(self, b: Path_drw, **kwargs):  # type: ignore
+        super().__init__(b=os.fspath(b), **kwargs)
+
+
+def test_add_dataclass_with_init_pep585(parser, tmp_cwd):
+    parser.add_dataclass_arguments(DataWithInit585, "data")
+    cfg = parser.parse_args(["--data.a=[1, 2]", "--data.b=."])
+    assert cfg.data == Namespace(a=[1, 2], b=Path_drw("."))