Repository: omni-us/jsonargparse

Issue:
Update `fail_untyped` flag to work with dataclasses whose arguments are classese with untyped arguments.
## 🚀 Feature request

The `fail_untyped` works with classes and nested classes, but id does not work with dataclasses which have class arguments with untyped arguments in their `__init__`.

### Motivation

Dataclasses come handy when we build complex CLI frameworks as it organise the input fields without the need of typing `class_path` and `init_args`. The powerful feature here is to have objects as arguments that could be configured directly from the CLI. However, even though when these objects have arguments that are typed it works great, but the majority of open source doesn't, and thus limits use quite a bit.

Here is a snip it to reproduce and to make sense of the above, quite complex 😅, description:
```py3
# main.py
from dataclasses import dataclass
from jsonargparse import CLI

class Database:
    def __init__(self, version) -> None:  # <-- HERE version is untyped; for `version: str` works!
        self._version = version

@dataclass
class MySQLConfig:
    db: Database
    host: str = "localhost"
    port: int = 8080

def my_app(cfg: MySQLConfig) -> None:
    print(f"{cfg.host}, {cfg.port}, {cfg.db._version}")

if __name__ == "__main__":
    CLI(my_app, fail_untyped=False)
```

Command:
```sh
python3 main.py --cfg.host "localhost" --cfg.port 8080 --cfg.db Database --cfg.db.version "0.0.1"
````
mind that when changing `version` to `version: str`, it works as expected.

Thank you for you time and for maintaining this great library!

Comments:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index b5624a2..c80d4ea 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -48,6 +48,7 @@ v4.19.0 (2022-12-27)
 
 Added
 ^^^^^
+- ``add_dataclass_arguments`` now supports the ``fail_untyped`` parameter
 - ``CLI`` now supports the ``fail_untyped`` and ``parser_class`` parameters.
 - ``bytes`` and ``bytearray`` registered on first use and decodes from standard
   Base64.
diff --git a/jsonargparse/signatures.py b/jsonargparse/signatures.py
index 1e3cf03..cd19e3f 100644
--- a/jsonargparse/signatures.py
+++ b/jsonargparse/signatures.py
@@ -312,6 +312,7 @@ class SignatureArguments(LoggerProperty):
             kwargs['required'] = True
         is_subclass_typehint = False
         is_final_class_typehint = is_final_class(annotation)
+        is_pure_dataclass_typehint = is_pure_dataclass(annotation)
         dest = (nested_key+'.' if nested_key else '') + name
         args = [dest if is_required and as_positional else '--'+dest]
         if param.origin:
@@ -326,7 +327,7 @@ class SignatureArguments(LoggerProperty):
         if annotation in {str, int, float, bool} or \
            is_subclass(annotation, (str, int, float)) or \
            is_final_class_typehint or \
-           is_pure_dataclass(annotation):
+           is_pure_dataclass_typehint:
             kwargs['type'] = annotation
         elif annotation != inspect_empty:
             try:
@@ -353,7 +354,7 @@ class SignatureArguments(LoggerProperty):
                 'sub_configs': sub_configs,
                 'instantiate': instantiate,
             }
-            if is_final_class_typehint:
+            if is_final_class_typehint or is_pure_dataclass_typehint:
                 kwargs.update(sub_add_kwargs)
             action = group.add_argument(*args, **kwargs)
             action.sub_add_kwargs = sub_add_kwargs
@@ -370,6 +371,7 @@ class SignatureArguments(LoggerProperty):
         nested_key: str,
         default: Optional[Union[Type, dict]] = None,
         as_group: bool = True,
+        fail_untyped: bool = True,
         **kwargs
     ) -> List[str]:
         """Adds arguments from a dataclass based on its field types and docstrings.
@@ -379,6 +381,7 @@ class SignatureArguments(LoggerProperty):
             nested_key: Key for nested namespace.
             default: Value for defaults. Must be instance of or kwargs for theclass.
             as_group: Whether arguments should be added to a new argument group.
+            fail_untyped: Whether to raise exception if a required parameter does not have a type.
 
         Returns:
             The list of arguments added.
@@ -413,6 +416,7 @@ class SignatureArguments(LoggerProperty):
                 nested_key,
                 params[field.name],
                 added_args,
+                fail_untyped=fail_untyped,
                 default=defaults.get(field.name, inspect_empty),
             )
 


Test patch:
diff --git a/jsonargparse_tests/test_signatures.py b/jsonargparse_tests/test_signatures.py
index 6a4d3cb..35400a1 100755
--- a/jsonargparse_tests/test_signatures.py
+++ b/jsonargparse_tests/test_signatures.py
@@ -1571,6 +1571,32 @@ class DataclassesTests(unittest.TestCase):
         self.assertEqual({'a': 1.2, 'b': 3.4}, cfg['a2'])
 
 
+    def test_dataclass_fail_untyped(self):
+
+        class MyClass:
+            def __init__(self, c1) -> None:
+                self.c1 = c1
+
+        @dataclasses.dataclass
+        class MyDataclass:
+            a1: MyClass
+            a2: str = "a2"
+            a3: str = "a3"
+
+        parser = ArgumentParser(exit_on_error=False)
+        parser.add_argument('--cfg', type=MyDataclass, fail_untyped=False)
+
+        with mock_module(MyDataclass, MyClass) as module:
+            class_path = f'"class_path": "{module}.MyClass"'
+            init_args = '"init_args": {"c1": 1}'
+            cfg = parser.parse_args(['--cfg.a1={'+class_path+', '+init_args+'}'])
+            cfg = parser.instantiate_classes(cfg)
+            self.assertIsInstance(cfg['cfg'], MyDataclass)
+            self.assertIsInstance(cfg['cfg'].a1, MyClass)
+            self.assertIsInstance(cfg['cfg'].a2, str)
+            self.assertIsInstance(cfg['cfg'].a3, str)
+
+
     def test_compose_dataclasses(self):
 
         @dataclasses.dataclass