Repository: lebrice/SimpleParsing

Issue:
Lists of enums are parsed by value on the command line
**Describe the bug**
While enums seem to be parsed by member name, a list of them seems to be parsed by member value, but only if the value is a string, not an integer  

**To Reproduce**
```python
import enum
from typing import *
from dataclasses import dataclass

from simple_parsing import ArgumentParser
from simple_parsing.helpers import list_field

parser = ArgumentParser()

class Color(enum.Enum):
    RED = "RED"
    ORANGE = "ORANGE"
    BLUE = "BLUE"

class Temperature(enum.Enum):
    HOT = 1
    WARM = 0
    COLD = -1
    MONTREAL = -35

@dataclass
class MyPreferences:
    """You can use Enums"""
    color: Color = Color.BLUE # my favorite colour
    # a list of colors
    color_list: List[Color] = list_field(Color.ORANGE)
    # pick a temperature
    temp: Temperature = Temperature.WARM
    # a list of temperatures
    temp_list: List[Temperature] = list_field(Temperature.COLD, Temperature.WARM)

parser.add_arguments(MyPreferences, "my_preferences")
args = parser.parse_args()
prefs: MyPreferences = args.my_preferences
print(prefs)
```

**Expected behavior**
A clear and concise description of what you expected to happen.

```console
$ python issue.py --color ORANGE --color_list RED BLUE --temp MONTREAL
MyPreferences(color=<Color.ORANGE: 'ORANGE'>, color_list=[<Color.RED: 'RED'>, <Color.BLUE: 'BLUE'>], temp=<Temperature.MONTREAL: -35>, temp_list=[<Temperature.COLD: -1>, <Temperature.WARM: 0>])
$ python issue.py --color ORANGE --color_list RED BLUE --temp MONTREAL --temp_list MONTREAL
MyPreferences(color=<Color.ORANGE: 'ORANGE'>, color_list=[<Color.RED: 'RED'>, <Color.BLUE: 'BLUE'>], temp=<Temperature.MONTREAL: -35>, temp_list=[<Temperature.MONTREAL: -35>])
```

**Actual behavior**
A clear and concise description of what is happening.

```console
$ python issue.py --color ORANGE --color_list RED BLUE --temp MONTREAL --temp_list MONTREAL
usage: enums.py [-h] [--color Color] [--color_list Color] [--temp Temperature]
                [--temp_list Temperature]
enums.py: error: argument --temp_list: invalid Temperature value: 'MONTREAL'
```

**Desktop (please complete the following information):**
 - Version 0.0.15.post1
 - Python version: 3.9.0

**Additional context**
If I add the proper encoders and decoders, I can load and save both kinds of enum lists from .json files just fine:
```
@encode.register
def encode_Color(obj: Color) -> str:
    return obj.name

register_decoding_fn(Color, Color)

@encode.register
def encode_Temperature(obj: Temperature) -> str:
    return obj.name

register_decoding_fn(Temperature, lambda temp: Temperature[temp])
```

Discussion:

Patch:
diff --git a/simple_parsing/utils.py b/simple_parsing/utils.py
index 56f41da..88b14ff 100644
--- a/simple_parsing/utils.py
+++ b/simple_parsing/utils.py
@@ -187,8 +187,8 @@ def get_item_type(container_type: Type[Container[T]]) -> T:
 
 
 def get_argparse_type_for_container(
-    container_type: Type,
-) -> Union[Type, Callable[[str], bool]]:
+    container_type: Type[Container[T]],
+) -> Union[Type[T], Callable[[str], T]]:
     """Gets the argparse 'type' option to be used for a given container type.
     When an annotation is present, the 'type' option of argparse is set to that type.
     if not, then the default value of 'str' is returned.
@@ -208,6 +208,12 @@ def get_argparse_type_for_container(
         return str2bool
     if T is Any:
         return str
+    if is_enum(T):
+        # IDEA: Fix this weirdness by first moving all this weird parsing logic into the
+        # field wrapper class, and then split it up into different subclasses of FieldWrapper,
+        # each for a different type of field.
+        from simple_parsing.wrappers.field_parsing import parse_enum
+        return parse_enum(T)
     return T
 
 
diff --git a/simple_parsing/wrappers/field_parsing.py b/simple_parsing/wrappers/field_parsing.py
index 05bf312..09baf37 100644
--- a/simple_parsing/wrappers/field_parsing.py
+++ b/simple_parsing/wrappers/field_parsing.py
@@ -3,6 +3,8 @@
 Somewhat analogous to the 'parse' function in the
 `helpers.serialization.parsing` package.
 """
+import enum
+import functools
 from dataclasses import Field
 from logging import getLogger
 from typing import Any, Callable, Dict, List, Optional, Tuple, Type, TypeVar, Union
@@ -11,6 +13,7 @@ import typing_inspect as tpi
 
 from ..utils import (
     get_type_arguments,
+    is_enum,
     is_homogeneous_tuple_type,
     is_list,
     is_tuple,
@@ -122,7 +125,10 @@ def get_parsing_fn(t: Type[T]) -> Callable[[Any], T]:
         logger.debug(f"parsing a Union field: {t}")
         args = get_type_arguments(t)
         return parse_union(*args)
-
+    
+    elif is_enum(t):
+        logger.debug(f"Parsing an Enum field of type {t}")
+        return parse_enum(t)
     # import typing_inspect as tpi
     # from .serializable import get_dataclass_type_from_forward_ref, Serializable
 
@@ -237,7 +243,6 @@ def parse_tuple(
 
         return parsed_value
 
-    _parse_tuple.__name__ = "BOB"
 
     return _parse_tuple
 
@@ -256,3 +261,36 @@ def no_op(v: T) -> T:
         [type]: The value unchanged.
     """
     return v
+
+
+E = TypeVar("E", bound=enum.Enum)
+
+
+def parse_enum(enum_type: Type[E]) -> Callable[[str], E]:
+    """Returns a function to use to parse an enum of type `enum_type` from a string.
+
+    Parameters
+    ----------
+    - enum_type : Type[enum.Enum]
+
+        The type of enum to create a parsing function for.
+
+    Returns
+    -------
+    Callable[[str], E]
+        A function that parses an enum object of type `enum_type` from a string.
+    """
+    # Save the function, since the same type will always be parsed the same way. Also
+    # makes testing easier.
+    if enum_type in _parsing_fns:
+        return _parsing_fns[enum_type]
+    
+    # NOTE: Use `functools.wraps` so that fn name is the enum, so the metavar shows up
+    # just like the enum on the command-line, and not like
+    # "(...).parse_enum.<locals>._parse_enum" or something.
+    @functools.wraps(enum_type)
+    def _parse_enum(v: str) -> E:
+        return enum_type[v]
+    _parsing_fns[enum_type] = _parse_enum
+    return _parse_enum
+ 
\ No newline at end of file


Test patch:
diff --git a/test/test_base.py b/test/test_base.py
index fb12434..2e36962 100644
--- a/test/test_base.py
+++ b/test/test_base.py
@@ -244,3 +244,76 @@ def test_using_a_Type_type():
 
     foo = OtherFoo.setup("")
     assert foo.a == Extended()
+
+
+def test_issue62():
+    import enum
+    from simple_parsing.helpers import list_field
+    from typing import List
+    parser = ArgumentParser()
+
+    class Color(enum.Enum):
+        RED = "red"
+        ORANGE = "orange"
+        BLUE = "blue"
+
+    class Temperature(enum.Enum):
+        HOT = 1
+        WARM = 0
+        COLD = -1
+        MONTREAL = -35
+
+    @dataclass
+    class MyPreferences(TestSetup):
+        """You can use Enums"""
+
+        color: Color = Color.BLUE  # my favorite colour
+        # a list of colors
+        color_list: List[Color] = list_field(Color.ORANGE)
+        # Some floats.
+        floats: List[float] = list_field(1.1, 2.2, 3.3)
+        # pick a temperature
+        temp: Temperature = Temperature.WARM
+        # a list of temperatures
+        temp_list: List[Temperature] = list_field(Temperature.COLD, Temperature.WARM)
+
+    parser.add_arguments(MyPreferences, "my_preferences")
+    assert MyPreferences.setup(
+        "--color ORANGE --color_list RED BLUE --temp MONTREAL"
+    ) == MyPreferences(
+        color=Color.ORANGE,
+        color_list=[Color.RED, Color.BLUE],
+        temp=Temperature.MONTREAL,
+        temp_list=[Temperature.COLD, Temperature.WARM],
+    )
+    assert MyPreferences.setup(
+        "--color ORANGE --color_list RED BLUE --temp MONTREAL --temp_list MONTREAL HOT"
+    ) == MyPreferences(
+        color=Color.ORANGE,
+        color_list=[Color.RED, Color.BLUE],
+        temp=Temperature.MONTREAL,
+        temp_list=[Temperature.MONTREAL, Temperature.HOT],
+    )
+    assert Temperature["MONTREAL"] is Temperature.MONTREAL
+    assert Temperature(-35) is Temperature.MONTREAL
+
+    # NOTE: This kind of test (comparing the help str) is unreliable, changes depending on the
+    # python version.
+    # assert MyPreferences.get_help_text() == textwrap.dedent("""\
+    #     usage: pytest [-h] [--color Color] [--color_list Color] [--floats float]
+    #                   [--temp Temperature] [--temp_list Temperature]
+
+    #     optional arguments:
+    #       -h, --help            show this help message and exit
+
+    #     test_issue62.<locals>.MyPreferences ['my_preferences']:
+    #       You can use Enums
+
+    #       --color Color         my favorite colour (default: BLUE)
+    #       --color_list Color    a list of colors (default: [<Color.ORANGE: 'orange'>])
+    #       --floats float        Some floats. (default: [1.1, 2.2, 3.3])
+    #       --temp Temperature    pick a temperature (default: WARM)
+    #       --temp_list Temperature
+    #                             a list of temperatures (default: [<Temperature.COLD:
+    #                             -1>, <Temperature.WARM: 0>])
+    # """)
diff --git a/test/test_fields.py b/test/test_fields.py
index a026d3c..9056766 100644
--- a/test/test_fields.py
+++ b/test/test_fields.py
@@ -46,14 +46,14 @@ class Color(Enum):
 from simple_parsing.wrappers.field_parsing import get_parsing_fn
 
 from simple_parsing.utils import str2bool
-
+from simple_parsing.wrappers.field_parsing import parse_enum
 
 @pytest.mark.parametrize(
     "annotation, expected_options",
     [
         (Tuple[int, int], dict(nargs=2, type=int)),
-        (Tuple[Color, Color], dict(nargs=2, type=Color)),
-        (Optional[Tuple[Color, Color]], dict(nargs=2, type=Color, required=False)),
+        (Tuple[Color, Color], dict(nargs=2, type=parse_enum(Color))),
+        (Optional[Tuple[Color, Color]], dict(nargs=2, type=parse_enum(Color), required=False)),
         (List[str], dict(nargs="*", type=str)),
         (Optional[List[str]], dict(nargs="*", type=str, required=False)),
         (Optional[str], dict(nargs="?", type=str, required=False)),