def get_scope_data(scope): ret = [] for si in ScopeInfoIterator(self.context.options.known_scope_to_info).iterate([scope]): help_info = HelpInfoExtracter(si.scope).get_option_scope_help_info_from_parser( self.context.options.get_parser(si.scope)) ret.append({ # We don't use _asdict(), because then .description wouldn't be available. 'scope_info': si, # We do use _asdict() here, so our mustache library can do property expansion. 'help_info': help_info._asdict(), }) return ret
def gen_tasks_options_reference_data(options): """Generate the template data for the options reference rst doc.""" goal_dict = {} goal_names = [] for goal in Goal.all(): tasks = [] for task_name in goal.ordered_task_names(): task_type = goal.task_type_by_name(task_name) # task_type may actually be a synthetic subclass of the authored class from the source code. # We want to display the authored class's name in the docs. for authored_task_type in task_type.mro(): if authored_task_type.__module__ != 'abc': break doc_rst = indent_docstring_by_n(authored_task_type.__doc__ or '', 2) doc_html = rst_to_html(dedent_docstring(authored_task_type.__doc__)) parser = options.get_parser(task_type.options_scope) oschi = HelpInfoExtracter.get_option_scope_help_info_from_parser(parser) impl = '{0}.{1}'.format(authored_task_type.__module__, authored_task_type.__name__) tasks.append(TemplateData( impl=impl, doc_html=doc_html, doc_rst=doc_rst, ogroup=oref_template_data_from_help_info(oschi))) goal_dict[goal.name] = TemplateData(goal=goal, tasks=tasks) goal_names.append(goal.name) goals = [goal_dict[name] for name in sorted(goal_names, key=lambda x: x.lower())] return goals
def do_test(args, kwargs, expected_display_args, expected_scoped_cmd_line_args, expected_unscoped_cmd_line_args): ohi = HelpInfoExtracter('bar.baz').get_option_help_info( args, kwargs) self.assertListEqual(expected_display_args, ohi.display_args) self.assertListEqual(expected_scoped_cmd_line_args, ohi.scoped_cmd_line_args) self.assertListEqual(expected_unscoped_cmd_line_args, ohi.unscoped_cmd_line_args)
def do_test(args, kwargs, expected_display_args, expected_scoped_cmd_line_args): # The scoped and unscoped args are the same in global scope. expected_unscoped_cmd_line_args = expected_scoped_cmd_line_args ohi = HelpInfoExtracter('').get_option_help_info(args, kwargs) self.assertListEqual(expected_display_args, ohi.display_args) self.assertListEqual(expected_scoped_cmd_line_args, ohi.scoped_cmd_line_args) self.assertListEqual(expected_unscoped_cmd_line_args, ohi.unscoped_cmd_line_args)
def __init__(self, options: Options): super().__init__(options.for_global_scope().colors) self._bin_name = options.for_global_scope().pants_bin_name self._all_help_info = HelpInfoExtracter.get_all_help_info( options, # We only care about the options-related help info, so we pass in # dummy values for union_membership and consumed_scopes_mapper. UnionMembership({}), lambda x: tuple(), )
def __init__(self, options: Options): super().__init__(options.for_global_scope().colors) self._all_help_info = HelpInfoExtracter.get_all_help_info( options, # We only care about the options-related help info, so we pass in # dummy values for the other arguments. UnionMembership({}), lambda x: tuple(), RegisteredTargetTypes({}), )
def do_test(args, kwargs, expected_default): # Defaults are computed in the parser and added into the kwargs, so we # must jump through this hoop in this test. parser = Parser(env={}, config=Config.load([]), scope_info=GlobalOptionsRegistrar.get_scope_info(), parent_parser=None, option_tracker=OptionTracker()) parser.register(*args, **kwargs) oshi = HelpInfoExtracter.get_option_scope_help_info_from_parser(parser).basic self.assertEquals(1, len(oshi)) ohi = oshi[0] self.assertEqual(expected_default, ohi.default)
def fill_template(options, task_type): for authored_task_type in task_type.mro(): if authored_task_type.__module__ != "abc": break doc_rst = indent_docstring_by_n(authored_task_type.__doc__ or "", 2) doc_html = rst_to_html(dedent_docstring(authored_task_type.__doc__)) parser = options.get_parser(task_type.options_scope) oschi = HelpInfoExtracter.get_option_scope_help_info_from_parser(parser) impl = "{0}.{1}".format(authored_task_type.__module__, authored_task_type.__name__) return TemplateData( impl=impl, doc_html=doc_html, doc_rst=doc_rst, ogroup=oref_template_data_from_help_info(oschi) )
def get_scope_data(scope): ret = [] for si in ScopeInfoIterator(self.context.options.known_scope_to_info).iterate([scope]): help_info = HelpInfoExtracter(si.scope).get_option_scope_help_info_from_parser( self.context.options.get_parser(si.scope)) ret.append({ # We don't use _asdict(), because then .description wouldn't be available. 'scope_info': si, # We do use _asdict() here, so our mustache library can do property expansion. 'help_info': asdict(help_info), }) return ret
def do_test( args, kwargs, expected_display_args, expected_scoped_cmd_line_args, expected_unscoped_cmd_line_args, ): ohi = HelpInfoExtracter("bar.baz").get_option_help_info(args, kwargs) assert tuple(expected_display_args) == ohi.display_args assert tuple(expected_scoped_cmd_line_args) == ohi.scoped_cmd_line_args assert tuple( expected_unscoped_cmd_line_args) == ohi.unscoped_cmd_line_args
def do_test(kwargs, expected_basic=False, expected_recursive=False, expected_advanced=False): def exp_to_len(exp): return int(exp) # True -> 1, False -> 0. oshi = HelpInfoExtracter('').get_option_scope_help_info([([], kwargs)]) self.assertEqual(exp_to_len(expected_basic), len(oshi.basic)) self.assertEqual(exp_to_len(expected_recursive), len(oshi.recursive)) self.assertEqual(exp_to_len(expected_advanced), len(oshi.advanced))
def _format_for_global_scope(show_advanced, show_deprecated, args, kwargs): parser = Parser( env={}, config=Config.load([]), scope_info=GlobalOptions.get_scope_info(), parent_parser=None, ) parser.register(*args, **kwargs) # Force a parse to generate the derivation history. parser.parse_args(Parser.ParseArgsRequest((), OptionValueContainerBuilder(), [], False)) oshi = HelpInfoExtracter("").get_option_scope_help_info("", parser, False) return HelpFormatter( show_advanced=show_advanced, show_deprecated=show_deprecated, color=False ).format_options(oshi)
def fill_template(options, task_type): for authored_task_type in task_type.mro(): if authored_task_type.__module__ != 'abc': break doc_rst = indent_docstring_by_n(authored_task_type.__doc__ or '', 2) doc_html = rst_to_html(dedent_docstring(authored_task_type.__doc__)) parser = options.get_parser(task_type.options_scope) oschi = HelpInfoExtracter.get_option_scope_help_info_from_parser(parser) impl = '{0}.{1}'.format(authored_task_type.__module__, authored_task_type.__name__) return TemplateData( impl=impl, doc_html=doc_html, doc_rst=doc_rst, ogroup=oref_template_data_from_help_info(oschi))
def do_test(kwargs, expected_basic=False, expected_advanced=False): def exp_to_len(exp): return int(exp) # True -> 1, False -> 0. parser = Parser( env={}, config=Config.load([]), scope_info=GlobalOptions.get_scope_info(), ) parser.register("--foo", **kwargs) oshi = HelpInfoExtracter("").get_option_scope_help_info( "", parser, False) assert exp_to_len(expected_basic) == len(oshi.basic) assert exp_to_len(expected_advanced) == len(oshi.advanced)
def do_test(args, kwargs, expected_default_str): # Defaults are computed in the parser and added into the kwargs, so we # must jump through this hoop in this test. parser = Parser( env={}, config=Config.load([]), scope_info=GlobalOptions.get_scope_info(), ) parser.register(*args, **kwargs) oshi = HelpInfoExtracter(parser.scope).get_option_scope_help_info( "description", parser, False) assert oshi.description == "description" assert len(oshi.basic) == 1 ohi = oshi.basic[0] assert to_help_str(ohi.default) == expected_default_str
def get_from_parser(parser): oschi = HelpInfoExtracter.get_option_scope_help_info_from_parser(parser) # We ignore advanced options, as they aren't intended to be used on the cmd line. for ohi in oschi.basic: autocomplete_options_by_scope[oschi.scope].update(ohi.unscoped_cmd_line_args) autocomplete_options_by_scope[oschi.scope].update(ohi.scoped_cmd_line_args) # Autocomplete to this option in the enclosing goal scope, but exclude options registered # on us, but not by us, e.g., recursive options (which are registered by # GlobalOptionsRegisterer). # We exclude those because they are already registered on the goal scope anyway # (via the recursion) and it would be confusing and superfluous to have autocompletion # to both --goal-recursive-opt and --goal-task-recursive-opt in goal scope. if issubclass(ohi.registering_class, TaskBase): goal_scope = oschi.scope.partition(".")[0] autocomplete_options_by_scope[goal_scope].update(ohi.scoped_cmd_line_args)
def _print_help(self, request: HelpRequest) -> ExitCode: global_options = self.options.for_global_scope() all_help_info = HelpInfoExtracter.get_all_help_info( self.options, self.union_membership, self.graph_session.goal_consumed_subsystem_scopes, ) help_printer = HelpPrinter( bin_name=global_options.pants_bin_name, help_request=request, all_help_info=all_help_info, color=global_options.colors, ) return help_printer.print_help()
def run( self, build_config: BuildConfiguration, graph_session: GraphSession, options: Options, specs: Specs, union_membership: UnionMembership, ) -> ExitCode: for server_request_type in union_membership.get(ExplorerServerRequest): logger.info(f"Using {server_request_type.__name__} to create the explorer server.") break else: logger.error( softwrap( """ There is no Explorer backend server implementation registered. Activate a backend/plugin that registers an implementation for the `ExplorerServerRequest` union to fix this issue. """ ) ) return 127 all_help_info = HelpInfoExtracter.get_all_help_info( options, union_membership, graph_session.goal_consumed_subsystem_scopes, RegisteredTargetTypes.create(build_config.target_types), build_config, ) request_state = RequestState( all_help_info=all_help_info, build_configuration=build_config, scheduler_session=graph_session.scheduler_session, ) server_request = server_request_type( address=self.address, port=self.port, request_state=request_state, ) server = request_state.product_request( ExplorerServer, (server_request,), poll=True, timeout=90, ) return server.run()
def get_from_parser(parser): oschi = HelpInfoExtracter.get_option_scope_help_info_from_parser(parser) # We ignore advanced options, as they aren't intended to be used on the cmd line. option_help_infos = oschi.basic + oschi.recursive for ohi in option_help_infos: autocomplete_options_by_scope[oschi.scope].update(ohi.unscoped_cmd_line_args) autocomplete_options_by_scope[oschi.scope].update(ohi.scoped_cmd_line_args) # Autocomplete to this option in the enclosing goal scope, but exclude options registered # on us, but not by us, e.g., recursive options (which are registered by # GlobalOptionsRegisterer). # We exclude those because they are already registered on the goal scope anyway # (via the recursion) and it would be confusing and superfluous to have autocompletion # to both --goal-recursive-opt and --goal-task-recursive-opt in goal scope. if issubclass(ohi.registering_class, TaskBase): goal_scope = oschi.scope.partition('.')[0] autocomplete_options_by_scope[goal_scope].update(ohi.scoped_cmd_line_args)
def run( self, build_config: BuildConfiguration, graph_session: GraphSession, options: Options, specs: Specs, union_membership: UnionMembership, ) -> ExitCode: all_help_info = HelpInfoExtracter.get_all_help_info( options, union_membership, graph_session.goal_consumed_subsystem_scopes, RegisteredTargetTypes.create(build_config.target_types), build_config, ) global_options = options.for_global_scope() help_printer = HelpPrinter( help_request=self.create_help_request(options), all_help_info=all_help_info, color=global_options.colors, ) return help_printer.print_help()
def run(self, start_time: float) -> ExitCode: self._set_start_time(start_time) with maybe_profiled(self.profile_path): global_options = self.options.for_global_scope() streaming_handlers = global_options.streaming_workunits_handlers report_interval = global_options.streaming_workunits_report_interval callbacks = Subsystem.get_streaming_workunit_callbacks( streaming_handlers) streaming_reporter = StreamingWorkunitHandler( self.graph_session.scheduler_session, callbacks=callbacks, report_interval_seconds=report_interval, ) if self.options.help_request: all_help_info = HelpInfoExtracter.get_all_help_info( self.options, self.union_membership, self.graph_session.goal_consumed_subsystem_scopes, ) help_printer = HelpPrinter( bin_name=global_options.pants_bin_name, help_request=self.options.help_request, all_help_info=all_help_info, use_color=global_options.colors, ) return help_printer.print_help() with streaming_reporter.session(): engine_result = PANTS_FAILED_EXIT_CODE try: engine_result = self._run_v2() except Exception as e: ExceptionSink.log_exception(e) run_tracker_result = self._finish_run(engine_result) return self._merge_exit_codes(engine_result, run_tracker_result)
def format_options(self, scope, description, option_registrations_iter): """Return a help message for the specified options. :param scope: The options scope. :param description: The description of the scope. :param option_registrations_iter: An iterator over (args, kwargs) pairs, as passed in to options registration. """ oshi = HelpInfoExtracter( self._scope).get_option_scope_help_info(option_registrations_iter) lines = [] def add_option(ohis, *, category=None): lines.append("") display_scope = f"`{scope}`" if scope else "Global" if category: title = f"{display_scope} {category} options" lines.append(self._maybe_green(f"{title}\n{'-' * len(title)}")) else: title = f"{display_scope} options" lines.append(self._maybe_green(f"{title}\n{'-' * len(title)}")) if description: lines.append(f"\n{description}") lines.append(" ") if not ohis: lines.append("No options available.") return for ohi in ohis: lines.extend([*self.format_option(ohi), ""]) add_option(oshi.basic) if self._show_advanced: add_option(oshi.advanced, category="advanced") if self._show_deprecated: add_option(oshi.deprecated, category="deprecated") return [*lines, "\n"]
def test_get_all_help_info(): class Global(Subsystem): """Global options.""" options_scope = GLOBAL_SCOPE @classmethod def register_options(cls, register): register("-o", "--opt1", type=int, default=42, help="Option 1") class Foo(Subsystem): """A foo.""" options_scope = "foo" @classmethod def register_options(cls, register): register("--opt2", type=bool, default=True, help="Option 2") register("--opt3", advanced=True, choices=["a", "b", "c"]) class Bar(GoalSubsystem): """The bar goal.""" name = "bar" options = Options.create( env={}, config=Config.load_file_contents(""), known_scope_infos=[ Global.get_scope_info(), Foo.get_scope_info(), Bar.get_scope_info() ], args=["./pants"], bootstrap_option_values=None, ) Global.register_options_on_scope(options) Foo.register_options_on_scope(options) Bar.register_options_on_scope(options) def fake_consumed_scopes_mapper(scope: str) -> Tuple[str, ...]: return ("somescope", f"used_by_{scope or 'GLOBAL_SCOPE'}") all_help_info = HelpInfoExtracter.get_all_help_info( options, UnionMembership({}), fake_consumed_scopes_mapper) all_help_info_dict = dataclasses.asdict(all_help_info) expected_all_help_info_dict = { "scope_to_help_info": { GLOBAL_SCOPE: { "scope": GLOBAL_SCOPE, "description": "Global options.", "is_goal": False, "basic": ({ "display_args": ("-o=<int>", "--opt1=<int>"), "comma_separated_display_args": "-o=<int>, --opt1=<int>", "scoped_cmd_line_args": ("-o", "--opt1"), "unscoped_cmd_line_args": ("-o", "--opt1"), "config_key": "opt1", "env_var": "PANTS_OPT1", "value_history": { "ranked_values": ( { "rank": Rank.NONE, "value": None, "details": None }, { "rank": Rank.HARDCODED, "value": 42, "details": None }, ), }, "typ": int, "default": 42, "help": "Option 1", "deprecated_message": None, "removal_version": None, "removal_hint": None, "choices": None, "comma_separated_choices": None, }, ), "advanced": tuple(), "deprecated": tuple(), }, "foo": { "scope": "foo", "description": "A foo.", "is_goal": False, "basic": ({ "display_args": ("--[no-]foo-opt2", ), "comma_separated_display_args": "--[no-]foo-opt2", "scoped_cmd_line_args": ("--foo-opt2", "--no-foo-opt2"), "unscoped_cmd_line_args": ("--opt2", "--no-opt2"), "config_key": "opt2", "env_var": "PANTS_FOO_OPT2", "value_history": { "ranked_values": ( { "rank": Rank.NONE, "value": None, "details": None }, { "rank": Rank.HARDCODED, "value": True, "details": None }, ), }, "typ": bool, "default": True, "help": "Option 2", "deprecated_message": None, "removal_version": None, "removal_hint": None, "choices": None, "comma_separated_choices": None, }, ), "advanced": ({ "display_args": ("--foo-opt3=<str>", ), "comma_separated_display_args": "--foo-opt3=<str>", "scoped_cmd_line_args": ("--foo-opt3", ), "unscoped_cmd_line_args": ("--opt3", ), "config_key": "opt3", "env_var": "PANTS_FOO_OPT3", "value_history": { "ranked_values": ({ "rank": Rank.NONE, "value": None, "details": None }, ), }, "typ": str, "default": None, "help": "No help available.", "deprecated_message": None, "removal_version": None, "removal_hint": None, "choices": ("a", "b", "c"), "comma_separated_choices": "a, b, c", }, ), "deprecated": tuple(), }, "bar": { "scope": "bar", "description": "The bar goal.", "is_goal": True, "basic": tuple(), "advanced": tuple(), "deprecated": tuple(), }, }, "name_to_goal_info": { "bar": { "name": "bar", "description": "The bar goal.", "consumed_scopes": ("somescope", "used_by_bar"), "is_implemented": True, } }, } assert expected_all_help_info_dict == all_help_info_dict
def test_get_all_help_info(): class Global(Subsystem): options_scope = GLOBAL_SCOPE help = "Global options." @classmethod def register_options(cls, register): register("-o", "--opt1", type=int, default=42, help="Option 1") class Foo(Subsystem): options_scope = "foo" help = "A foo." @classmethod def register_options(cls, register): register("--opt2", type=bool, default=True, help="Option 2") register("--opt3", advanced=True, choices=["a", "b", "c"]) class Bar(GoalSubsystem): name = "bar" help = "The bar goal." class QuxField(StringField): alias = "qux" default = "blahblah" help = "A qux string." class QuuxField(IntField): alias = "quux" required = True help = "A quux int.\n\nMust be non-zero. Or zero. Whatever you like really." class BazLibrary(Target): alias = "baz_library" help = "A library of baz-es.\n\nUse it however you like." core_fields = [QuxField, QuuxField] options = Options.create( env={}, config=Config.load_file_contents(""), known_scope_infos=[ Global.get_scope_info(), Foo.get_scope_info(), Bar.get_scope_info() ], args=["./pants"], bootstrap_option_values=None, ) Global.register_options_on_scope(options) Foo.register_options_on_scope(options) Bar.register_options_on_scope(options) def fake_consumed_scopes_mapper(scope: str) -> Tuple[str, ...]: return ("somescope", f"used_by_{scope or 'GLOBAL_SCOPE'}") all_help_info = HelpInfoExtracter.get_all_help_info( options, UnionMembership({}), fake_consumed_scopes_mapper, RegisteredTargetTypes({BazLibrary.alias: BazLibrary}), ) all_help_info_dict = dataclasses.asdict(all_help_info) expected_all_help_info_dict = { "scope_to_help_info": { GLOBAL_SCOPE: { "scope": GLOBAL_SCOPE, "description": "Global options.", "is_goal": False, "basic": ({ "display_args": ("-o=<int>", "--opt1=<int>"), "comma_separated_display_args": "-o=<int>, --opt1=<int>", "scoped_cmd_line_args": ("-o", "--opt1"), "unscoped_cmd_line_args": ("-o", "--opt1"), "config_key": "opt1", "env_var": "PANTS_OPT1", "value_history": { "ranked_values": ( { "rank": Rank.NONE, "value": None, "details": None }, { "rank": Rank.HARDCODED, "value": 42, "details": None }, ), }, "typ": int, "default": 42, "help": "Option 1", "deprecation_active": False, "deprecated_message": None, "removal_version": None, "removal_hint": None, "choices": None, "comma_separated_choices": None, }, ), "advanced": tuple(), "deprecated": tuple(), }, "foo": { "scope": "foo", "description": "A foo.", "is_goal": False, "basic": ({ "display_args": ("--[no-]foo-opt2", ), "comma_separated_display_args": "--[no-]foo-opt2", "scoped_cmd_line_args": ("--foo-opt2", "--no-foo-opt2"), "unscoped_cmd_line_args": ("--opt2", "--no-opt2"), "config_key": "opt2", "env_var": "PANTS_FOO_OPT2", "value_history": { "ranked_values": ( { "rank": Rank.NONE, "value": None, "details": None }, { "rank": Rank.HARDCODED, "value": True, "details": None }, ), }, "typ": bool, "default": True, "help": "Option 2", "deprecation_active": False, "deprecated_message": None, "removal_version": None, "removal_hint": None, "choices": None, "comma_separated_choices": None, }, ), "advanced": ({ "display_args": ("--foo-opt3=<str>", ), "comma_separated_display_args": "--foo-opt3=<str>", "scoped_cmd_line_args": ("--foo-opt3", ), "unscoped_cmd_line_args": ("--opt3", ), "config_key": "opt3", "env_var": "PANTS_FOO_OPT3", "value_history": { "ranked_values": ({ "rank": Rank.NONE, "value": None, "details": None }, ), }, "typ": str, "default": None, "help": "No help available.", "deprecation_active": False, "deprecated_message": None, "removal_version": None, "removal_hint": None, "choices": ("a", "b", "c"), "comma_separated_choices": "a, b, c", }, ), "deprecated": tuple(), }, "bar": { "scope": "bar", "description": "The bar goal.", "is_goal": True, "basic": tuple(), "advanced": tuple(), "deprecated": tuple(), }, }, "name_to_goal_info": { "bar": { "name": "bar", "description": "The bar goal.", "consumed_scopes": ("somescope", "used_by_bar"), "is_implemented": True, } }, "name_to_target_type_info": { "baz_library": { "alias": "baz_library", "summary": "A library of baz-es.", "description": "A library of baz-es.\n\nUse it however you like.", "fields": ( { "alias": "qux", "default": "'blahblah'", "description": "A qux string.", "required": False, "type_hint": "str | None", }, { "alias": "quux", "default": None, "description": "A quux int.\n\nMust be non-zero. Or zero. " "Whatever you like really.", "required": True, "type_hint": "int", }, ), } }, } assert expected_all_help_info_dict == all_help_info_dict
def do_test(expected_default, **kwargs): kwargs["default"] = RankedValue(Rank.HARDCODED, kwargs["default"]) assert expected_default == HelpInfoExtracter.compute_default(**kwargs)
def gen_glopts_reference_data(options): parser = options.get_parser(GLOBAL_SCOPE) oschi = HelpInfoExtracter.get_option_scope_help_info_from_parser(parser) return oref_template_data_from_help_info(oschi)
def test_choices_enum(self) -> None: kwargs = {"type": LogLevel} ohi = HelpInfoExtracter("").get_option_help_info([], kwargs) assert ohi.choices == "info, debug"
def test_choices(self) -> None: kwargs = {"choices": ["info", "debug"]} ohi = HelpInfoExtracter("").get_option_help_info([], kwargs) assert ohi.choices == "info, debug"
def test_passthrough(self): kwargs = {"passthrough": True, "type": list, "member_type": str} ohi = HelpInfoExtracter("").get_option_help_info(["--thing"], kwargs) assert 2 == len(ohi.display_args) assert any(args.startswith("--thing") for args in ohi.display_args) assert any(args.startswith("... -- ") for args in ohi.display_args)
def test_deprecated(): kwargs = {"removal_version": "999.99.9", "removal_hint": "do not use this"} ohi = HelpInfoExtracter("").get_option_help_info(["--foo"], kwargs) assert "999.99.9" == ohi.removal_version assert "do not use this" == ohi.removal_hint assert ohi.deprecated_message is not None
def test_choices() -> None: kwargs = {"choices": ["info", "debug"]} ohi = HelpInfoExtracter("").get_option_help_info(["--foo"], kwargs) assert ohi.choices == ("info", "debug")
def do_test(args, kwargs, expected_default): ohi = HelpInfoExtracter('').get_option_help_info(args, kwargs) self.assertEqual(expected_default, ohi.default)
def test_not_deprecated(): ohi = HelpInfoExtracter("").get_option_help_info(["--foo"], {}) assert ohi.removal_version is None assert not ohi.deprecation_active
def test_list_of_enum() -> None: kwargs = {"type": list, "member_type": LogLevel} ohi = HelpInfoExtracter("").get_option_help_info(["--foo"], kwargs) assert ohi.choices == ("info", "debug")