Repository: pantsbuild/pants

Issue:
Invalid option enum values lead to parse error that does not list valid choices.
For example:
```console
$ ./pants dependencies --type=x src/python/pants/util/::
09:44:50.91 [ERROR] Error computing value for --type in scope 'dependencies' (may also be from PANTS_* environment variables).
Caused by:
ValueError: 'x' is not a valid DependencyType

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/option/parser.py", line 582, in to_value_type
    return type_arg(val_str)
  File "/usr/lib/python3.7/enum.py", line 315, in __call__
    return cls.__new__(cls, value)
  File "/usr/lib/python3.7/enum.py", line 569, in __new__
    raise exc
  File "/usr/lib/python3.7/enum.py", line 553, in __new__
    result = cls._missing_(value)
  File "/usr/lib/python3.7/enum.py", line 582, in _missing_
    raise ValueError("%r is not a valid %s" % (value, cls.__name__))
ValueError: 'x' is not a valid DependencyType

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/option/parser.py", line 284, in parse_args
    dest, kwargs, flag_vals, parse_args_request.passthrough_args
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/option/parser.py", line 673, in _compute_value
    flag_vals = [to_value_type(expand(x)) for x in flag_val_strs]
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/option/parser.py", line 673, in <listcomp>
    flag_vals = [to_value_type(expand(x)) for x in flag_val_strs]
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/option/parser.py", line 617, in to_value_type
    return self.to_value_type(val_str, type_arg, member_type, dest)
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/option/parser.py", line 585, in to_value_type
    f"Error applying type '{type_arg.__name__}' to option value '{val_str}', "
pants.option.errors.ParseError: Error applying type 'DependencyType' to option value 'x', for option '--type' in scope 'dependencies': 'x' is not a valid DependencyType
ValueError: 'x' is not a valid DependencyType

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/option/parser.py", line 582, in to_value_type
    return type_arg(val_str)
  File "/usr/lib/python3.7/enum.py", line 315, in __call__
    return cls.__new__(cls, value)
  File "/usr/lib/python3.7/enum.py", line 569, in __new__
    raise exc
  File "/usr/lib/python3.7/enum.py", line 553, in __new__
    result = cls._missing_(value)
  File "/usr/lib/python3.7/enum.py", line 582, in _missing_
    raise ValueError("%r is not a valid %s" % (value, cls.__name__))
ValueError: 'x' is not a valid DependencyType

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/option/parser.py", line 284, in parse_args
    dest, kwargs, flag_vals, parse_args_request.passthrough_args
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/option/parser.py", line 673, in _compute_value
    flag_vals = [to_value_type(expand(x)) for x in flag_val_strs]
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/option/parser.py", line 673, in <listcomp>
    flag_vals = [to_value_type(expand(x)) for x in flag_val_strs]
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/option/parser.py", line 617, in to_value_type
    return self.to_value_type(val_str, type_arg, member_type, dest)
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/option/parser.py", line 585, in to_value_type
    f"Error applying type '{type_arg.__name__}' to option value '{val_str}', "
pants.option.errors.ParseError: Error applying type 'DependencyType' to option value 'x', for option '--type' in scope 'dependencies': 'x' is not a valid DependencyType

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/bin/daemon_pants_runner.py", line 130, in single_daemonized_run
    cancellation_latch=cancellation_latch,
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/bin/local_pants_runner.py", line 143, in create
    options.for_scope(scope)
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/util/memo.py", line 123, in memoize
    result = func(*args, **kwargs)
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/option/options.py", line 422, in for_scope
    values = self._parser_hierarchy.get_parser_by_scope(scope).parse_args(parse_args_request)
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/option/parser.py", line 294, in parse_args
    f"Error computing value for {args_str} in {self._scope_str()} (may also be "
pants.option.errors.ParseError: Error computing value for --type in scope 'dependencies' (may also be from PANTS_* environment variables).
Caused by:
ValueError: 'x' is not a valid DependencyType

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/option/parser.py", line 582, in to_value_type
    return type_arg(val_str)
  File "/usr/lib/python3.7/enum.py", line 315, in __call__
    return cls.__new__(cls, value)
  File "/usr/lib/python3.7/enum.py", line 569, in __new__
    raise exc
  File "/usr/lib/python3.7/enum.py", line 553, in __new__
    result = cls._missing_(value)
  File "/usr/lib/python3.7/enum.py", line 582, in _missing_
    raise ValueError("%r is not a valid %s" % (value, cls.__name__))
ValueError: 'x' is not a valid DependencyType

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/option/parser.py", line 284, in parse_args
    dest, kwargs, flag_vals, parse_args_request.passthrough_args
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/option/parser.py", line 673, in _compute_value
    flag_vals = [to_value_type(expand(x)) for x in flag_val_strs]
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/option/parser.py", line 673, in <listcomp>
    flag_vals = [to_value_type(expand(x)) for x in flag_val_strs]
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/option/parser.py", line 617, in to_value_type
    return self.to_value_type(val_str, type_arg, member_type, dest)
  File "/home/jsirois/dev/pantsbuild/jsirois-pants/src/python/pants/option/parser.py", line 585, in to_value_type
    f"Error applying type '{type_arg.__name__}' to option value '{val_str}', "
pants.option.errors.ParseError: Error applying type 'DependencyType' to option value 'x', for option '--type' in scope 'dependencies': 'x' is not a valid DependencyType


(Use --no-process-execution-local-cleanup to inspect chroots and/or -ldebug for more logs. See https://www.pantsbuild.org/v2.6/docs/troubleshooting for common issues. Consider reaching out for help: https://www.pantsbuild.org/v2.6/docs/getting-help.)
```

This is as opposed to, for example, straight up argparse which gives something like this:
```console
$ pex --venv bob
usage: pex [-o OUTPUT.PEX] [options] [-- arg1 arg2 ...]

pex builds a PEX (Python Executable) file based on the given specifications: sources, requirements, their dependencies and other options.
Command-line options can be provided in one or more files by prefixing the filenames with an @ symbol. These files must contain one argument per line.
pex: error: argument --venv: invalid choice: 'bob' (choose from 'prepend', 'append')
```


Discussion:

Resolving patch:
diff --git a/src/python/pants/option/parser.py b/src/python/pants/option/parser.py
--- a/src/python/pants/option/parser.py
+++ b/src/python/pants/option/parser.py
@@ -8,7 +8,6 @@
 import json
 import os
 import re
-import traceback
 import typing
 from collections import defaultdict
 from dataclasses import dataclass
@@ -292,7 +291,7 @@ def add_flag_val(v: int | float | bool | str | None) -> None:
                 args_str = ", ".join(args)
                 raise type(e)(
                     f"Error computing value for {args_str} in {self._scope_str()} (may also be "
-                    f"from PANTS_* environment variables).\nCaused by:\n{traceback.format_exc()}"
+                    f"from PANTS_* environment variables).\nCaused by:\n{e}"
                 )
 
             # If the option is explicitly given, check deprecation and mutual exclusion.
@@ -581,9 +580,11 @@ def to_value_type(self, val_str, type_arg, member_type, dest):
                 return DictValueComponent.create(val_str)
             return type_arg(val_str)
         except (TypeError, ValueError) as e:
+            if issubclass(type_arg, Enum):
+                choices = ", ".join(f"{choice.value}" for choice in type_arg)
+                raise ParseError(f"Invalid choice '{val_str}'. Choose from: {choices}")
             raise ParseError(
-                f"Error applying type '{type_arg.__name__}' to option value '{val_str}', "
-                f"for option '--{dest}' in {self._scope_str()}: {e}"
+                f"Error applying type '{type_arg.__name__}' to option value '{val_str}': {e}"
             )
 
     @classmethod


Test patch:
diff --git a/src/python/pants/option/options_test.py b/src/python/pants/option/options_test.py
--- a/src/python/pants/option/options_test.py
+++ b/src/python/pants/option/options_test.py
@@ -1310,13 +1310,22 @@ def test_drop_flag_values(self) -> None:
         )
 
     def test_enum_option_type_parse_error(self) -> None:
-        self.maxDiff = None
         with pytest.raises(ParseError) as exc:
             options = self._parse(flags="enum-opt --some-enum=invalid-value")
-            options.for_scope("enum-opt").some_enum
+            options.for_scope("enum-opt")
+
+        assert (
+            "Invalid choice 'invalid-value'. "
+            "Choose from: a-value, another-value, yet-another, one-more"
+        ) in str(exc.value)
+
+    def test_non_enum_option_type_parse_error(self) -> None:
+        with pytest.raises(ParseError) as exc:
+            options = self._parse(flags="--a=not-a-number")
+            options.for_global_scope()
+
         assert (
-            "Error applying type 'SomeEnumOption' to option value 'invalid-value', for option "
-            "'--some_enum' in scope 'enum-opt'"
+            "Error applying type 'int' to option value 'not-a-number': invalid literal for int()"
         ) in str(exc.value)
 
     def test_mutually_exclusive_options(self) -> None: