Repository: omni-us/jsonargparse

Issue:
Edge case of `field(default=False, init=False)`
## 🐛 Bug report

Hello Mauricio!

Here's an edge case for you

```python
  File "/home/ubuntu/carlos/stuff/kk.py", line 12, in <module>
    print(CLI(Bar))
  File "/home/ubuntu/.pyenv/versions/env/lib/python3.10/site-packages/jsonargparse/_cli.py", line 88, in CLI
    _add_component_to_parser(components, parser, as_positional, fail_untyped, config_help)
  File "/home/ubuntu/.pyenv/versions/env/lib/python3.10/site-packages/jsonargparse/_cli.py", line 169, in _add_component_to_parser
    added_args = parser.add_class_arguments(component, as_group=False, **kwargs)
  File "/home/ubuntu/.pyenv/versions/env/lib/python3.10/site-packages/jsonargparse/_signatures.py", line 107, in add_class_arguments
    added_args = self._add_signature_arguments(
  File "/home/ubuntu/.pyenv/versions/env/lib/python3.10/site-packages/jsonargparse/_signatures.py", line 308, in _add_signature_arguments
    self._add_signature_parameter(
  File "/home/ubuntu/.pyenv/versions/env/lib/python3.10/site-packages/jsonargparse/_signatures.py", line 434, in _add_signature_parameter
    action = container.add_argument(*args, **kwargs)
  File "/home/ubuntu/.pyenv/versions/env/lib/python3.10/site-packages/jsonargparse/_core.py", line 130, in add_argument
    self.add_class_arguments(kwargs.pop("type"), nested_key, **kwargs)
  File "/home/ubuntu/.pyenv/versions/env/lib/python3.10/site-packages/jsonargparse/_signatures.py", line 133, in add_class_arguments
    self.set_defaults(**defaults)  # type: ignore[attr-defined]
  File "/home/ubuntu/.pyenv/versions/env/lib/python3.10/site-packages/jsonargparse/_core.py", line 207, in set_defaults
    self.set_defaults(kwargs)
  File "/home/ubuntu/.pyenv/versions/env/lib/python3.10/site-packages/jsonargparse/_core.py", line 196, in set_defaults
    raise NSKeyError(f'No action for key "{dest}" to set its default.')
jsonargparse._namespace.NSKeyError: No action for key "metrics.x" to set its default.
```

### To reproduce

```python
from dataclasses import dataclass, field
from jsonargparse import CLI

@dataclass
class Foo:
    x: bool = field(default=False, init=False)

@dataclass
class Bar:
    metrics: Foo = field(default_factory=Foo)

print(CLI(Bar))
```

### Motivation

I have a dataclass with dataclasses
I want to add fields that are not part of the init
I cannot use `__post_init__` instead because it will be frozen (not included in the snippet above)

### Expected behavior

Works as expected

### Environment

- jsonargparse version: 4.35.0
- Python version: 3.10.12
- How jsonargparse was installed: pip
- OS: Linux

As always, thank you for this great tool 🙇

Hints from issue comments:
@mauvilsa: Thank you for reporting!

Solution patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 9a58560d..adc8b85a 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -22,6 +22,8 @@ Fixed
 - Adding ``attrs.define`` dataclasses with nested dataclasses that are marked with
   ``attrs.field`` (such as for a default factory) are not parsed correctly (`#643
   <https://github.com/omni-us/jsonargparse/pull/643>`__)
+- Nested dataclass with ``init=False`` not working correctly (`#650
+  <https://github.com/omni-us/jsonargparse/pull/650>`__).
 
 
 v4.35.0 (2024-12-16)
diff --git a/jsonargparse/_signatures.py b/jsonargparse/_signatures.py
index 8b8afe24..9dff645a 100644
--- a/jsonargparse/_signatures.py
+++ b/jsonargparse/_signatures.py
@@ -122,6 +122,10 @@ def add_class_arguments(
                 defaults = default.lazy_get_init_args().as_dict()
             elif is_dataclass_like(default.__class__):
                 defaults = dataclass_to_dict(default)
+                args = set(k[len(prefix) :] for k in added_args)
+                skip_not_added = [k for k in defaults if k not in args]
+                if skip_not_added:
+                    skip.update(skip_not_added)  # skip init=False
             elif isinstance(default, Namespace):
                 defaults = default.as_dict()
             if defaults:
@@ -428,9 +432,10 @@ def _add_signature_parameter(
                 kwargs.update(sub_add_kwargs)
             with ActionTypeHint.allow_default_instance_context():
                 action = container.add_argument(*args, **kwargs)
-            action.sub_add_kwargs = sub_add_kwargs
-            if is_subclass_typehint and len(subclass_skip) > 0:
-                action.sub_add_kwargs["skip"] = subclass_skip
+            if action is not None:  # None when class without any parameters
+                action.sub_add_kwargs = sub_add_kwargs
+                if is_subclass_typehint and len(subclass_skip) > 0:
+                    action.sub_add_kwargs["skip"] = subclass_skip
             added_args.append(dest)
         elif is_required and fail_untyped:
             raise ValueError(


Test patch:
diff --git a/jsonargparse_tests/test_dataclass_like.py b/jsonargparse_tests/test_dataclass_like.py
index 4a753453..d023957a 100644
--- a/jsonargparse_tests/test_dataclass_like.py
+++ b/jsonargparse_tests/test_dataclass_like.py
@@ -255,6 +255,27 @@ def test_dataclass_field_init_false(parser):
     assert isinstance(init.data, DataInitFalse)
 
 
+@dataclasses.dataclass
+class NestedDataInitFalse:
+    x: bool = dataclasses.field(default=False, init=False)
+
+
+@dataclasses.dataclass
+class ParentDataInitFalse:
+    y: NestedDataInitFalse = dataclasses.field(default_factory=NestedDataInitFalse)
+
+
+def test_nested_dataclass_field_init_false(parser):
+    parser.add_class_arguments(ParentDataInitFalse, "data")
+    assert parser.get_defaults() == Namespace()
+    cfg = parser.parse_args([])
+    assert cfg == Namespace()
+    init = parser.instantiate_classes(cfg)
+    assert isinstance(init.data, ParentDataInitFalse)
+    assert isinstance(init.data.y, NestedDataInitFalse)
+    assert init.data.y.x is False
+
+
 @dataclasses.dataclass
 class DataFieldFactory:
     a1: List[int] = dataclasses.field(default_factory=lambda: [1, 2, 3])