Repository: lebrice/SimpleParsing

Issue:
Support frozen dataclass instances as subgroup values
**Is your feature request related to a problem? Please describe.**
I removed support for dataclass instances in subgroups dict. It would be nice to re-add support for them, as long as they are frozen.


Discussion:

Patch:
diff --git a/requirements.txt b/requirements.txt
index 758575a..0465575 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,2 @@
-typing_extensions
 docstring-parser~=0.15
+typing_extensions>=4.3.0
diff --git a/setup.py b/setup.py
index 8752e1a..65ad470 100644
--- a/setup.py
+++ b/setup.py
@@ -16,7 +16,11 @@ with open("requirements.txt") as req_file:
     install_requires = req_file.read().splitlines(keepends=False)
 
 extras_require: dict[str, list[str]] = {
-    "test": ["pytest", "pytest-xdist"],
+    "test": [
+        "pytest",
+        "pytest-xdist",
+        "pytest-regressions",
+    ],
     "yaml": ["pyyaml"],
 }
 extras_require["all"] = list(set(sum(extras_require.values(), [])))
diff --git a/simple_parsing/conflicts.py b/simple_parsing/conflicts.py
index 19968a4..315524d 100644
--- a/simple_parsing/conflicts.py
+++ b/simple_parsing/conflicts.py
@@ -136,7 +136,7 @@ class ConflictResolver:
         for w in wrappers:
             if isinstance(w, DataclassWrapper):
                 field_wrappers.extend(w.fields)
-                logger.debug(f"Wrapper {w.dest} has fields {w.fields}")
+                # logger.debug(f"Wrapper {w.dest} has fields {w.fields}")
             else:
                 field_wrappers.append(w)
 
diff --git a/simple_parsing/helpers/subgroups.py b/simple_parsing/helpers/subgroups.py
index 1751050..d948799 100644
--- a/simple_parsing/helpers/subgroups.py
+++ b/simple_parsing/helpers/subgroups.py
@@ -6,20 +6,24 @@ import typing
 from dataclasses import _MISSING_TYPE, MISSING
 from enum import Enum
 from logging import getLogger as get_logger
-from typing import Any, Callable, TypeVar, overload
+from typing import Any, Callable, TypeVar, Union, overload
 
-from simple_parsing.utils import DataclassT, is_dataclass_type
+from typing_extensions import TypeAlias
+
+from simple_parsing.utils import DataclassT, is_dataclass_instance, is_dataclass_type
 
 logger = get_logger(__name__)
 
+SubgroupKey: TypeAlias = Union[str, int, bool, Enum]
+
 Key = TypeVar("Key", str, int, bool, Enum)
 
 
 @overload
 def subgroups(
-    subgroups: dict[Key, type[DataclassT] | functools.partial[DataclassT]],
+    subgroups: dict[Key, DataclassT | type[DataclassT] | functools.partial[DataclassT]],
     *args,
-    default: Key,
+    default: Key | DataclassT,
     default_factory: _MISSING_TYPE = MISSING,
     **kwargs,
 ) -> DataclassT:
@@ -28,7 +32,7 @@ def subgroups(
 
 @overload
 def subgroups(
-    subgroups: dict[Key, type[DataclassT] | functools.partial[DataclassT]],
+    subgroups: dict[Key, DataclassT | type[DataclassT] | functools.partial[DataclassT]],
     *args,
     default: _MISSING_TYPE = MISSING,
     default_factory: type[DataclassT] | functools.partial[DataclassT],
@@ -39,7 +43,7 @@ def subgroups(
 
 @overload
 def subgroups(
-    subgroups: dict[Key, type[DataclassT] | functools.partial[DataclassT]],
+    subgroups: dict[Key, DataclassT | type[DataclassT] | functools.partial[DataclassT]],
     *args,
     default: _MISSING_TYPE = MISSING,
     default_factory: _MISSING_TYPE = MISSING,
@@ -49,9 +53,9 @@ def subgroups(
 
 
 def subgroups(
-    subgroups: dict[Key, type[DataclassT] | functools.partial[DataclassT]],
+    subgroups: dict[Key, DataclassT | type[DataclassT] | functools.partial[DataclassT]],
     *args,
-    default: Key | _MISSING_TYPE = MISSING,
+    default: Key | DataclassT | _MISSING_TYPE = MISSING,
     default_factory: type[DataclassT] | functools.partial[DataclassT] | _MISSING_TYPE = MISSING,
     **kwargs,
 ) -> DataclassT:
@@ -60,9 +64,6 @@ def subgroups(
     This is different than adding a subparser action. There can only be one subparser action, while
     there can be arbitrarily many subgroups. Subgroups can also be nested!
 
-    TODO: We don't yet inspect the default values for the fields of each subgroup, when the
-    subgroup values aren't classes (e.g. `functools.partial`s). The help text is therefore slightly
-    wrong: it shows the field defaults from the class definition, not those of the subgroup.
 
     Parameters
     ----------
@@ -81,7 +82,19 @@ def subgroups(
     """
     if default_factory is not MISSING and default is not MISSING:
         raise ValueError("Can't pass both default and default_factory!")
-    if default is not MISSING and default not in subgroups:
+    from collections.abc import Hashable
+
+    if is_dataclass_instance(default):
+        if not isinstance(default, Hashable):
+            raise ValueError(
+                "'default' can either be a key of the subgroups dict or a hashable (frozen) "
+                "dataclass."
+            )
+        if default not in subgroups.values():
+            # TODO: (@lebrice): Do we really need to enforce this? What is the reasoning behind this
+            # restriction again?
+            raise ValueError(f"Default value {default} needs to be a value in the subgroups dict.")
+    elif default is not MISSING and default not in subgroups.keys():
         raise ValueError("default must be a key in the subgroups dict!")
 
     if default_factory is not MISSING and default_factory not in list(subgroups.values()):
@@ -106,7 +119,8 @@ def subgroups(
     # instantiate that value in order to inspect the attributes and its values..
 
     # NOTE: This needs to be the right frame where the subgroups are set.
-    caller_frame = inspect.currentframe().f_back
+    _current_frame = inspect.currentframe()
+    caller_frame = _current_frame.f_back if _current_frame else None
     for subgroup_key, subgroup_value in subgroups.items():
         if is_lambda(subgroup_value):
             raise NotImplementedError(
@@ -117,7 +131,9 @@ def subgroups(
                 "change between subgroups, consider using a `functools.partial` instead. "
             )
 
-        if is_dataclass_type(subgroup_value):
+        if is_dataclass_instance(subgroup_value):
+            dataclass_type = type(subgroup_value)
+        elif is_dataclass_type(subgroup_value):
             # all good! Just use that dataclass.
             dataclass_type = subgroup_value
         else:
@@ -145,10 +161,16 @@ def subgroups(
     # subgroup_field_values = {}
 
     if default is not MISSING:
-        assert default in subgroups.keys()
-        default_factory = subgroups[default]
-        metadata["subgroup_default"] = default
-        default = MISSING
+        if is_dataclass_instance(default):
+            assert default in subgroups.values()
+            subgroup_key = [k for k, v in subgroups.items() if v is default][0]
+            metadata["subgroup_default"] = subgroup_key
+            default = subgroup_key
+        else:
+            assert default in subgroups.keys()
+            default_factory = subgroups[default]
+            metadata["subgroup_default"] = default
+            default = MISSING
 
     elif default_factory is not MISSING:
         # assert default_factory in subgroups.values()
diff --git a/simple_parsing/parsing.py b/simple_parsing/parsing.py
index 1c748bd..0941263 100644
--- a/simple_parsing/parsing.py
+++ b/simple_parsing/parsing.py
@@ -5,22 +5,31 @@ from __future__ import annotations
 
 import argparse
 import dataclasses
+import functools
 import itertools
 import shlex
 import sys
+import typing
 from argparse import SUPPRESS, Action, HelpFormatter, Namespace, _
 from collections import defaultdict
 from logging import getLogger
 from pathlib import Path
-from typing import Any, Callable, Sequence, TypeVar, overload
+from typing import Any, Callable, Sequence, Type, TypeVar, overload
 
+from simple_parsing.helpers.subgroups import SubgroupKey
 from simple_parsing.wrappers.dataclass_wrapper import DataclassWrapperType
 
 from . import utils
 from .conflicts import ConflictResolution, ConflictResolver
 from .help_formatter import SimpleHelpFormatter
 from .helpers.serialization.serializable import read_file
-from .utils import Dataclass, DataclassT, dict_union, is_dataclass_type
+from .utils import (
+    Dataclass,
+    DataclassT,
+    dict_union,
+    is_dataclass_instance,
+    is_dataclass_type,
+)
 from .wrappers import DashVariant, DataclassWrapper, FieldWrapper
 from .wrappers.field_wrapper import ArgumentGenerationMode, NestedMode
 
@@ -152,6 +161,7 @@ class ArgumentParser(argparse.ArgumentParser):
             add_config_path_arg = bool(config_path)
         self.add_config_path_arg = add_config_path_arg
 
+    # TODO: Remove, since the base class already has nicer type hints.
     def add_argument(
         self,
         *name_or_flags: str,
@@ -185,9 +195,21 @@ class ArgumentParser(argparse.ArgumentParser):
     ) -> DataclassWrapperType:
         pass
 
+    @overload
     def add_arguments(
         self,
-        dataclass: type[DataclassT] | Dataclass,
+        dataclass: DataclassT,
+        dest: str,
+        *,
+        prefix: str = "",
+        default: None = None,
+        dataclass_wrapper_class: type[DataclassWrapper] = DataclassWrapper,
+    ) -> DataclassWrapper[DataclassT]:
+        pass
+
+    def add_arguments(
+        self,
+        dataclass: type[DataclassT] | DataclassT,
         dest: str,
         *,
         prefix: str = "",
@@ -223,8 +245,23 @@ class ArgumentParser(argparse.ArgumentParser):
         The generated DataclassWrapper instance. Feel free to inspect / play around with this if
         you want :)
         """
+        if is_dataclass_instance(dataclass):
+            if default is not None:
+                raise ValueError("Can't use `default` when `dataclass` is a dataclass instance.")
+            dataclass = typing.cast(DataclassT, dataclass)
+            dataclass_type = type(dataclass)
+            default = dataclass
+        else:
+            if not is_dataclass_type(dataclass):
+                raise ValueError(
+                    f"`dataclass` should be a dataclass type or instance. Got {dataclass}."
+                )
+            dataclass = typing.cast(Type[DataclassT], dataclass)
+            dataclass_type = dataclass
+            default = default
+
         new_wrapper = self._add_arguments(
-            dataclass=dataclass,
+            dataclass_type=dataclass_type,
             name=dest,
             prefix=prefix,
             default=default,
@@ -403,35 +440,43 @@ class ArgumentParser(argparse.ArgumentParser):
 
     def _add_arguments(
         self,
-        dataclass: type[DataclassT] | DataclassT,
+        dataclass_type: type[DataclassT],
         name: str,
         *,
         prefix: str = "",
         dataclass_fn: Callable[..., DataclassT] | None = None,
-        default: Dataclass | None = None,
+        default: DataclassT | dict | None = None,
         dataclass_wrapper_class: type[DataclassWrapperType] = DataclassWrapper,
         parent: DataclassWrapper | None = None,
     ) -> DataclassWrapper[DataclassT] | DataclassWrapperType:
+        assert is_dataclass_type(dataclass_type)
+        assert (
+            default is None
+            or is_dataclass_instance(default)
+            or default is argparse.SUPPRESS
+            or isinstance(default, dict)
+        )
+        assert dataclass_fn is None or callable(dataclass_fn)
+
         for wrapper in self._wrappers:
             if wrapper.dest == name:
-                if wrapper.dataclass == dataclass:
+                if wrapper.dataclass == dataclass_type:
                     raise argparse.ArgumentError(
                         argument=None,
                         message=f"Destination attribute {name} is already used for "
-                        f"dataclass of type {dataclass}. Make sure all destinations"
-                        f" are unique. (new dataclass type: {dataclass})",
+                        f"dataclass of type {dataclass_type}. Make sure all destinations"
+                        f" are unique. (new dataclass type: {dataclass_type})",
                     )
-        if not isinstance(dataclass, type):
+        if not isinstance(dataclass_type, type):
             if default is None:
-                default = dataclass
-            dataclass = type(dataclass)
-
-        dataclass_fn = dataclass_fn or dataclass
+                default = dataclass_type
+            dataclass_type = type(dataclass_type)
 
+        dataclass_fn = dataclass_fn or dataclass_type
         # Create this object that  holds the dataclass we will create arguments for and the
         # arguments that were passed.
         new_wrapper = dataclass_wrapper_class(
-            dataclass=dataclass,
+            dataclass=dataclass_type,
             name=name,
             prefix=prefix,
             default=default,
@@ -557,7 +602,7 @@ class ArgumentParser(argparse.ArgumentParser):
         unresolved_subgroups = _get_subgroup_fields(wrappers)
         # Dictionary of the subgroup choices that were resolved (key: subgroup dest, value: chosen
         # subgroup name).
-        resolved_subgroups: dict[str, str] = {}
+        resolved_subgroups: dict[str, SubgroupKey] = {}
 
         if not unresolved_subgroups:
             # No subgroups to parse.
@@ -584,7 +629,7 @@ class ArgumentParser(argparse.ArgumentParser):
         for current_nesting_level in itertools.count():
             # Do rounds of parsing with just the subgroup arguments, until all the subgroups
             # are resolved to a dataclass type.
-            logger.info(
+            logger.debug(
                 f"Starting subgroup parsing round {current_nesting_level}: {list(unresolved_subgroups.keys())}"
             )
             # Add all the unresolved subgroups arguments.
@@ -596,16 +641,19 @@ class ArgumentParser(argparse.ArgumentParser):
                     assert argument_options["required"]
                 else:
                     assert argument_options["default"] is subgroup_field.subgroup_default
+                    assert not is_dataclass_instance(argument_options["default"])
 
                 # TODO: Do we really need to care about this "SUPPRESS" stuff here?
                 if argparse.SUPPRESS in subgroup_field.parent.defaults:
                     assert argument_options["default"] is argparse.SUPPRESS
                     argument_options["default"] = argparse.SUPPRESS
-                logger.info(
+
+                logger.debug(
                     f"Adding subgroup argument: add_argument(*{flags} **{str(argument_options)})"
                 )
                 subgroup_choice_parser.add_argument(*flags, **argument_options)
 
+            # Parse `args` repeatedly until all the subgroup choices are resolved.
             parsed_args, unused_args = subgroup_choice_parser.parse_known_args(
                 args=args, namespace=namespace
             )
@@ -618,42 +666,49 @@ class ArgumentParser(argparse.ArgumentParser):
                 # NOTE: There should always be a parsed value for the subgroup argument on the
                 # namespace. This is because we added all the subgroup arguments before we get
                 # here.
-                assert hasattr(parsed_args, dest)
-                chosen_subgroup_key = getattr(parsed_args, dest)
-                assert chosen_subgroup_key in subgroup_field.subgroup_choices
+                subgroup_dict = subgroup_field.subgroup_choices
+                chosen_subgroup_key: SubgroupKey = getattr(parsed_args, dest)
+                assert chosen_subgroup_key in subgroup_dict
+
+                # Changing the default value of the (now parsed) field for the subgroup choice,
+                # just so it shows (default: {chosen_subgroup_key}) on the command-line.
+                # Note: This really isn't required, we could have it just be the default value, but
+                # it seems a bit more consistent with us then showing the --help string for the
+                # chosen dataclass type (as we're doing below).
+                # subgroup_field.set_default(chosen_subgroup_key)
+                logger.debug(
+                    f"resolved the subgroup at {dest!r}: will use the subgroup at key "
+                    f"{chosen_subgroup_key!r}"
+                )
 
-                chosen_subgroup_dataclass_fn = subgroup_field.subgroup_choices[chosen_subgroup_key]
-                dataclass_types = subgroup_field.field.metadata["subgroup_dataclass_types"]
-                chosen_subgroup_dataclass_type = dataclass_types[chosen_subgroup_key]
-                assert is_dataclass_type(chosen_subgroup_dataclass_type)
+                default_or_dataclass_fn = subgroup_dict[chosen_subgroup_key]
+                if is_dataclass_instance(default_or_dataclass_fn):
+                    # The chosen value in the subgroup dict is a frozen dataclass instance.
+                    default = default_or_dataclass_fn
+                    dataclass_fn = functools.partial(dataclasses.replace, default)
+                    dataclass_type = type(default)
+                else:
+                    default = None
+                    dataclass_fn = default_or_dataclass_fn
+                    dataclass_type = subgroup_field.field.metadata["subgroup_dataclass_types"][
+                        chosen_subgroup_key
+                    ]
 
-                resolved_subgroups[dest] = chosen_subgroup_key
+                assert default is None or is_dataclass_instance(default)
+                assert callable(dataclass_fn)
+                assert is_dataclass_type(dataclass_type)
 
-                logger.info(
-                    f"resolved the subgroup at dest {dest} to a value of "
-                    f"{chosen_subgroup_key}, which means to use the "
-                    f"{chosen_subgroup_dataclass_type} dataclass"
-                )
-                parent_dataclass_wrapper = subgroup_field.parent
-                # The default value for the subgroup field should be the value that was chosen.
-                # Manually set the default value for this field.
-                subgroup_field.set_default(chosen_subgroup_key)
-
-                # NOTE: Here the `default` for the new argument group is `None`, because
-                # `subgroups` only allows using a dict[Hashable, callable], and we want to avoid
-                # calling the callables unless we really need to.
-                default = None
                 name = dest.split(".")[-1]
+                parent_dataclass_wrapper = subgroup_field.parent
                 # NOTE: Using self._add_arguments so it returns the modified wrapper and doesn't
                 # affect the `self._wrappers` list.
                 new_wrapper = self._add_arguments(
-                    dataclass=chosen_subgroup_dataclass_type,
-                    dataclass_fn=chosen_subgroup_dataclass_fn,
+                    dataclass_type=dataclass_type,
                     name=name,
+                    dataclass_fn=dataclass_fn,
                     default=default,
                     parent=parent_dataclass_wrapper,
                 )
-
                 # Make the new wrapper a child of the class which contains the field.
                 # - it isn't already a child
                 # - it's parent is the parent dataclass wrapper
@@ -664,7 +719,11 @@ class ArgumentParser(argparse.ArgumentParser):
                 assert parent_dataclass_wrapper in _flatten_wrappers(wrappers)
                 assert new_wrapper in _flatten_wrappers(wrappers)
 
+                # Mark this subgroup as resolved.
                 unresolved_subgroups.pop(dest)
+                resolved_subgroups[dest] = chosen_subgroup_key
+                # TODO: Should we remove the FieldWrapper for the subgroups now that it's been
+                # resolved?
 
             # Find the new subgroup fields that weren't resolved before.
             # TODO: What if a name conflict occurs between a subgroup field and one of the new
@@ -679,15 +738,15 @@ class ArgumentParser(argparse.ArgumentParser):
             unresolved_subgroups = {
                 k: v for k, v in all_subgroup_fields.items() if k not in resolved_subgroups
             }
-            logger.info(f"All subgroups: {list(all_subgroup_fields.keys())}")
-            logger.info(f"Resolved subgroups: {resolved_subgroups}")
-            logger.info(f"Unresolved subgroups: {list(unresolved_subgroups.keys())}")
+            logger.debug(f"All subgroups: {list(all_subgroup_fields.keys())}")
+            logger.debug(f"Resolved subgroups: {resolved_subgroups}")
+            logger.debug(f"Unresolved subgroups: {list(unresolved_subgroups.keys())}")
 
             if not unresolved_subgroups:
-                logger.info("Done parsing all the subgroups!")
+                logger.debug("Done parsing all the subgroups!")
                 break
             else:
-                logger.info(
+                logger.debug(
                     f"Done parsing a round of subparsers at nesting level "
                     f"{current_nesting_level}. Moving to the next round which has "
                     f"{len(unresolved_subgroups)} unresolved subgroup choices."
@@ -878,7 +937,7 @@ class ArgumentParser(argparse.ArgumentParser):
                     continue
 
                 if field.is_subgroup:
-                    # Skip the subgroup fields.
+                    # Skip the subgroup fields, since we added a child DataclassWrapper for them.
                     logger.debug(f"Not calling the subgroup FieldWrapper for dest {field.dest}")
                     continue
 
@@ -1068,5 +1127,5 @@ def _create_dataclass_instance(
         else:
             logger.debug(f"All fields for {wrapper.dest} were either at their default, or None.")
             return None
-
+    logger.debug(f"Calling constructor: {constructor}(**{constructor_args})")
     return constructor(**constructor_args)
diff --git a/simple_parsing/wrappers/dataclass_wrapper.py b/simple_parsing/wrappers/dataclass_wrapper.py
index a66ca55..2c00c98 100644
--- a/simple_parsing/wrappers/dataclass_wrapper.py
+++ b/simple_parsing/wrappers/dataclass_wrapper.py
@@ -14,7 +14,7 @@ import docstring_parser as dp
 from typing_extensions import Literal
 
 from .. import docstring, utils
-from ..utils import Dataclass, DataclassT
+from ..utils import Dataclass, DataclassT, is_dataclass_instance, is_dataclass_type
 from .field_wrapper import FieldWrapper
 from .wrapper import Wrapper
 
@@ -45,7 +45,12 @@ class DataclassWrapper(Wrapper, Generic[DataclassT]):
         super().__init__()
         self.dataclass = dataclass
         self._name = name
+        assert is_dataclass_type(dataclass)  # FIXME: Remove
+        if dataclass_fn:
+            assert callable(dataclass_fn), dataclass_fn
         self.dataclass_fn = dataclass_fn or dataclass
+        assert not is_dataclass_instance(self.dataclass_fn)  # FIXME: Remove
+
         self._default = default
         self.prefix = prefix
         self._parent = parent
@@ -203,8 +208,6 @@ class DataclassWrapper(Wrapper, Generic[DataclassT]):
                     f"Adding a subgroup field {wrapped_field.name} just so it shows up in the "
                     f"--help text."
                 )
-                # check that the default was properly set to the chosen subgroup value
-                assert wrapped_field.default in wrapped_field.subgroup_choices.keys()
 
             logger.info(f"group.add_argument(*{wrapped_field.option_strings}, **{arg_options})")
             # TODO: Perhaps we could hook into the `action` that is returned here to know if the
diff --git a/simple_parsing/wrappers/field_wrapper.py b/simple_parsing/wrappers/field_wrapper.py
index 74c0d24..0becac2 100644
--- a/simple_parsing/wrappers/field_wrapper.py
+++ b/simple_parsing/wrappers/field_wrapper.py
@@ -7,7 +7,7 @@ import sys
 import typing
 from enum import Enum, auto
 from logging import getLogger
-from typing import Any, ClassVar, Hashable, Union, cast
+from typing import Any, Callable, ClassVar, Hashable, Union, cast
 
 from typing_extensions import Literal
 
@@ -15,6 +15,7 @@ from simple_parsing.help_formatter import TEMPORARY_TOKEN
 
 from .. import docstring, utils
 from ..helpers.custom_actions import BooleanOptionalAction
+from ..utils import Dataclass
 from .field_metavar import get_metavar
 from .field_parsing import get_parsing_fn
 from .wrapper import Wrapper
@@ -961,7 +962,7 @@ class FieldWrapper(Wrapper):
         return "subgroups" in self.field.metadata
 
     @property
-    def subgroup_choices(self) -> dict[str, type]:
+    def subgroup_choices(self) -> dict[Hashable, Callable[[], Dataclass] | Dataclass]:
         if not self.is_subgroup:
             raise RuntimeError(f"Field {self.field} doesn't have subgroups! ")
         return self.field.metadata["subgroups"]


Test patch:
diff --git a/test/test_subgroups.py b/test/test_subgroups.py
index 40cf0cd..39204b5 100644
--- a/test/test_subgroups.py
+++ b/test/test_subgroups.py
@@ -2,12 +2,16 @@ from __future__ import annotations
 
 import dataclasses
 import functools
+import inspect
 import shlex
+import sys
 from dataclasses import dataclass, field
 from functools import partial
+from pathlib import Path
 from typing import Callable, TypeVar
 
 import pytest
+from pytest_regressions.file_regression import FileRegressionFixture
 from typing_extensions import Annotated
 
 from simple_parsing import ArgumentParser, subgroups
@@ -579,7 +583,7 @@ class ModelBConfig(ModelConfig):
 
 
 @dataclass
-class Config:
+class Config(TestSetup):
 
     # Which model to use
     model: ModelConfig = subgroups(
@@ -674,6 +678,94 @@ def test_annotated_as_subgroups():
     assert Config.setup("--num_layers 123").model == Model(num_layers=123, hidden_dim=32)
 
 
+@dataclasses.dataclass(frozen=True)
+class FrozenConfig:
+    a: int = 1
+    b: str = "bob"
+
+
+def test_subgroups_doesnt_support_nonfrozen_instances():
+    with pytest.raises(
+        ValueError,
+        match="'default' can either be a key of the subgroups dict or a hashable",
+    ):
+        _ = subgroups({"a": A, "b": B}, default=A(a=1.0))
+
+    with pytest.raises(
+        ValueError,
+        match="'default' can either be a key of the subgroups dict or a hashable",
+    ):
+        _ = subgroups({"a": A(a=1.0), "b": B}, default=A(a=1.0))
+
+
+odd = FrozenConfig(a=1, b="odd")
+even = FrozenConfig(a=2, b="even")
+
+
+@dataclasses.dataclass
+class ConfigWithFrozen(TestSetup):
+    conf: FrozenConfig = subgroups({"odd": odd, "even": even}, default=odd)
+
+
+@pytest.mark.parametrize(
+    ("command", "expected"),
+    [
+        ("", ConfigWithFrozen(odd)),
+        ("--conf=odd", ConfigWithFrozen(odd)),
+        ("--conf=even", ConfigWithFrozen(even)),
+        ("--conf=odd --a 123", ConfigWithFrozen(dataclasses.replace(odd, a=123))),
+        ("--conf=even --a 100", ConfigWithFrozen(dataclasses.replace(even, a=100))),
+    ],
+)
+def test_subgroups_supports_frozen_instances(command: str, expected: ConfigWithFrozen):
+    assert ConfigWithFrozen.setup(command) == expected
+
+
+@pytest.mark.parametrize(
+    ("dataclass_type", "command"),
+    [
+        (Config, "--help"),
+        (Config, "--model=model_a --help"),
+        (Config, "--model=model_b --help"),
+        (ConfigWithFrozen, "--help"),
+        (ConfigWithFrozen, "--conf=odd --help"),
+        (ConfigWithFrozen, "--conf=even --help"),
+        (ConfigWithFrozen, "--conf=odd --a 123 --help"),
+        (ConfigWithFrozen, "--conf=even --a 100 --help"),
+    ],
+)
+def test_help(
+    dataclass_type: type[TestSetup],
+    command: str,
+    file_regression: FileRegressionFixture,
+    request: pytest.FixtureRequest,
+):
+    if sys.version_info[:2] != (3, 11):
+        pytest.skip("The regression check is only ran with Python 3.11")
+    here = Path(__file__).relative_to(Path.cwd())
+    file_regression.check(
+        f"""\
+# Regression file for [this test]({here}:{inspect.getsourcelines(test_help)[1]})
+
+Given Source code:
+
+```python
+{''.join(inspect.getsourcelines(dataclass_type)[0])}
+```
+
+and command: {command!r}
+
+We expect to get:
+
+```console
+{dataclass_type.get_help_text(command)}
+```
+""",
+        basename=request.node.name,
+        extension=".md",
+    )
+
+
 # def test_subgroups_with_partials():
 #     class Model:
 #         def __init__(self, num_layers: int = 3, hidden_dim: int = 64):
diff --git a/test/test_subgroups/test_help[simple-Config---help].md b/test/test_subgroups/test_help[simple-Config---help].md
new file mode 100644
index 0000000..c433813
--- /dev/null
+++ b/test/test_subgroups/test_help[simple-Config---help].md
@@ -0,0 +1,41 @@
+# Regression file for [this test](test/test_subgroups.py:724)
+
+Given Source code:
+
+```python
+@dataclass
+class Config(TestSetup):
+
+    # Which model to use
+    model: ModelConfig = subgroups(
+        {"model_a": ModelAConfig, "model_b": ModelBConfig},
+        default_factory=ModelAConfig,
+    )
+
+```
+
+and command: '--help'
+
+We expect to get:
+
+```console
+usage: pytest [-h] [--model {model_a,model_b}] [--lr float] [--optimizer str]
+              [--betas float float]
+
+options:
+  -h, --help            show this help message and exit
+
+Config ['config']:
+  Config(model: 'ModelConfig' = <factory>)
+
+  --model {model_a,model_b}
+                        Which model to use (default: model_a)
+
+ModelAConfig ['config.model']:
+  ModelAConfig(lr: 'float' = 0.0003, optimizer: 'str' = 'Adam', betas: 'tuple[float, float]' = (0.9, 0.999))
+
+  --lr float            (default: 0.0003)
+  --optimizer str       (default: Adam)
+  --betas float float   (default: (0.9, 0.999))
+
+```
diff --git a/test/test_subgroups/test_help[simple-Config---model=model_a --help].md b/test/test_subgroups/test_help[simple-Config---model=model_a --help].md
new file mode 100644
index 0000000..d05ea28
--- /dev/null
+++ b/test/test_subgroups/test_help[simple-Config---model=model_a --help].md
@@ -0,0 +1,41 @@
+# Regression file for [this test](test/test_subgroups.py:724)
+
+Given Source code:
+
+```python
+@dataclass
+class Config(TestSetup):
+
+    # Which model to use
+    model: ModelConfig = subgroups(
+        {"model_a": ModelAConfig, "model_b": ModelBConfig},
+        default_factory=ModelAConfig,
+    )
+
+```
+
+and command: '--model=model_a --help'
+
+We expect to get:
+
+```console
+usage: pytest [-h] [--model {model_a,model_b}] [--lr float] [--optimizer str]
+              [--betas float float]
+
+options:
+  -h, --help            show this help message and exit
+
+Config ['config']:
+  Config(model: 'ModelConfig' = <factory>)
+
+  --model {model_a,model_b}
+                        Which model to use (default: model_a)
+
+ModelAConfig ['config.model']:
+  ModelAConfig(lr: 'float' = 0.0003, optimizer: 'str' = 'Adam', betas: 'tuple[float, float]' = (0.9, 0.999))
+
+  --lr float            (default: 0.0003)
+  --optimizer str       (default: Adam)
+  --betas float float   (default: (0.9, 0.999))
+
+```
diff --git a/test/test_subgroups/test_help[simple-Config---model=model_b --help].md b/test/test_subgroups/test_help[simple-Config---model=model_b --help].md
new file mode 100644
index 0000000..34f0afa
--- /dev/null
+++ b/test/test_subgroups/test_help[simple-Config---model=model_b --help].md
@@ -0,0 +1,41 @@
+# Regression file for [this test](test/test_subgroups.py:724)
+
+Given Source code:
+
+```python
+@dataclass
+class Config(TestSetup):
+
+    # Which model to use
+    model: ModelConfig = subgroups(
+        {"model_a": ModelAConfig, "model_b": ModelBConfig},
+        default_factory=ModelAConfig,
+    )
+
+```
+
+and command: '--model=model_b --help'
+
+We expect to get:
+
+```console
+usage: pytest [-h] [--model {model_a,model_b}] [--lr float] [--optimizer str]
+              [--momentum float]
+
+options:
+  -h, --help            show this help message and exit
+
+Config ['config']:
+  Config(model: 'ModelConfig' = <factory>)
+
+  --model {model_a,model_b}
+                        Which model to use (default: model_a)
+
+ModelBConfig ['config.model']:
+  ModelBConfig(lr: 'float' = 0.001, optimizer: 'str' = 'SGD', momentum: 'float' = 1.234)
+
+  --lr float            (default: 0.001)
+  --optimizer str       (default: SGD)
+  --momentum float      (default: 1.234)
+
+```
diff --git a/test/test_subgroups/test_help[simple-ConfigWithFrozen---conf=even --a 100 --help].md b/test/test_subgroups/test_help[simple-ConfigWithFrozen---conf=even --a 100 --help].md
new file mode 100644
index 0000000..e9fd3ef
--- /dev/null
+++ b/test/test_subgroups/test_help[simple-ConfigWithFrozen---conf=even --a 100 --help].md
@@ -0,0 +1,33 @@
+# Regression file for [this test](test/test_subgroups.py:724)
+
+Given Source code:
+
+```python
+@dataclasses.dataclass
+class ConfigWithFrozen(TestSetup):
+    conf: FrozenConfig = subgroups({"odd": odd, "even": even}, default=odd)
+
+```
+
+and command: '--conf=even --a 100 --help'
+
+We expect to get:
+
+```console
+usage: pytest [-h] [--conf {odd,even}] [-a int] [-b str]
+
+options:
+  -h, --help         show this help message and exit
+
+ConfigWithFrozen ['config_with_frozen']:
+  ConfigWithFrozen(conf: 'FrozenConfig' = 'odd')
+
+  --conf {odd,even}  (default: odd)
+
+FrozenConfig ['config_with_frozen.conf']:
+  FrozenConfig(a: 'int' = 1, b: 'str' = 'bob')
+
+  -a int, --a int    (default: 2)
+  -b str, --b str    (default: even)
+
+```
diff --git a/test/test_subgroups/test_help[simple-ConfigWithFrozen---conf=even --help].md b/test/test_subgroups/test_help[simple-ConfigWithFrozen---conf=even --help].md
new file mode 100644
index 0000000..a2b877e
--- /dev/null
+++ b/test/test_subgroups/test_help[simple-ConfigWithFrozen---conf=even --help].md
@@ -0,0 +1,33 @@
+# Regression file for [this test](test/test_subgroups.py:724)
+
+Given Source code:
+
+```python
+@dataclasses.dataclass
+class ConfigWithFrozen(TestSetup):
+    conf: FrozenConfig = subgroups({"odd": odd, "even": even}, default=odd)
+
+```
+
+and command: '--conf=even --help'
+
+We expect to get:
+
+```console
+usage: pytest [-h] [--conf {odd,even}] [-a int] [-b str]
+
+options:
+  -h, --help         show this help message and exit
+
+ConfigWithFrozen ['config_with_frozen']:
+  ConfigWithFrozen(conf: 'FrozenConfig' = 'odd')
+
+  --conf {odd,even}  (default: odd)
+
+FrozenConfig ['config_with_frozen.conf']:
+  FrozenConfig(a: 'int' = 1, b: 'str' = 'bob')
+
+  -a int, --a int    (default: 2)
+  -b str, --b str    (default: even)
+
+```
diff --git a/test/test_subgroups/test_help[simple-ConfigWithFrozen---conf=odd --a 123 --help].md b/test/test_subgroups/test_help[simple-ConfigWithFrozen---conf=odd --a 123 --help].md
new file mode 100644
index 0000000..6e3d8b9
--- /dev/null
+++ b/test/test_subgroups/test_help[simple-ConfigWithFrozen---conf=odd --a 123 --help].md
@@ -0,0 +1,33 @@
+# Regression file for [this test](test/test_subgroups.py:724)
+
+Given Source code:
+
+```python
+@dataclasses.dataclass
+class ConfigWithFrozen(TestSetup):
+    conf: FrozenConfig = subgroups({"odd": odd, "even": even}, default=odd)
+
+```
+
+and command: '--conf=odd --a 123 --help'
+
+We expect to get:
+
+```console
+usage: pytest [-h] [--conf {odd,even}] [-a int] [-b str]
+
+options:
+  -h, --help         show this help message and exit
+
+ConfigWithFrozen ['config_with_frozen']:
+  ConfigWithFrozen(conf: 'FrozenConfig' = 'odd')
+
+  --conf {odd,even}  (default: odd)
+
+FrozenConfig ['config_with_frozen.conf']:
+  FrozenConfig(a: 'int' = 1, b: 'str' = 'bob')
+
+  -a int, --a int    (default: 1)
+  -b str, --b str    (default: odd)
+
+```
diff --git a/test/test_subgroups/test_help[simple-ConfigWithFrozen---conf=odd --help].md b/test/test_subgroups/test_help[simple-ConfigWithFrozen---conf=odd --help].md
new file mode 100644
index 0000000..7a715e3
--- /dev/null
+++ b/test/test_subgroups/test_help[simple-ConfigWithFrozen---conf=odd --help].md
@@ -0,0 +1,33 @@
+# Regression file for [this test](test/test_subgroups.py:724)
+
+Given Source code:
+
+```python
+@dataclasses.dataclass
+class ConfigWithFrozen(TestSetup):
+    conf: FrozenConfig = subgroups({"odd": odd, "even": even}, default=odd)
+
+```
+
+and command: '--conf=odd --help'
+
+We expect to get:
+
+```console
+usage: pytest [-h] [--conf {odd,even}] [-a int] [-b str]
+
+options:
+  -h, --help         show this help message and exit
+
+ConfigWithFrozen ['config_with_frozen']:
+  ConfigWithFrozen(conf: 'FrozenConfig' = 'odd')
+
+  --conf {odd,even}  (default: odd)
+
+FrozenConfig ['config_with_frozen.conf']:
+  FrozenConfig(a: 'int' = 1, b: 'str' = 'bob')
+
+  -a int, --a int    (default: 1)
+  -b str, --b str    (default: odd)
+
+```
diff --git a/test/test_subgroups/test_help[simple-ConfigWithFrozen---help].md b/test/test_subgroups/test_help[simple-ConfigWithFrozen---help].md
new file mode 100644
index 0000000..ee19ae6
--- /dev/null
+++ b/test/test_subgroups/test_help[simple-ConfigWithFrozen---help].md
@@ -0,0 +1,33 @@
+# Regression file for [this test](test/test_subgroups.py:724)
+
+Given Source code:
+
+```python
+@dataclasses.dataclass
+class ConfigWithFrozen(TestSetup):
+    conf: FrozenConfig = subgroups({"odd": odd, "even": even}, default=odd)
+
+```
+
+and command: '--help'
+
+We expect to get:
+
+```console
+usage: pytest [-h] [--conf {odd,even}] [-a int] [-b str]
+
+options:
+  -h, --help         show this help message and exit
+
+ConfigWithFrozen ['config_with_frozen']:
+  ConfigWithFrozen(conf: 'FrozenConfig' = 'odd')
+
+  --conf {odd,even}  (default: odd)
+
+FrozenConfig ['config_with_frozen.conf']:
+  FrozenConfig(a: 'int' = 1, b: 'str' = 'bob')
+
+  -a int, --a int    (default: 1)
+  -b str, --b str    (default: odd)
+
+```
diff --git a/test/test_subgroups/test_help[verbose-Config---help].md b/test/test_subgroups/test_help[verbose-Config---help].md
new file mode 100644
index 0000000..c433813
--- /dev/null
+++ b/test/test_subgroups/test_help[verbose-Config---help].md
@@ -0,0 +1,41 @@
+# Regression file for [this test](test/test_subgroups.py:724)
+
+Given Source code:
+
+```python
+@dataclass
+class Config(TestSetup):
+
+    # Which model to use
+    model: ModelConfig = subgroups(
+        {"model_a": ModelAConfig, "model_b": ModelBConfig},
+        default_factory=ModelAConfig,
+    )
+
+```
+
+and command: '--help'
+
+We expect to get:
+
+```console
+usage: pytest [-h] [--model {model_a,model_b}] [--lr float] [--optimizer str]
+              [--betas float float]
+
+options:
+  -h, --help            show this help message and exit
+
+Config ['config']:
+  Config(model: 'ModelConfig' = <factory>)
+
+  --model {model_a,model_b}
+                        Which model to use (default: model_a)
+
+ModelAConfig ['config.model']:
+  ModelAConfig(lr: 'float' = 0.0003, optimizer: 'str' = 'Adam', betas: 'tuple[float, float]' = (0.9, 0.999))
+
+  --lr float            (default: 0.0003)
+  --optimizer str       (default: Adam)
+  --betas float float   (default: (0.9, 0.999))
+
+```
diff --git a/test/test_subgroups/test_help[verbose-Config---model=model_a --help].md b/test/test_subgroups/test_help[verbose-Config---model=model_a --help].md
new file mode 100644
index 0000000..d05ea28
--- /dev/null
+++ b/test/test_subgroups/test_help[verbose-Config---model=model_a --help].md
@@ -0,0 +1,41 @@
+# Regression file for [this test](test/test_subgroups.py:724)
+
+Given Source code:
+
+```python
+@dataclass
+class Config(TestSetup):
+
+    # Which model to use
+    model: ModelConfig = subgroups(
+        {"model_a": ModelAConfig, "model_b": ModelBConfig},
+        default_factory=ModelAConfig,
+    )
+
+```
+
+and command: '--model=model_a --help'
+
+We expect to get:
+
+```console
+usage: pytest [-h] [--model {model_a,model_b}] [--lr float] [--optimizer str]
+              [--betas float float]
+
+options:
+  -h, --help            show this help message and exit
+
+Config ['config']:
+  Config(model: 'ModelConfig' = <factory>)
+
+  --model {model_a,model_b}
+                        Which model to use (default: model_a)
+
+ModelAConfig ['config.model']:
+  ModelAConfig(lr: 'float' = 0.0003, optimizer: 'str' = 'Adam', betas: 'tuple[float, float]' = (0.9, 0.999))
+
+  --lr float            (default: 0.0003)
+  --optimizer str       (default: Adam)
+  --betas float float   (default: (0.9, 0.999))
+
+```
diff --git a/test/test_subgroups/test_help[verbose-Config---model=model_b --help].md b/test/test_subgroups/test_help[verbose-Config---model=model_b --help].md
new file mode 100644
index 0000000..34f0afa
--- /dev/null
+++ b/test/test_subgroups/test_help[verbose-Config---model=model_b --help].md
@@ -0,0 +1,41 @@
+# Regression file for [this test](test/test_subgroups.py:724)
+
+Given Source code:
+
+```python
+@dataclass
+class Config(TestSetup):
+
+    # Which model to use
+    model: ModelConfig = subgroups(
+        {"model_a": ModelAConfig, "model_b": ModelBConfig},
+        default_factory=ModelAConfig,
+    )
+
+```
+
+and command: '--model=model_b --help'
+
+We expect to get:
+
+```console
+usage: pytest [-h] [--model {model_a,model_b}] [--lr float] [--optimizer str]
+              [--momentum float]
+
+options:
+  -h, --help            show this help message and exit
+
+Config ['config']:
+  Config(model: 'ModelConfig' = <factory>)
+
+  --model {model_a,model_b}
+                        Which model to use (default: model_a)
+
+ModelBConfig ['config.model']:
+  ModelBConfig(lr: 'float' = 0.001, optimizer: 'str' = 'SGD', momentum: 'float' = 1.234)
+
+  --lr float            (default: 0.001)
+  --optimizer str       (default: SGD)
+  --momentum float      (default: 1.234)
+
+```
diff --git a/test/test_subgroups/test_help[verbose-ConfigWithFrozen---conf=even --a 100 --help].md b/test/test_subgroups/test_help[verbose-ConfigWithFrozen---conf=even --a 100 --help].md
new file mode 100644
index 0000000..e9fd3ef
--- /dev/null
+++ b/test/test_subgroups/test_help[verbose-ConfigWithFrozen---conf=even --a 100 --help].md
@@ -0,0 +1,33 @@
+# Regression file for [this test](test/test_subgroups.py:724)
+
+Given Source code:
+
+```python
+@dataclasses.dataclass
+class ConfigWithFrozen(TestSetup):
+    conf: FrozenConfig = subgroups({"odd": odd, "even": even}, default=odd)
+
+```
+
+and command: '--conf=even --a 100 --help'
+
+We expect to get:
+
+```console
+usage: pytest [-h] [--conf {odd,even}] [-a int] [-b str]
+
+options:
+  -h, --help         show this help message and exit
+
+ConfigWithFrozen ['config_with_frozen']:
+  ConfigWithFrozen(conf: 'FrozenConfig' = 'odd')
+
+  --conf {odd,even}  (default: odd)
+
+FrozenConfig ['config_with_frozen.conf']:
+  FrozenConfig(a: 'int' = 1, b: 'str' = 'bob')
+
+  -a int, --a int    (default: 2)
+  -b str, --b str    (default: even)
+
+```
diff --git a/test/test_subgroups/test_help[verbose-ConfigWithFrozen---conf=even --help].md b/test/test_subgroups/test_help[verbose-ConfigWithFrozen---conf=even --help].md
new file mode 100644
index 0000000..a2b877e
--- /dev/null
+++ b/test/test_subgroups/test_help[verbose-ConfigWithFrozen---conf=even --help].md
@@ -0,0 +1,33 @@
+# Regression file for [this test](test/test_subgroups.py:724)
+
+Given Source code:
+
+```python
+@dataclasses.dataclass
+class ConfigWithFrozen(TestSetup):
+    conf: FrozenConfig = subgroups({"odd": odd, "even": even}, default=odd)
+
+```
+
+and command: '--conf=even --help'
+
+We expect to get:
+
+```console
+usage: pytest [-h] [--conf {odd,even}] [-a int] [-b str]
+
+options:
+  -h, --help         show this help message and exit
+
+ConfigWithFrozen ['config_with_frozen']:
+  ConfigWithFrozen(conf: 'FrozenConfig' = 'odd')
+
+  --conf {odd,even}  (default: odd)
+
+FrozenConfig ['config_with_frozen.conf']:
+  FrozenConfig(a: 'int' = 1, b: 'str' = 'bob')
+
+  -a int, --a int    (default: 2)
+  -b str, --b str    (default: even)
+
+```
diff --git a/test/test_subgroups/test_help[verbose-ConfigWithFrozen---conf=odd --a 123 --help].md b/test/test_subgroups/test_help[verbose-ConfigWithFrozen---conf=odd --a 123 --help].md
new file mode 100644
index 0000000..6e3d8b9
--- /dev/null
+++ b/test/test_subgroups/test_help[verbose-ConfigWithFrozen---conf=odd --a 123 --help].md
@@ -0,0 +1,33 @@
+# Regression file for [this test](test/test_subgroups.py:724)
+
+Given Source code:
+
+```python
+@dataclasses.dataclass
+class ConfigWithFrozen(TestSetup):
+    conf: FrozenConfig = subgroups({"odd": odd, "even": even}, default=odd)
+
+```
+
+and command: '--conf=odd --a 123 --help'
+
+We expect to get:
+
+```console
+usage: pytest [-h] [--conf {odd,even}] [-a int] [-b str]
+
+options:
+  -h, --help         show this help message and exit
+
+ConfigWithFrozen ['config_with_frozen']:
+  ConfigWithFrozen(conf: 'FrozenConfig' = 'odd')
+
+  --conf {odd,even}  (default: odd)
+
+FrozenConfig ['config_with_frozen.conf']:
+  FrozenConfig(a: 'int' = 1, b: 'str' = 'bob')
+
+  -a int, --a int    (default: 1)
+  -b str, --b str    (default: odd)
+
+```
diff --git a/test/test_subgroups/test_help[verbose-ConfigWithFrozen---conf=odd --help].md b/test/test_subgroups/test_help[verbose-ConfigWithFrozen---conf=odd --help].md
new file mode 100644
index 0000000..7a715e3
--- /dev/null
+++ b/test/test_subgroups/test_help[verbose-ConfigWithFrozen---conf=odd --help].md
@@ -0,0 +1,33 @@
+# Regression file for [this test](test/test_subgroups.py:724)
+
+Given Source code:
+
+```python
+@dataclasses.dataclass
+class ConfigWithFrozen(TestSetup):
+    conf: FrozenConfig = subgroups({"odd": odd, "even": even}, default=odd)
+
+```
+
+and command: '--conf=odd --help'
+
+We expect to get:
+
+```console
+usage: pytest [-h] [--conf {odd,even}] [-a int] [-b str]
+
+options:
+  -h, --help         show this help message and exit
+
+ConfigWithFrozen ['config_with_frozen']:
+  ConfigWithFrozen(conf: 'FrozenConfig' = 'odd')
+
+  --conf {odd,even}  (default: odd)
+
+FrozenConfig ['config_with_frozen.conf']:
+  FrozenConfig(a: 'int' = 1, b: 'str' = 'bob')
+
+  -a int, --a int    (default: 1)
+  -b str, --b str    (default: odd)
+
+```
diff --git a/test/test_subgroups/test_help[verbose-ConfigWithFrozen---help].md b/test/test_subgroups/test_help[verbose-ConfigWithFrozen---help].md
new file mode 100644
index 0000000..ee19ae6
--- /dev/null
+++ b/test/test_subgroups/test_help[verbose-ConfigWithFrozen---help].md
@@ -0,0 +1,33 @@
+# Regression file for [this test](test/test_subgroups.py:724)
+
+Given Source code:
+
+```python
+@dataclasses.dataclass
+class ConfigWithFrozen(TestSetup):
+    conf: FrozenConfig = subgroups({"odd": odd, "even": even}, default=odd)
+
+```
+
+and command: '--help'
+
+We expect to get:
+
+```console
+usage: pytest [-h] [--conf {odd,even}] [-a int] [-b str]
+
+options:
+  -h, --help         show this help message and exit
+
+ConfigWithFrozen ['config_with_frozen']:
+  ConfigWithFrozen(conf: 'FrozenConfig' = 'odd')
+
+  --conf {odd,even}  (default: odd)
+
+FrozenConfig ['config_with_frozen.conf']:
+  FrozenConfig(a: 'int' = 1, b: 'str' = 'bob')
+
+  -a int, --a int    (default: 1)
+  -b str, --b str    (default: odd)
+
+```
diff --git a/test/test_suppress.py b/test/test_suppress.py
index a93b8fd..673536d 100644
--- a/test/test_suppress.py
+++ b/test/test_suppress.py
@@ -30,7 +30,7 @@ def test_suppress_default(simple_attribute, silent):
     assert parser.parse_args(shlex.split("--b 0")) == argparse.Namespace(some_class={"b": 0})
 
 
-def test_suppress_with_unsuppressed_dataclass(simple_attribute, silent):
+def test_with_unsuppressed_dataclass(simple_attribute, silent):
     some_type, passed_value, expected_value = simple_attribute
 
     @dataclass
@@ -47,27 +47,22 @@ def test_suppress_with_unsuppressed_dataclass(simple_attribute, silent):
     parser.add_arguments(SomeClass1, dest="some_class1", default=argparse.SUPPRESS)
     parser.add_arguments(SomeClass2, dest="some_class2")
 
-    parsed_args, unparsed_args = parser.parse_known_args(
-        shlex.split(f"--a {passed_value} --b 0 --c {passed_value} --c {passed_value}")
-    )
+    parsed_args = parser.parse_args(shlex.split(f"--a {passed_value} --b 0 --c {passed_value}"))
     assert parsed_args == argparse.Namespace(
         some_class1={"a": expected_value, "b": 0},
-        some_class2=SomeClass2(expected_value),
+        some_class2=SomeClass2(c=expected_value),
     )
-    assert unparsed_args == []
 
-    parsed_args, unparsed_args = parser.parse_known_args(
-        shlex.split(f"--a {passed_value} --c {passed_value}")
-    )
+    parsed_args = parser.parse_args(shlex.split(f"--a {passed_value} --c {passed_value}"))
     assert parsed_args == argparse.Namespace(
-        some_class1={"a": expected_value}, some_class2=SomeClass2(expected_value)
+        some_class1={"a": expected_value},
+        some_class2=SomeClass2(c=expected_value),
     )
-    assert unparsed_args == []
 
-    with raises_missing_required_arg():
-        parser.parse_known_args(shlex.split(f"--a {passed_value}"))
+    with raises_missing_required_arg("-c/--c"):
+        parser.parse_args(shlex.split(f"--a {passed_value}"))
 
-    with raises_missing_required_arg():
+    with raises_missing_required_arg("-c/--c"):
         parser.parse_args("")
 
     assert parser.parse_args(shlex.split(f"--c {passed_value}")) == argparse.Namespace(
@@ -75,7 +70,7 @@ def test_suppress_with_unsuppressed_dataclass(simple_attribute, silent):
     )
 
 
-def test_suppress_conflict_with_unsuppressed_dataclass(simple_attribute, silent):
+def test_conflict_with_unsuppressed_dataclass(simple_attribute, silent):
     some_type, passed_value, expected_value = simple_attribute
 
     @dataclass
@@ -87,15 +82,15 @@ def test_suppress_conflict_with_unsuppressed_dataclass(simple_attribute, silent)
     parser.add_arguments(SomeClass, dest="some_class1", default=argparse.SUPPRESS)
     parser.add_arguments(SomeClass, dest="some_class2")
 
-    parsed_args, unparsed_args = parser.parse_known_args(
+    parsed_args = parser.parse_args(
         shlex.split(
             f"--some_class1.a {passed_value} --some_class1.b 0 --some_class2.a {passed_value}"
         )
     )
     assert parsed_args == argparse.Namespace(
-        some_class1={"a": expected_value, "b": 0}, some_class2=SomeClass(expected_value)
+        some_class1={"a": expected_value, "b": 0},
+        some_class2=SomeClass(expected_value),
     )
-    assert unparsed_args == []
 
     parsed_args, unparsed_args = parser.parse_known_args(
         shlex.split(f"--a {passed_value} --some_class2.a {passed_value}")
@@ -103,10 +98,10 @@ def test_suppress_conflict_with_unsuppressed_dataclass(simple_attribute, silent)
     assert parsed_args == argparse.Namespace(some_class2=SomeClass(expected_value))
     assert unparsed_args == shlex.split(f"--a {passed_value}")
 
-    with raises_missing_required_arg():
+    with raises_missing_required_arg("-some_class2.a/--some_class2.a"):
         parser.parse_known_args(shlex.split(f"--a {passed_value}"))
 
-    with raises_missing_required_arg():
+    with raises_missing_required_arg("-some_class2.a/--some_class2.a"):
         parser.parse_args("")
 
     assert parser.parse_args(shlex.split(f"--some_class2.a {passed_value}")) == argparse.Namespace(
@@ -114,7 +109,7 @@ def test_suppress_conflict_with_unsuppressed_dataclass(simple_attribute, silent)
     )
 
 
-def test_suppress_with_regular_suppressed_args(simple_attribute, silent):
+def test_with_regular_suppressed_args(simple_attribute, silent):
     some_type, passed_value, expected_value = simple_attribute
 
     @dataclass
@@ -129,23 +124,17 @@ def test_suppress_with_regular_suppressed_args(simple_attribute, silent):
     else:
         parser.add_argument("--c", type=some_type, default=argparse.SUPPRESS)
 
-    parsed_args, unparsed_args = parser.parse_known_args(
-        shlex.split(f"--a {passed_value} --b 0 --c {passed_value}")
-    )
+    parsed_args = parser.parse_args(shlex.split(f"--a {passed_value} --b 0 --c {passed_value}"))
     assert parsed_args == argparse.Namespace(
         some_class={"a": expected_value, "b": 0}, c=expected_value
     )
-    assert unparsed_args == []
 
     parsed_args, unparsed_args = parser.parse_known_args(shlex.split(f"--a {passed_value}"))
     assert parsed_args == argparse.Namespace(some_class={"a": expected_value})
     assert unparsed_args == []
 
-    parsed_args, unparsed_args = parser.parse_known_args(
-        shlex.split(f"--a {passed_value} --c {passed_value}")
-    )
+    parsed_args = parser.parse_args(shlex.split(f"--a {passed_value} --c {passed_value}"))
     assert parsed_args == argparse.Namespace(some_class={"a": expected_value}, c=expected_value)
-    assert unparsed_args == []
 
     assert parser.parse_args("") == argparse.Namespace()
     assert parser.parse_args(shlex.split("--b 0")) == argparse.Namespace(some_class={"b": 0})
@@ -166,23 +155,16 @@ def test_suppress_with_regular_unsuppressed_args(simple_attribute, silent):
     else:
         parser.add_argument("--c", type=some_type)
 
-    parsed_args, unparsed_args = parser.parse_known_args(
-        shlex.split(f"--a {passed_value} --b 0 --c {passed_value}")
-    )
+    parsed_args = parser.parse_args(shlex.split(f"--a {passed_value} --b 0 --c {passed_value}"))
     assert parsed_args == argparse.Namespace(
         some_class={"a": expected_value, "b": 0}, c=expected_value
     )
-    assert unparsed_args == []
 
-    parsed_args, unparsed_args = parser.parse_known_args(shlex.split(f"--a {passed_value}"))
+    parsed_args = parser.parse_args(shlex.split(f"--a {passed_value}"))
     assert parsed_args == argparse.Namespace(some_class={"a": expected_value}, c=None)
-    assert unparsed_args == []
 
-    parsed_args, unparsed_args = parser.parse_known_args(
-        shlex.split(f"--a {passed_value} --c {passed_value}")
-    )
+    parsed_args = parser.parse_args(shlex.split(f"--a {passed_value} --c {passed_value}"))
     assert parsed_args == argparse.Namespace(some_class={"a": expected_value}, c=expected_value)
-    assert unparsed_args == []
 
     assert parser.parse_args("") == argparse.Namespace(c=None)
     assert parser.parse_args(shlex.split("--b 0")) == argparse.Namespace(