From 3b6d43782a0b95d3085f2bb7143f85ef8a4d6ac7 Mon Sep 17 00:00:00 2001 From: "ivan.prado" Date: Fri, 8 Apr 2022 17:13:46 +0200 Subject: [PATCH 1/6] force_full_path option to use always full paths to fields as option string --- simple_parsing/parsing.py | 11 ++++++++++ simple_parsing/wrappers/field_wrapper.py | 28 +++++++++++++++++++++--- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/simple_parsing/parsing.py b/simple_parsing/parsing.py index bf81fceb..f6804469 100644 --- a/simple_parsing/parsing.py +++ b/simple_parsing/parsing.py @@ -13,6 +13,7 @@ from .help_formatter import SimpleHelpFormatter from .utils import Dataclass from .wrappers import DataclassWrapper, FieldWrapper, DashVariant +from .wrappers.field_wrapper import FullPathMode logger = getLogger(__name__) @@ -28,6 +29,7 @@ def __init__( conflict_resolution: ConflictResolution = ConflictResolution.AUTO, add_option_string_dash_variants: DashVariant = DashVariant.AUTO, add_dest_to_option_strings: bool = False, + force_full_path: FullPathMode = FullPathMode.DISABLED, formatter_class: Type[HelpFormatter] = SimpleHelpFormatter, **kwargs, ): @@ -64,6 +66,14 @@ def __init__( `conflict_resolution` and each field ends up with a unique option string. + - force_full_path: FullPathMode, optional + + Whether or not to force the full path to be used for the option string. + When set to FullPathMode.DISABLED (default), the option string is as + flatten as possible. When set to FullPathMode.FULL, the option string + is the full path to the field. Sometimes is desired to ignore the + first level. Use FullPathMode.FULL_WITHOUT_ROOT in such cases. + - formatter_class : Type[HelpFormatter], optional The formatter class to use. By default, uses @@ -86,6 +96,7 @@ def __init__( self._preprocessing_done: bool = False FieldWrapper.add_dash_variants = add_option_string_dash_variants FieldWrapper.add_dest_to_option_strings = add_dest_to_option_strings + FieldWrapper.force_full_path = force_full_path @overload def add_arguments( diff --git a/simple_parsing/wrappers/field_wrapper.py b/simple_parsing/wrappers/field_wrapper.py index 408c12b5..20d2a949 100644 --- a/simple_parsing/wrappers/field_wrapper.py +++ b/simple_parsing/wrappers/field_wrapper.py @@ -2,7 +2,7 @@ import typing import dataclasses import inspect -from enum import Enum +from enum import Enum, auto from logging import getLogger from typing import cast, ClassVar, Any, Optional, List, Type, Dict, Set, Union, Tuple @@ -19,6 +19,19 @@ logger = getLogger(__name__) +class FullPathMode(Enum): + """ + Whether or not to force the full path to be used for the option string. + When set to FullPathMode.DISABLED (default), the option string is as + flatten as possible. When set to FullPathMode.FULL, the option string + is the full path to the field. Sometimes is desired to ignore the + first level. Use FullPathMode.FULL_WITHOUT_ROOT in such cases. + """ + DISABLED = auto() + FULL = auto() + FULL_WITHOUT_ROOT = auto() + + class DashVariant(Enum): """Specifies whether to prefer only '_', both '_'/'-', or only '-', for cmd-line-flags. @@ -66,6 +79,9 @@ class FieldWrapper(Wrapper[dataclasses.Field]): # Whether to add the `dest` to the list of option strings. add_dest_to_option_strings: ClassVar[bool] = True + # Whether to force all options to use its full path + force_full_path: ClassVar[FullPathMode] = FullPathMode.DISABLED + def __init__(self, field: dataclasses.Field, parent: Any = None, prefix: str = ""): super().__init__(wrapped=field, name=field.name) self.field: dataclasses.Field = field @@ -528,12 +544,18 @@ def option_strings(self) -> List[str]: return [self.dest] dashes.append(dash) - options.append(option) + force_full_path = type(self).force_full_path + base_option = option + if force_full_path == FullPathMode.FULL: + base_option = self.dest + elif force_full_path == FullPathMode.FULL_WITHOUT_ROOT: + base_option = ".".join(self.dest.split(".")[1:]) + options.append(base_option) if dash == "-": # also add a double-dash option: dashes.append("--") - options.append(option) + options.append(base_option) # add all the aliases that were passed to the `field` function. for alias in self.aliases: From 2bf19c072a405d3c0dfd16affaaad999a2f05252 Mon Sep 17 00:00:00 2001 From: "ivan.prado" Date: Mon, 11 Apr 2022 14:08:14 +0200 Subject: [PATCH 2/6] `add_dest_to_option_strings` removed in favour of `argument_generation_mode` and `nested_mode` --- examples/simple/option_strings.py | 3 +- simple_parsing/parsing.py | 51 ++++++++------- simple_parsing/wrappers/field_wrapper.py | 68 +++++++++++-------- test/test_generation_mode.py | 83 ++++++++++++++++++++++++ test/test_issue_46.py | 4 +- test/testutils.py | 7 ++ 6 files changed, 164 insertions(+), 52 deletions(-) create mode 100644 test/test_generation_mode.py diff --git a/examples/simple/option_strings.py b/examples/simple/option_strings.py index 6be3184e..c6ddb814 100644 --- a/examples/simple/option_strings.py +++ b/examples/simple/option_strings.py @@ -1,5 +1,6 @@ from dataclasses import dataclass from simple_parsing import ArgumentParser, choice, field +from simple_parsing.wrappers.field_wrapper import ArgumentGenerationMode @dataclass @@ -42,7 +43,7 @@ class HParams: """ # Now if we wanted to also be able to set the arguments using their full paths: -parser = ArgumentParser(add_dest_to_option_strings=True) +parser = ArgumentParser(argument_generation_mode=ArgumentGenerationMode.BOTH) parser.add_arguments(HParams, dest="hparams") parser.print_help() expected += """ diff --git a/simple_parsing/parsing.py b/simple_parsing/parsing.py index f6804469..4a029308 100644 --- a/simple_parsing/parsing.py +++ b/simple_parsing/parsing.py @@ -13,7 +13,7 @@ from .help_formatter import SimpleHelpFormatter from .utils import Dataclass from .wrappers import DataclassWrapper, FieldWrapper, DashVariant -from .wrappers.field_wrapper import FullPathMode +from .wrappers.field_wrapper import ArgumentGenerationMode, NestedMode logger = getLogger(__name__) @@ -28,8 +28,8 @@ def __init__( *args, conflict_resolution: ConflictResolution = ConflictResolution.AUTO, add_option_string_dash_variants: DashVariant = DashVariant.AUTO, - add_dest_to_option_strings: bool = False, - force_full_path: FullPathMode = FullPathMode.DISABLED, + argument_generation_mode = ArgumentGenerationMode.FLAT, + nested_mode: NestedMode = NestedMode.DEFAULT, formatter_class: Type[HelpFormatter] = SimpleHelpFormatter, **kwargs, ): @@ -51,28 +51,33 @@ def __init__( "--no-cache" and "--no_cache" can both be used to point to the same attribute `no_cache` on some dataclass. - - add_dest_to_option_strings: bool, optional + - argument_generation_mode : ArgumentGenerationMode, optional - Whether or not to add the `dest` of each field to the list of option - strings for the argument. - When True (default), each field can be referenced using either the - auto-generated option string or the full 'destination' of the field - in the resulting namespace. - When False, only uses the auto-generated option strings. + How to generate the arguments. In the ArgumentGenerationMode.FLAT mode, + the default one, the arguments are flat when possible, ignoring + their nested structure and including it only on the presence of a + conflict. - The auto-generated option strings are usually just the field names, - except when there are multiple arguments with the same name. In this - case, the conflicts are resolved as determined by the value of - `conflict_resolution` and each field ends up with a unique option - string. + In the ArgumentGenerationMode.NESTED mode, the arguments are always + composed reflecting their nested structure. - - force_full_path: FullPathMode, optional + In the ArgumentGenerationMode.BOTH mode, both kind of arguments + are generated. - Whether or not to force the full path to be used for the option string. - When set to FullPathMode.DISABLED (default), the option string is as - flatten as possible. When set to FullPathMode.FULL, the option string - is the full path to the field. Sometimes is desired to ignore the - first level. Use FullPathMode.FULL_WITHOUT_ROOT in such cases. + - nested_mode : NestedMode, optional + + How to handle argument generation in for nested arguments + in the modes ArgumentGenerationMode.NESTED and ArgumentGenerationMode.BOTH. + In the NestedMode.DEFAULT mode, the nested arguments are generated + reflecting their full 'destination' path from the returning namespace. + + In the NestedMode.WITHOUT_ROOT, the first level is removed. This is useful + because sometimes the first level is uninformative. For example, + 'args' is redundant and worthless for the arguments + '--args.input.path --args.output.path'. + We would prefer to remove the root level in such a case + so that the arguments get generated as + '--input.path --output.path'. - formatter_class : Type[HelpFormatter], optional @@ -95,8 +100,8 @@ def __init__( self._preprocessing_done: bool = False FieldWrapper.add_dash_variants = add_option_string_dash_variants - FieldWrapper.add_dest_to_option_strings = add_dest_to_option_strings - FieldWrapper.force_full_path = force_full_path + FieldWrapper.argument_generation_mode = argument_generation_mode + FieldWrapper.nested_mode = nested_mode @overload def add_arguments( diff --git a/simple_parsing/wrappers/field_wrapper.py b/simple_parsing/wrappers/field_wrapper.py index 20d2a949..011351b5 100644 --- a/simple_parsing/wrappers/field_wrapper.py +++ b/simple_parsing/wrappers/field_wrapper.py @@ -19,17 +19,27 @@ logger = getLogger(__name__) -class FullPathMode(Enum): +class ArgumentGenerationMode(Enum): """ - Whether or not to force the full path to be used for the option string. - When set to FullPathMode.DISABLED (default), the option string is as - flatten as possible. When set to FullPathMode.FULL, the option string - is the full path to the field. Sometimes is desired to ignore the - first level. Use FullPathMode.FULL_WITHOUT_ROOT in such cases. + Enum for argument generation modes. """ - DISABLED = auto() - FULL = auto() - FULL_WITHOUT_ROOT = auto() + # Tries to generate flat arguments, removing the argument destination path when possible. + FLAT = auto() + # Generates arguments with their full destination path. + NESTED = auto() + # Generates both the flat and nested arguments. + BOTH = auto() + + +class NestedMode(Enum): + """ + Controls how nested arguments are generated. + """ + # By default, the full destination path is used. + DEFAULT = auto() + # The full destination path is used, but the first level is removed. + # Useful because sometimes the first level is uninformative (i.e. 'args'). + WITHOUT_ROOT = auto() class DashVariant(Enum): @@ -76,11 +86,11 @@ class FieldWrapper(Wrapper[dataclasses.Field]): # TODO: This can often make "--help" messages a bit crowded add_dash_variants: ClassVar[DashVariant] = DashVariant.AUTO - # Whether to add the `dest` to the list of option strings. - add_dest_to_option_strings: ClassVar[bool] = True + # Whether to follow a flat or nested argument structure. + argument_generation_mode: ClassVar[ArgumentGenerationMode] = ArgumentGenerationMode.FLAT - # Whether to force all options to use its full path - force_full_path: ClassVar[FullPathMode] = FullPathMode.DISABLED + # Controls how nested arguments are generated. + nested_mode: ClassVar[NestedMode] = NestedMode.DEFAULT def __init__(self, field: dataclasses.Field, parent: Any = None, prefix: str = ""): super().__init__(wrapped=field, name=field.name) @@ -531,11 +541,20 @@ def option_strings(self) -> List[str]: dashes: List[str] = [] # contains the leading dashes. options: List[str] = [] # contains the name following the dashes. + def add_args(dash:str, candidates: List[str]) -> None: + for candidate in candidates: + options.append(candidate) + dashes.append(dash) + # Handle user passing us "True" or "only" directly. add_dash_variants = DashVariant(FieldWrapper.add_dash_variants) + gen_mode = type(self).argument_generation_mode + nested_mode = type(self).nested_mode + dash = "-" if len(self.name) == 1 else "--" option = f"{self.prefix}{self.name}" + nested_option = self.dest if nested_mode == NestedMode.DEFAULT else ".".join(self.dest.split(".")[1:]) if add_dash_variants == DashVariant.DASH: option = option.replace("_", "-") @@ -543,19 +562,18 @@ def option_strings(self) -> List[str]: # Can't be positional AND have flags at same time. Also, need dest to be be this and not just option. return [self.dest] - dashes.append(dash) - force_full_path = type(self).force_full_path - base_option = option - if force_full_path == FullPathMode.FULL: - base_option = self.dest - elif force_full_path == FullPathMode.FULL_WITHOUT_ROOT: - base_option = ".".join(self.dest.split(".")[1:]) - options.append(base_option) + if gen_mode == ArgumentGenerationMode.FLAT: + candidates = [option] + elif gen_mode == ArgumentGenerationMode.NESTED: + candidates = [nested_option] + else: + candidates = [option, nested_option] + + add_args(dash, candidates) if dash == "-": # also add a double-dash option: - dashes.append("--") - options.append(base_option) + add_args("--", candidates) # add all the aliases that were passed to the `field` function. for alias in self.aliases: @@ -587,10 +605,6 @@ def option_strings(self) -> List[str]: options.extend(additional_options) dashes.extend(additional_dashes) - if type(self).add_dest_to_option_strings: - dashes.append("-" if len(self.dest) == 1 else "--") - options.append(self.dest) - # remove duplicates by creating a set. option_strings = set(f"{dash}{option}" for dash, option in zip(dashes, options)) # TODO: possibly sort the option strings, if argparse doesn't do it diff --git a/test/test_generation_mode.py b/test/test_generation_mode.py new file mode 100644 index 00000000..ad7c431a --- /dev/null +++ b/test/test_generation_mode.py @@ -0,0 +1,83 @@ +from dataclasses import dataclass +from pathlib import Path + +import pytest + +from simple_parsing.wrappers.field_wrapper import ArgumentGenerationMode, NestedMode +from . import TestSetup + + +@dataclass +class ModelOptions: + path: str + device: str + + +@dataclass +class ServerOptions(TestSetup): + host: str + port: int + model: ModelOptions + + +def assert_as_expected(options: ServerOptions): + assert isinstance(options, ServerOptions) + assert options.host == "myserver" + assert options.port == 80 + assert options.model.path == "a_path" + assert options.model.device == "cpu" + + +def test_flat(): + options = ServerOptions.setup( + "--host myserver " "--port 80 " "--path a_path " "--device cpu", + ) + assert_as_expected(options) + + with pytest.raises(SystemExit): + ServerOptions.setup( + "--opts.host myserver " "--opts.port 80 " "--opts.model.path a_path " "--opts.model.device cpu", + dest="opts" + ) + + +@pytest.mark.parametrize("without_root", [True, False]) +def test_both(without_root): + options = ServerOptions.setup( + "--host myserver " "--port 80 " "--path a_path " "--device cpu", + dest="opts", + argument_generation_mode=ArgumentGenerationMode.BOTH + ) + assert_as_expected(options) + + args = "--opts.host myserver " "--opts.port 80 " "--opts.model.path a_path " "--opts.model.device cpu" + if without_root: + args = args.replace("opts.", "") + options = ServerOptions.setup( + args, + dest="opts", + argument_generation_mode=ArgumentGenerationMode.BOTH, + nested_mode=NestedMode.WITHOUT_ROOT if without_root else NestedMode.DEFAULT, + ) + assert_as_expected(options) + + +@pytest.mark.parametrize("without_root", [True, False]) +def test_nested(without_root): + with pytest.raises(SystemExit): + options = ServerOptions.setup( + "--host myserver " "--port 80 " "--path a_path " "--device cpu", + dest="opts", + argument_generation_mode=ArgumentGenerationMode.NESTED + ) + + args = "--opts.host myserver " "--opts.port 80 " "--opts.model.path a_path " "--opts.model.device cpu" + if without_root: + args = args.replace("opts.", "") + options = ServerOptions.setup( + args, + dest="opts", + argument_generation_mode=ArgumentGenerationMode.NESTED, + nested_mode=NestedMode.WITHOUT_ROOT if without_root else NestedMode.DEFAULT, + ) + assert_as_expected(options) diff --git a/test/test_issue_46.py b/test/test_issue_46.py index 068ce975..3c09783a 100644 --- a/test/test_issue_46.py +++ b/test/test_issue_46.py @@ -3,6 +3,8 @@ import textwrap import pytest +from simple_parsing.wrappers.field_wrapper import ArgumentGenerationMode + @dataclass class JBuildRelease: @@ -47,7 +49,7 @@ def test_issue_46(assert_equals_stdout): def test_issue_46_solution2(assert_equals_stdout): # This (now) works: - parser = simple_parsing.ArgumentParser(add_dest_to_option_strings=True) + parser = simple_parsing.ArgumentParser(argument_generation_mode=ArgumentGenerationMode.BOTH) parser.add_argument("--run_id", type=str) parser.add_arguments(JBuildRelease, dest="jbuild", prefix="jbuild.") diff --git a/test/testutils.py b/test/testutils.py index 605cf320..a6b9af65 100644 --- a/test/testutils.py +++ b/test/testutils.py @@ -29,6 +29,7 @@ ) from simple_parsing.utils import camel_case from simple_parsing.wrappers import DataclassWrapper +from simple_parsing.wrappers.field_wrapper import ArgumentGenerationMode, NestedMode xfail = pytest.mark.xfail parametrize = pytest.mark.parametrize @@ -135,6 +136,10 @@ def setup( add_option_string_dash_variants: DashVariant = DashVariant.AUTO, parse_known_args: bool = False, attempt_to_reorder: bool = False, + *, + argument_generation_mode: ArgumentGenerationMode = ArgumentGenerationMode.FLAT, + nested_mode: NestedMode = NestedMode.DEFAULT, + ) -> Dataclass: """Basic setup for a test. @@ -148,6 +153,8 @@ def setup( parser = simple_parsing.ArgumentParser( conflict_resolution=conflict_resolution_mode, add_option_string_dash_variants=add_option_string_dash_variants, + argument_generation_mode=argument_generation_mode, + nested_mode=nested_mode, ) if dest is None: dest = camel_case(cls.__name__) From a7f72239d8a2ec0f7d17caff8225a320cd1de6de Mon Sep 17 00:00:00 2001 From: "ivan.prado" Date: Thu, 21 Apr 2022 09:22:36 +0200 Subject: [PATCH 3/6] Improved nested_mode doc --- simple_parsing/parsing.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/simple_parsing/parsing.py b/simple_parsing/parsing.py index 4e5bb0c1..91b2ae82 100644 --- a/simple_parsing/parsing.py +++ b/simple_parsing/parsing.py @@ -71,11 +71,12 @@ def __init__( In the NestedMode.DEFAULT mode, the nested arguments are generated reflecting their full 'destination' path from the returning namespace. - In the NestedMode.WITHOUT_ROOT, the first level is removed. This is useful - because sometimes the first level is uninformative. For example, - 'args' is redundant and worthless for the arguments + In the NestedMode.WITHOUT_ROOT, the first level is removed. This is useful when + parser.add_arguments is only called once, and where the same prefix would be shared + by all arguments. For example, if you have a single dataclass MyArguments and + you call parser.add_arguments(MyArguments, "args"), the arguments could look like this: '--args.input.path --args.output.path'. - We would prefer to remove the root level in such a case + We could prefer to remove the root level in such a case so that the arguments get generated as '--input.path --output.path'. From 367b5bc3fbb3c512fda27d2492a0fc491c43dc76 Mon Sep 17 00:00:00 2001 From: "ivan.prado" Date: Thu, 21 Apr 2022 09:27:43 +0200 Subject: [PATCH 4/6] Improve test_generation_mode --- test/test_generation_mode.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/test/test_generation_mode.py b/test/test_generation_mode.py index ad7c431a..c329cbdf 100644 --- a/test/test_generation_mode.py +++ b/test/test_generation_mode.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from pathlib import Path import pytest @@ -20,19 +19,14 @@ class ServerOptions(TestSetup): model: ModelOptions -def assert_as_expected(options: ServerOptions): - assert isinstance(options, ServerOptions) - assert options.host == "myserver" - assert options.port == 80 - assert options.model.path == "a_path" - assert options.model.device == "cpu" +expected = ServerOptions(host="myserver", port=80, model=ModelOptions(path="a_path", device="cpu")) def test_flat(): options = ServerOptions.setup( "--host myserver " "--port 80 " "--path a_path " "--device cpu", ) - assert_as_expected(options) + assert options == expected with pytest.raises(SystemExit): ServerOptions.setup( @@ -48,7 +42,7 @@ def test_both(without_root): dest="opts", argument_generation_mode=ArgumentGenerationMode.BOTH ) - assert_as_expected(options) + assert options == expected args = "--opts.host myserver " "--opts.port 80 " "--opts.model.path a_path " "--opts.model.device cpu" if without_root: @@ -59,7 +53,7 @@ def test_both(without_root): argument_generation_mode=ArgumentGenerationMode.BOTH, nested_mode=NestedMode.WITHOUT_ROOT if without_root else NestedMode.DEFAULT, ) - assert_as_expected(options) + assert options == expected @pytest.mark.parametrize("without_root", [True, False]) @@ -80,4 +74,4 @@ def test_nested(without_root): argument_generation_mode=ArgumentGenerationMode.NESTED, nested_mode=NestedMode.WITHOUT_ROOT if without_root else NestedMode.DEFAULT, ) - assert_as_expected(options) + assert options == expected From 799c996207d16ee60cd395d0dfcae31bbebda025 Mon Sep 17 00:00:00 2001 From: "ivan.prado" Date: Fri, 29 Apr 2022 13:29:31 +0200 Subject: [PATCH 5/6] Fix a bug introduced in https://github.com/lebrice/SimpleParsing/pull/117. It affects the DASH option in the nested mode. --- simple_parsing/wrappers/field_wrapper.py | 1 + 1 file changed, 1 insertion(+) diff --git a/simple_parsing/wrappers/field_wrapper.py b/simple_parsing/wrappers/field_wrapper.py index 3a69823c..9f120582 100644 --- a/simple_parsing/wrappers/field_wrapper.py +++ b/simple_parsing/wrappers/field_wrapper.py @@ -559,6 +559,7 @@ def add_args(dash:str, candidates: List[str]) -> None: nested_option = self.dest if nested_mode == NestedMode.DEFAULT else ".".join(self.dest.split(".")[1:]) if add_dash_variants == DashVariant.DASH: option = option.replace("_", "-") + nested_option = nested_option.replace("_", "-") if self.field.metadata.get("positional"): # Can't be positional AND have flags at same time. Also, need dest to be be this and not just option. From e910083846b70813ecee4e4490a370f01ef7a719 Mon Sep 17 00:00:00 2001 From: "ivan.prado" Date: Fri, 29 Apr 2022 16:06:53 +0200 Subject: [PATCH 6/6] Including a test case --- test/test_custom_args.py | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/test/test_custom_args.py b/test/test_custom_args.py index c738f29d..3782e560 100644 --- a/test/test_custom_args.py +++ b/test/test_custom_args.py @@ -164,30 +164,56 @@ def test_store_false_action(): def test_only_dashes(): + @dataclass + class AClass(TestSetup): + """foo""" + + a_var: int + + @dataclass class SomeClass(TestSetup): """lol""" my_var: int + a: AClass assert_help_output_equals( SomeClass.get_help_text(add_option_string_dash_variants=DashVariant.DASH), textwrap.dedent( """\ - usage: pytest [-h] --my-var int + usage: pytest [-h] --my-var int --a-var int optional arguments: - -h, --help show this help message and exit - + -h, --help show this help message and exit + test_only_dashes..SomeClass ['some_class']: - lol - - --my-var int + lol + + --my-var int + + test_only_dashes..AClass ['some_class.a']: + foo + + --a-var int """ ), ) - sc = SomeClass.setup("--my-var 2", add_option_string_dash_variants=DashVariant.DASH) + sc = SomeClass.setup("--my-var 2 --a-var 3", add_option_string_dash_variants=DashVariant.DASH) + assert sc.my_var == 2 + assert sc.a.a_var == 3 + sc = SomeClass.setup("--some-class.my-var 2 --some-class.a.a-var 3", + add_option_string_dash_variants=DashVariant.DASH, + argument_generation_mode=ArgumentGenerationMode.NESTED) assert sc.my_var == 2 + assert sc.a.a_var == 3 + sc = SomeClass.setup("--my-var 2 --a.a-var 3", + add_option_string_dash_variants=DashVariant.DASH, + argument_generation_mode=ArgumentGenerationMode.NESTED, + nested_mode=NestedMode.WITHOUT_ROOT) + assert sc.my_var == 2 + assert sc.a.a_var == 3 + def test_list_of_choices():