Repository: lebrice/SimpleParsing

Issue:
Default not displayed in the help text when no description comment exists
This example:
```
from dataclasses import dataclass

from simple_parsing import ArgumentParser

parser = ArgumentParser()


@dataclass
class Options:
    """These are the options"""

    foo: str = "aaa"  # Description
    bar: str = "bbb"


parser.add_arguments(Options, dest="options")

args = parser.parse_args(["--help"])
```

outputs:

```
usage: scratch.py [-h] [--foo str] [--bar str]

optional arguments:
  -h, --help  show this help message and exit

Options ['options']:
  These are the options

  --foo str   Description (default: aaa)
  --bar str
```

Why is the default value displayed for `foo`, but not for `bar`? Even when no description comment is given, it would be useful to know the default value set, I would think?

Discussion:

Patch:
diff --git a/examples/simple/basic.py b/examples/simple/basic.py
index 401c415..c2c06e4 100644
--- a/examples/simple/basic.py
+++ b/examples/simple/basic.py
@@ -34,22 +34,23 @@ optional arguments:
 HParams ['hparams']:
    Set of options for the training of a Model.
 
-  --num_layers int
-  --num_units int
-  --optimizer str
+  --num_layers int      (default: 4)
+  --num_units int       (default: 64)
+  --optimizer str       (default: ADAM)
   --learning_rate float
+                        (default: 0.001)
 """
 
 
 print(parser.equivalent_argparse_code())
 expected += """
-parser = ArgumentParser()
+parser = ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
 
 group = parser.add_argument_group(title="HParams ['hparams']", description="Set of options for the training of a Model.")
-group.add_argument(*['--num_layers'], **{'type': int, 'help': None, 'required': False, 'dest': 'hparams.num_layers', 'default': 4})
-group.add_argument(*['--num_units'], **{'type': int, 'help': None, 'required': False, 'dest': 'hparams.num_units', 'default': 64})
-group.add_argument(*['--optimizer'], **{'type': str, 'help': None, 'required': False, 'dest': 'hparams.optimizer', 'default': 'ADAM'})
-group.add_argument(*['--learning_rate'], **{'type': float, 'help': None, 'required': False, 'dest': 'hparams.learning_rate', 'default': 0.001})
+group.add_argument(*['--num_layers'], **{'type': int, 'help': ' ', 'required': False, 'dest': 'hparams.num_layers', 'default': 4})
+group.add_argument(*['--num_units'], **{'type': int, 'help': ' ', 'required': False, 'dest': 'hparams.num_units', 'default': 64})
+group.add_argument(*['--optimizer'], **{'type': str, 'help': ' ', 'required': False, 'dest': 'hparams.optimizer', 'default': 'ADAM'})
+group.add_argument(*['--learning_rate'], **{'type': float, 'help': ' ', 'required': False, 'dest': 'hparams.learning_rate', 'default': 0.001})
 
 args = parser.parse_args()
 print(args)
diff --git a/examples/simple/choice.py b/examples/simple/choice.py
index ab21ece..4f0627b 100644
--- a/examples/simple/choice.py
+++ b/examples/simple/choice.py
@@ -30,8 +30,10 @@ optional arguments:
 HParams ['hparams']:
    Set of options for the training of a Model.
 
-  --num_layers int
-  --num_units int
+  --num_layers int      (default: 4)
+  --num_units int       (default: 64)
   --optimizer {ADAM,SGD,RMSPROP}
+                        (default: ADAM)
   --learning_rate float
-"""
\ No newline at end of file
+                        (default: 0.001)
+"""
diff --git a/examples/simple/inheritance.py b/examples/simple/inheritance.py
index 34b8613..0dc4cce 100644
--- a/examples/simple/inheritance.py
+++ b/examples/simple/inheritance.py
@@ -47,23 +47,24 @@ MAML ['hparams']:
   Overwrites some of the default values and adds new arguments/attributes.
       
 
-  --num_layers int
-  --num_units int
-  --optimizer str
+  --num_layers int      (default: 6)
+  --num_units int       (default: 128)
+  --optimizer str       (default: ADAM)
   --learning_rate float
+                        (default: 0.001)
   --name str            method (default: MAML)
 """
 
 
 print(parser.equivalent_argparse_code())
 expected += """
-parser = ArgumentParser()
+parser = ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
 
 group = parser.add_argument_group(title="MAML ['hparams']", description="Overwrites some of the default values and adds new arguments/attributes.")
-group.add_argument(*['--num_layers'], **{'type': int, 'help': None, 'required': False, 'dest': 'hparams.num_layers', 'default': 6})
-group.add_argument(*['--num_units'], **{'type': int, 'help': None, 'required': False, 'dest': 'hparams.num_units', 'default': 128})
-group.add_argument(*['--optimizer'], **{'type': str, 'help': None, 'required': False, 'dest': 'hparams.optimizer', 'default': 'ADAM'})
-group.add_argument(*['--learning_rate'], **{'type': float, 'help': None, 'required': False, 'dest': 'hparams.learning_rate', 'default': 0.001})
+group.add_argument(*['--num_layers'], **{'type': int, 'help': ' ', 'required': False, 'dest': 'hparams.num_layers', 'default': 6})
+group.add_argument(*['--num_units'], **{'type': int, 'help': ' ', 'required': False, 'dest': 'hparams.num_units', 'default': 128})
+group.add_argument(*['--optimizer'], **{'type': str, 'help': ' ', 'required': False, 'dest': 'hparams.optimizer', 'default': 'ADAM'})
+group.add_argument(*['--learning_rate'], **{'type': float, 'help': ' ', 'required': False, 'dest': 'hparams.learning_rate', 'default': 0.001})
 group.add_argument(*['--name'], **{'type': str, 'help': 'method', 'required': False, 'dest': 'hparams.name', 'default': 'MAML'})
 
 args = parser.parse_args()
diff --git a/examples/simple/option_strings.py b/examples/simple/option_strings.py
index 0f598b5..14f1401 100644
--- a/examples/simple/option_strings.py
+++ b/examples/simple/option_strings.py
@@ -30,9 +30,13 @@ HParams ['hparams']:
    Set of options for the training of a Model. 
 
   -n int, --num_layers int
+                        (default: 4)
   -u int, --num_units int
+                        (default: 64)
   -o str, --opt str, --optimizer str
+                        (default: ADAM)
   -lr float, --learning_rate float
+                        (default: 0.001)
 """
 
 # Now if we wanted to also be able to set the arguments using their full paths:
@@ -49,7 +53,11 @@ HParams ['hparams']:
    Set of options for the training of a Model. 
 
   -n int, --num_layers int, --hparams.num_layers int
+                        (default: 4)
   -u int, --num_units int, --hparams.num_units int
+                        (default: 64)
   -o str, --opt str, --optimizer str, --hparams.optimizer str
+                        (default: ADAM)
   -lr float, --learning_rate float, --hparams.learning_rate float
-"""
\ No newline at end of file
+                        (default: 0.001)
+"""
diff --git a/examples/simple/reuse.py b/examples/simple/reuse.py
index 3ba4324..a2e824d 100644
--- a/examples/simple/reuse.py
+++ b/examples/simple/reuse.py
@@ -45,18 +45,26 @@ optional arguments:
   -h, --help            show this help message and exit
 
 HParams ['train']:
-  Set of options for the training of a Model.
+   Set of options for the training of a Model.
 
   --train.num_layers int
+                        (default: 4)
   --train.num_units int
+                        (default: 64)
   --train.optimizer str
+                        (default: ADAM)
   --train.learning_rate float
+                        (default: 0.001)
 
 HParams ['valid']:
-  Set of options for the training of a Model.
+   Set of options for the training of a Model.
 
   --valid.num_layers int
+                        (default: 4)
   --valid.num_units int
+                        (default: 64)
   --valid.optimizer str
+                        (default: ADAM)
   --valid.learning_rate float
+                        (default: 0.001)
 """
\ No newline at end of file
diff --git a/simple_parsing/parsing.py b/simple_parsing/parsing.py
index 057eaf6..dc46ea3 100644
--- a/simple_parsing/parsing.py
+++ b/simple_parsing/parsing.py
@@ -192,7 +192,7 @@ class ArgumentParser(argparse.ArgumentParser):
             A string containing the auto-generated argparse code.
         """
         self._preprocessing()
-        code = f"parser = ArgumentParser()"
+        code = f"parser = ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)"
         for wrapper in self._wrappers:
             code += "\n"
             code += wrapper.equivalent_argparse_code()
diff --git a/simple_parsing/wrappers/field_wrapper.py b/simple_parsing/wrappers/field_wrapper.py
index f926251..582ea1c 100644
--- a/simple_parsing/wrappers/field_wrapper.py
+++ b/simple_parsing/wrappers/field_wrapper.py
@@ -154,7 +154,13 @@ class FieldWrapper(Wrapper[dataclasses.Field]):
         # TODO: [Improvement] @lebrice Maybe overhaul the 'type' function, in a
         # similar way as with is done with fields in the Serializable class. 
         _arg_options["type"] = get_parsing_fn(self.type)
-        _arg_options["help"] = self.help
+        if self.help:
+            _arg_options["help"] = self.help
+        elif self.default is not None:
+            # issue 64: Need to add an empty 'help' string, so that the formatter
+            # automatically adds the (default: '123')
+            _arg_options["help"] = " "
+
         _arg_options["required"] = self.required
         _arg_options["dest"] = self.dest
         _arg_options["default"] = self.default


Test patch:
diff --git a/test/test_issue64.py b/test/test_issue64.py
new file mode 100644
index 0000000..63fdabd
--- /dev/null
+++ b/test/test_issue64.py
@@ -0,0 +1,105 @@
+from dataclasses import dataclass
+
+from simple_parsing import ArgumentParser
+from io import StringIO
+import textwrap
+import pytest
+
+@dataclass
+class Options:
+    """These are the options"""
+
+    foo: str = "aaa"  # Description
+    bar: str = "bbb"
+
+
+@pytest.mark.xfail(reason="Issue64 is solved below.")
+def test_reproduce_issue64():
+
+    parser = ArgumentParser("issue64")
+
+    parser.add_arguments(Options, dest="options")
+
+    # args = parser.parse_args(["--help"])
+
+    s = StringIO()
+    parser.print_help(file=s)
+    s.seek(0)
+
+    assert s.read() == textwrap.dedent(
+        """\
+    usage: issue64 [-h] [--foo str] [--bar str]
+
+    optional arguments:
+      -h, --help  show this help message and exit
+
+    Options ['options']:
+      These are the options
+
+      --foo str   Description (default: aaa)
+      --bar str
+    """
+    )
+
+
+def test_vanilla_argparse_issue64():
+    """ This test shows that the ArgumentDefaultsHelpFormatter of argparse doesn't add
+    the "(default: xyz)" if the 'help' argument isn't already passed!
+
+    This begs the question: Should simple-parsing add a 'help' argument always, so that
+    the formatter can then add the default string after?
+    """
+    import argparse
+    parser = ArgumentParser("issue64", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
+
+    group = parser.add_argument_group("Options ['options']", description="These are the options")
+    group.add_argument("--foo", type=str, metavar="str", default="aaa", help="Description")
+    group.add_argument("--bar", type=str, metavar="str", default="bbb")
+
+    from io import StringIO
+    s = StringIO()
+    parser.print_help(file=s)
+    s.seek(0)
+
+    assert s.read() == textwrap.dedent(
+        """\
+    usage: issue64 [-h] [--foo str] [--bar str]
+
+    optional arguments:
+      -h, --help  show this help message and exit
+
+    Options ['options']:
+      These are the options
+
+      --foo str   Description (default: aaa)
+      --bar str
+    """
+    )
+
+
+def test_solved_issue64():
+    """ test that shows that Issue 64 is solved now, by adding a single space as the
+    'help' argument, the help formatter can then add the "(default: bbb)" after the
+    argument.
+    """
+    parser = ArgumentParser("issue64")
+    parser.add_arguments(Options, dest="options")
+
+    s = StringIO()
+    parser.print_help(file=s)
+    s.seek(0)
+
+    assert s.read() == textwrap.dedent(
+        """\
+    usage: issue64 [-h] [--foo str] [--bar str]
+
+    optional arguments:
+      -h, --help  show this help message and exit
+
+    Options ['options']:
+      These are the options
+
+      --foo str   Description (default: aaa)
+      --bar str   (default: bbb)
+    """
+    )
diff --git a/test/test_tuples.py b/test/test_tuples.py
index f42f5df..8430b1c 100644
--- a/test/test_tuples.py
+++ b/test/test_tuples.py
@@ -50,7 +50,7 @@ def test_tuple_with_ellipsis_help_format():
         test_tuple_with_ellipsis_help_format.<locals>.Container ['container']:
           Container(ints: Tuple[int, ...] = (1, 2, 3))
         
-          --ints int [int, ...]
+          --ints int [int, ...]   (default: (1, 2, 3))
         """)
 
 def test_each_type_is_used_correctly():
@@ -75,7 +75,7 @@ def test_each_type_is_used_correctly():
     test_each_type_is_used_correctly.<locals>.Container ['container']:
     A container with mixed items in a tuple. 
 
-      --mixed int str bool float
+      --mixed int str bool float   (default: (1, 'bob', False, 1.23))
     """)