diff --git a/Doc/library/argparse.rst b/Doc/library/argparse.rst --- a/Doc/library/argparse.rst +++ b/Doc/library/argparse.rst @@ -1048,9 +1048,9 @@ usage: doors.py [-h] {1,2,3} doors.py: error: argument door: invalid choice: 4 (choose from 1, 2, 3) -Any object that supports the ``in`` operator can be passed as the *choices* -value, so :class:`dict` objects, :class:`set` objects, custom containers, -etc. are all supported. +Any :term:`iterable` can be passed as the *choices* value, so :class:`dict` +objects, :class:`set` objects, custom :term:`sequences `, etc. +are all supported. required diff --git a/Lib/argparse.py b/Lib/argparse.py --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -567,23 +567,30 @@ return (result, ) * tuple_size return format + def _format_nargs(self, nargs, get_metavar): + if nargs is None: + result = '%s' % get_metavar(1) + elif nargs == OPTIONAL: + result = '[%s]' % get_metavar(1) + elif nargs == ZERO_OR_MORE: + result = '[%s [%s ...]]' % get_metavar(2) + elif nargs == ONE_OR_MORE: + result = '%s [%s ...]' % get_metavar(2) + elif nargs == REMAINDER: + result = '...' + elif nargs == PARSER: + result = '%s ...' % get_metavar(1) + else: + formats = ['%s' for _ in range(nargs)] + result = ' '.join(formats) % get_metavar(nargs) + return result + def _format_args(self, action, default_metavar): get_metavar = self._metavar_formatter(action, default_metavar) - if action.nargs is None: - result = '%s' % get_metavar(1) - elif action.nargs == OPTIONAL: - result = '[%s]' % get_metavar(1) - elif action.nargs == ZERO_OR_MORE: - result = '[%s [%s ...]]' % get_metavar(2) - elif action.nargs == ONE_OR_MORE: - result = '%s [%s ...]' % get_metavar(2) - elif action.nargs == REMAINDER: - result = '...' - elif action.nargs == PARSER: - result = '%s ...' % get_metavar(1) - else: - formats = ['%s' for _ in range(action.nargs)] - result = ' '.join(formats) % get_metavar(action.nargs) + try: + result = self._format_nargs(action.nargs, get_metavar) + except TypeError: + raise ValueError("length of metavar tuple does not match nargs") return result def _expand_help(self, action): @@ -1325,12 +1332,10 @@ if not callable(type_func): raise ValueError('%r is not callable' % (type_func,)) - # raise an error if the metavar does not match the type + # raise an error if the metavar length is not consistent with nargs + # or if the choices is not iterable, for example. if hasattr(self, "_get_formatter"): - try: - self._get_formatter()._format_args(action, None) - except TypeError: - raise ValueError("length of metavar tuple does not match nargs") + self._get_formatter()._format_args(action, None) return self._add_action(action) diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -4885,6 +4885,24 @@ def test_nargs_3_metavar_length3(self): self.do_test_no_exception(nargs=3, metavar=("1", "2", "3")) +# ========================== +# add_argument choices tests +# ========================== + +class TestAddArgumentChoices(TestCase): + + def test_non_iterable_choices(self): + """Check that adding a non-iterable choices argument raises TypeError. + This also checks that it is not sufficient to support just the "in" + operator (e.g. by implementing __contains__). See issue #16468.""" + class NonIterableChoices(object): + def __contains__(self, item): + return True + parser = argparse.ArgumentParser() + self.assertRaises(TypeError, parser.add_argument, "foo", + choices=NonIterableChoices()) + + # ============================ # from argparse import * tests # ============================