From d8b524c692166e9dd0274b2ca9020c6b75477963 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 29 Sep 2025 12:31:10 +0530 Subject: [PATCH] Cache parsing of command line specifications Change option specification from dict to tuple for better performance and immutability and better type checking. --- gen/go_code.py | 2 +- kittens/quick_access_terminal/main.py | 10 +- kitty/cli.py | 80 +++++++-------- kitty/fast_data_types.pyi | 6 +- kitty/launcher/cli-parser.h | 15 +-- kitty/rc/base.py | 4 +- kitty/simple_cli_definitions.py | 138 +++++++++++++------------- 7 files changed, 130 insertions(+), 125 deletions(-) diff --git a/gen/go_code.py b/gen/go_code.py index 9e4c28a41..8510f8e24 100755 --- a/gen/go_code.py +++ b/gen/go_code.py @@ -247,7 +247,7 @@ def clone_safe_launch_opts() -> Sequence[GoOption]: ans = [] allowed = clone_safe_opts() for o in go_options_for_seq(parse_option_spec(options_spec())[0]): - if o.obj_dict['name'] in allowed: + if o.obj_defn.name in allowed: ans.append(o) return tuple(ans) diff --git a/kittens/quick_access_terminal/main.py b/kittens/quick_access_terminal/main.py index 9e8b709db..7b1b0c98c 100644 --- a/kittens/quick_access_terminal/main.py +++ b/kittens/quick_access_terminal/main.py @@ -29,16 +29,16 @@ def migrate_help(x: str) -> str: def help_of(x: str) -> str: - return migrate_help(panel_opts[x]['help']) + return migrate_help(panel_opts[x].help) agr('qat', 'Window appearance') -opt('lines', '25', long_text=panel_opts['lines']['help']) +opt('lines', '25', long_text=panel_opts['lines'].help) -opt('columns', '80', long_text=panel_opts['columns']['help']) +opt('columns', '80', long_text=panel_opts['columns'].help) -opt('edge', 'top', choices=panel_opts['edge']['choices'], long_text=help_of('edge')) +opt('edge', 'top', choices=panel_opts['edge'].choices, long_text=help_of('edge')) opt('background_opacity', '0.85', option_type='unit_float', long_text=''' The background opacity of the window. This works the same as the kitty @@ -80,7 +80,7 @@ opt('output_name', '', long_text=help_of('output_name')) opt('start_as_hidden', 'no', option_type='to_bool', long_text='Whether to start the quick access terminal hidden. Useful if you are starting it as part of system startup.') -opt('focus_policy', 'exclusive', choices=panel_opts['focus_policy']['choices'], long_text=help_of('focus_policy')) +opt('focus_policy', 'exclusive', choices=panel_opts['focus_policy'].choices, long_text=help_of('focus_policy')) diff --git a/kitty/cli.py b/kitty/cli.py index 73d683ff8..545de0791 100644 --- a/kitty/cli.py +++ b/kitty/cli.py @@ -15,7 +15,7 @@ from .fast_data_types import parse_cli_from_spec, wcswidth from .options.types import Options as KittyOpts from .simple_cli_definitions import ( CompletionType, - OptionDict, + OptionDefinition, OptionSpecSeq, defval_for_opt, get_option_maps, @@ -34,34 +34,34 @@ go_type_map = { class GoOption: - def __init__(self, x: OptionDict) -> None: - flags = sorted(x['aliases'], key=len) + def __init__(self, x: OptionDefinition) -> None: + flags = sorted(x.aliases, key=len) short = '' self.aliases = [] if len(flags) > 1 and not flags[0].startswith("--"): short = flags[0][1:] - self.short, self.long = short, x['name'].replace('_', '-') + self.short, self.long = short, x.name.replace('_', '-') for f in flags: q = f[2:] if f.startswith('--') else f[1:] self.aliases.append(q) - self.type = x['type'] - if x['choices']: + self.type = x.type + if x.choices: self.type = 'choices' - self.default = x['default'] - self.obj_dict = x + self.default = x.default + self.obj_defn = x self.go_type = go_type_map[self.type] - if x['dest']: - self.go_var_name = ''.join(x.capitalize() for x in x['dest'].replace('-', '_').split('_')) + if x.dest: + self.go_var_name = ''.join(x.capitalize() for x in x.dest.replace('-', '_').split('_')) else: self.go_var_name = ''.join(x.capitalize() for x in self.long.replace('-', '_').split('_')) - self.help_text = serialize_as_go_string(self.obj_dict['help'].strip()) + self.help_text = serialize_as_go_string(self.obj_defn.help.strip()) def struct_declaration(self) -> str: return f'{self.go_var_name} {self.go_type}' @property def flags(self) -> list[str]: - return sorted(self.obj_dict['aliases']) + return sorted(self.obj_defn.aliases) def as_option(self, cmd_name: str = 'cmd', depth: int = 0, group: str = '') -> str: add = f'AddToGroup("{serialize_as_go_string(group)}", ' if group else 'Add(' @@ -77,8 +77,8 @@ class GoOption: cx = ', '.join(f'"{serialize_as_go_string(x)}"' for x in self.sorted_choices) ans += f'\nChoices: "{serialize_as_go_string(c)}",\n' ans += f'\nCompleter: cli.NamesCompleter("Choices for {self.long}", {cx}),' - elif self.obj_dict['completion'].type is not CompletionType.none: - ans += ''.join(self.obj_dict['completion'].as_go_code('Completer', ': ')) + ',' + elif self.obj_defn.completion.type is not CompletionType.none: + ans += ''.join(self.obj_defn.completion.as_go_code('Completer', ': ')) + ',' if depth > 0: ans += f'\n\tDepth: {depth},\n' if self.default: @@ -111,7 +111,7 @@ class GoOption: @property def sorted_choices(self) -> list[str]: - choices = sorted(self.obj_dict['choices']) + choices = sorted(self.obj_defn.choices) choices.remove(self.default or '') choices.insert(0, self.default or '') return choices @@ -374,7 +374,7 @@ def get_defaults_from_seq(seq: OptionSpecSeq) -> dict[str, Any]: ans: dict[str, Any] = {} for opt in seq: if not isinstance(opt, str): - ans[opt['dest']] = defval_for_opt(opt) + ans[opt.dest] = defval_for_opt(opt) return ans @@ -432,24 +432,24 @@ class PrintHelpForSeq: if isinstance(opt, str): a(f'{title(opt)}:') continue - help_text = opt['help'] + help_text = opt.help if help_text == '!': continue # hidden option - a(' ' + ', '.join(map(green, sorted(opt['aliases'], reverse=True)))) - defval = opt.get('default') - if (otype := opt.get('type', '')).startswith('bool-'): + a(' ' + ', '.join(map(green, sorted(opt.aliases, reverse=True)))) + defval = opt.default + if (otype := opt.type).startswith('bool-'): blocks[-1] += italic(f'[={help_defval_for_bool(otype)}]') else: dt = f'''=[{italic(defval or '""')}]''' blocks[-1] += dt - if opt.get('help'): + if opt.help: t = help_text.replace('%default', str(defval)).strip() # replace rst literal code block syntax t = t.replace('::\n\n', ':\n\n') t = t.replace('#placeholder_for_formatting#', '') wa(prettify(t), indent=4) - if opt.get('choices'): - wa('Choices: {}'.format(', '.join(opt['choices'])), indent=4) + if opt.choices: + wa('Choices: {}'.format(', '.join(opt.choices)), indent=4) a('') text = '\n'.join(blocks) + '\n\n' + version() @@ -510,25 +510,25 @@ def seq_as_rst( a(opt) a('~' * (len(opt) + 10)) continue - help_text = opt['help'] + help_text = opt.help if help_text == '!': continue # hidden option defn = '.. option:: ' - if (otype := opt.get('type', '')).startswith('bool-'): + if (otype := opt.type).startswith('bool-'): val_name = f' [={help_defval_for_bool(otype)}]' else: - val_name = ' <{}>'.format(opt['dest'].upper()) - a(defn + ', '.join(o + val_name for o in sorted(opt['aliases']))) - if opt.get('help'): - defval = opt.get('default') + val_name = ' <{}>'.format(opt.dest.upper()) + a(defn + ', '.join(o + val_name for o in sorted(opt.aliases))) + if opt.help: + defval = opt.default t = help_text.replace('%default', ':code:`' + escape_rst(str(defval)) + '`').strip() t = t.replace('#placeholder_for_formatting#', '') a('') a(textwrap.indent(prettify_rst(t), ' ' * 4)) if defval is not None: a(textwrap.indent(f'Default: :code:`{escape_rst(str(defval))}`', ' ' * 4)) - if opt.get('choices'): - a(textwrap.indent('Choices: {}'.format(', '.join(f':code:`{escape_rst(c)}`' for c in sorted(opt['choices']))), ' ' * 4)) + if opt.choices: + a(textwrap.indent('Choices: {}'.format(', '.join(f':code:`{escape_rst(c)}`' for c in sorted(opt.choices))), ' ' * 4)) a('') text = '\n'.join(blocks) @@ -541,8 +541,8 @@ def as_type_stub(seq: OptionSpecSeq, disabled: OptionSpecSeq, class_name: str, e for opt in chain(seq, disabled): if isinstance(opt, str): continue - name = opt['dest'] - otype = opt['type'] or 'str' + name = opt.dest + otype = opt.type or 'str' if otype in ('str', 'int', 'float'): t = otype if t == 'str' and defval_for_opt(opt) is None: @@ -550,8 +550,8 @@ def as_type_stub(seq: OptionSpecSeq, disabled: OptionSpecSeq, class_name: str, e elif otype == 'list': t = 'typing.Sequence[str]' elif otype in ('choice', 'choices'): - if opt['choices']: - t = 'typing.Literal[{}]'.format(','.join(f'{x!r}' for x in opt['choices'])) + if opt.choices: + t = 'typing.Literal[{}]'.format(','.join(f'{x!r}' for x in opt.choices)) else: t = 'str' elif otype.startswith('bool-'): @@ -616,14 +616,14 @@ def apply_preparsed_cli_flags( def parse_cmdline_inner( - args: list[str], oc: Options, disabled: OptionSpecSeq, names_map: dict[str, OptionDict], - values_map: dict[str, OptionDict], ans: Any, track_seen_options: dict[str, Any] | None = None + args: list[str], oc: Options, disabled: OptionSpecSeq, names_map: dict[str, OptionDefinition], + values_map: dict[str, OptionDefinition], ans: Any, track_seen_options: dict[str, Any] | None = None ) -> list[str]: preparsed = parse_cli_from_spec(args, names_map, values_map) leftover_args = apply_preparsed_cli_flags(preparsed, ans, lambda: oc, track_seen_options) for opt in disabled: if not isinstance(opt, str): - setattr(ans, opt['dest'], defval_for_opt(opt)) + setattr(ans, opt.dest, defval_for_opt(opt)) return leftover_args @@ -634,10 +634,10 @@ def parse_cmdline( names_map = oc.names_map.copy() values_map = oc.values_map.copy() if 'help' not in names_map: - names_map['help'] = {'type': 'bool-set', 'aliases': ('--help', '-h')} # type: ignore + names_map['help'] = OptionDefinition(type='bool-set', aliases=('--help', '-h')) values_map['help'] = False if 'version' not in names_map: - names_map['version'] = {'type': 'bool-set', 'aliases': ('--version', '-v')} # type: ignore + names_map['version'] = OptionDefinition(type='bool-set', aliases=('--version', '-v')) values_map['version'] = False try: return parse_cmdline_inner(sys.argv[1:] if args is None else args, oc, disabled, names_map, values_map, ans, track_seen_options) diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 79dde112b..31ef21b6a 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -8,7 +8,7 @@ from kitty.fonts.render import FontObject from kitty.marks import MarkerFunc from kitty.notifications import MacOSNotificationCategory from kitty.options.types import Options -from kitty.simple_cli_definitions import OptionDict +from kitty.simple_cli_definitions import OptionDefinition from kitty.types import LayerShellConfig, SignalInfo from kitty.typing_compat import EdgeLiteral, NotRequired, ReadableBuffer, WriteableBuffer @@ -1720,7 +1720,9 @@ def set_clipboard_data_types(ct: int, mime_types: Tuple[str, ...]) -> None: ... def get_clipboard_mime(ct: int, mime: Optional[str], callback: Callable[[bytes], None]) -> None: ... def run_with_activation_token(func: Callable[[str], None]) -> bool: ... def toggle_os_window_visibility(os_window_id: int, visible: bool | Literal[-1] = -1, move_to_active_screen: bool = False) -> bool: ... -def parse_cli_from_spec(args: list[str], names_map: dict[str, OptionDict], defval_map: dict[str, Any]) -> tuple[dict[str, tuple[Any, bool]], list[str]]: ... +def parse_cli_from_spec( + args: list[str], names_map: dict[str, OptionDefinition], defval_map: dict[str, Any] +) -> tuple[dict[str, tuple[Any, bool]], list[str]]: ... def layer_shell_config_for_os_window(os_window_id: int) -> dict[str, Any] | None: ... def set_layer_shell_config(os_window_id: int, cfg: LayerShellConfig) -> bool: ... def wrapped_kitten_names() -> List[str]: ... diff --git a/kitty/launcher/cli-parser.h b/kitty/launcher/cli-parser.h index c45aac418..b363d726d 100644 --- a/kitty/launcher/cli-parser.h +++ b/kitty/launcher/cli-parser.h @@ -559,14 +559,16 @@ parse_cli_from_python_spec(PyObject *self, PyObject *args) { memcpy(argv[i + 1], src, sz); } argv[++argc] = 0; - PyObject *key = NULL, *opt = NULL; + PyObject *key = NULL, *optdef = NULL; Py_ssize_t pos = 0; - while (PyDict_Next(names_map, &pos, &key, &opt)) { + while (PyDict_Next(names_map, &pos, &key, &optdef)) { FlagSpec flag = {.dest=PyUnicode_AsUTF8(key)}; - PyObject *pytype = PyDict_GetItemString(opt, "type"); - const char *type = pytype ? PyUnicode_AsUTF8(pytype) : ""; + RAII_PyObject(pytype, PyObject_GetAttrString(optdef, "type")); + if (!pytype) return NULL; + const char *type = PyUnicode_AsUTF8(pytype); PyObject *defval = PyDict_GetItemWithError(defval_map, key); if (!defval && PyErr_Occurred()) return NULL; - PyObject *pyaliases = PyDict_GetItemString(opt, "aliases"); + RAII_PyObject(pyaliases, PyObject_GetAttrString(optdef, "aliases")); + if (!pyaliases) return NULL; for (int a = 0; a < PyTuple_GET_SIZE(pyaliases); a++) { const char *alias = PyUnicode_AsUTF8(PyTuple_GET_ITEM(pyaliases, a)); if (vt_is_end(vt_insert(&spec.alias_map, alias, flag.dest))) return PyErr_NoMemory(); @@ -588,7 +590,8 @@ parse_cli_from_python_spec(PyObject *self, PyObject *args) { } else if (strcmp(type, "choices") == 0) { flag.defval.type = CLI_VALUE_CHOICE; flag.defval.strval = PyUnicode_AsUTF8(defval); - PyObject *pyc = PyDict_GetItemString(opt, "choices"); + RAII_PyObject(pyc, PyObject_GetAttrString(optdef, "choices")); + if (!pyc) return NULL; flag.defval.listval.items = alloc_for_cli(&spec, PyTuple_GET_SIZE(pyc) * sizeof(char*)); if (!flag.defval.listval.items) return PyErr_NoMemory(); flag.defval.listval.count = PyTuple_GET_SIZE(pyc); diff --git a/kitty/rc/base.py b/kitty/rc/base.py index 00883293c..7ff97ef1a 100644 --- a/kitty/rc/base.py +++ b/kitty/rc/base.py @@ -82,8 +82,8 @@ CmdGenerator = Iterator[CmdReturnType] PayloadType = Optional[Union[CmdReturnType, CmdGenerator]] PayloadGetType = PayloadGetter ArgsType = list[str] -ImageCompletion = CompletionSpec.from_string('type:file group:"Images"') -ImageCompletion.extensions = 'png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp', 'tiff' +ImageCompletion = CompletionSpec.from_string('type:file group:"Images"')._replace( + extensions=('png', 'jpg', 'jpeg', 'webp', 'gif', 'bmp', 'tiff')) SUPPORTED_IMAGE_FORMATS = tuple(x.upper() for x in ImageCompletion.extensions if x != 'jpg') diff --git a/kitty/simple_cli_definitions.py b/kitty/simple_cli_definitions.py index 63bea6840..1c73964ab 100644 --- a/kitty/simple_cli_definitions.py +++ b/kitty/simple_cli_definitions.py @@ -6,9 +6,9 @@ import re import sys -from dataclasses import dataclass from enum import Enum, auto -from typing import Any, Iterator, Sequence, TypedDict +from functools import lru_cache +from typing import Any, Iterator, NamedTuple, Sequence try: from kitty.constants import appname, is_macos @@ -42,8 +42,7 @@ class CompletionRelativeTo(Enum): config_dir = auto() -@dataclass -class CompletionSpec: +class CompletionSpec(NamedTuple): type: CompletionType = CompletionType.none kwds: tuple[str,...] = () @@ -54,27 +53,33 @@ class CompletionSpec: @staticmethod def from_string(raw: str) -> 'CompletionSpec': - self = CompletionSpec() + typ = CompletionType.none + kwds: tuple[str, ...] = () + extensions: tuple[str, ...] = () + mime_patterns: tuple[str, ...] = () + group = '' + relative_to = CompletionRelativeTo.cwd for x in shlex_split(raw): ck, vv = x.split(':', 1) if ck == 'type': - self.type = getattr(CompletionType, vv) + typ = getattr(CompletionType, vv) elif ck == 'kwds': - self.kwds += tuple(vv.split(',')) + kwds += tuple(vv.split(',')) elif ck == 'ext': - self.extensions += tuple(vv.split(',')) + extensions += tuple(vv.split(',')) elif ck == 'group': - self.group = vv + group = vv elif ck == 'mime': - self.mime_patterns += tuple(vv.split(',')) + mime_patterns += tuple(vv.split(',')) elif ck == 'relative': if vv == 'conf': - self.relative_to = CompletionRelativeTo.config_dir + relative_to = CompletionRelativeTo.config_dir else: raise ValueError(f'Unknown completion relative to value: {vv}') else: raise KeyError(f'Unknown completion property: {ck}') - return self + return CompletionSpec( + type=typ, kwds=kwds, extensions=extensions, mime_patterns=mime_patterns, group=group, relative_to=relative_to) def as_go_code(self, go_name: str, sep: str = ': ') -> Iterator[str]: completers = [] @@ -106,21 +111,23 @@ class CompletionSpec: yield f'{go_name}{sep}{completers[0]}' -class OptionDict(TypedDict): - dest: str - name: str - aliases: tuple[str, ...] - help: str - choices: tuple[str, ...] - type: str - default: str | None - condition: bool - completion: CompletionSpec +class OptionDefinition(NamedTuple): + dest: str = '' + name: str = '' + aliases: tuple[str, ...] = () + help: str = '' + choices: tuple[str, ...] = () + type: str = '' + default: str | None = None + condition: bool = False + completion: CompletionSpec = CompletionSpec() -OptionSpecSeq = Sequence[str | OptionDict] + +OptionSpecSeq = Sequence[str | OptionDefinition] +@lru_cache(64) def parse_option_spec(spec: str | None = None) -> tuple[OptionSpecSeq, OptionSpecSeq]: if spec is None: spec = kitty_options_spec() @@ -129,14 +136,10 @@ def parse_option_spec(spec: str | None = None) -> tuple[OptionSpecSeq, OptionSpe lines = spec.splitlines() prev_line = '' prev_indent = 0 - seq: list[str | OptionDict] = [] - disabled: list[str | OptionDict] = [] + seq: list[str | OptionDefinition] = [] + disabled: list[str | OptionDefinition] = [] mpat = re.compile('([a-z]+)=(.+)') - current_cmd: OptionDict = { - 'dest': '', 'aliases': (), 'help': '', 'choices': (), - 'type': '', 'condition': False, 'default': None, 'completion': CompletionSpec(), 'name': '' - } - empty_cmd = current_cmd + current_cmd = empty_cmd = OptionDefinition() def indent_of_line(x: str) -> int: return len(x) - len(x.lstrip()) @@ -152,11 +155,7 @@ def parse_option_spec(spec: str | None = None) -> tuple[OptionSpecSeq, OptionSpe if line.startswith('--'): parts = line.split(' ') defdest = parts[0][2:].replace('-', '_') - current_cmd = { - 'dest': defdest, 'aliases': tuple(parts), 'help': '', - 'choices': tuple(), 'type': '', 'name': defdest, - 'default': None, 'condition': True, 'completion': CompletionSpec(), - } + current_cmd = OptionDefinition(dest=defdest, aliases=tuple(parts), name=defdest, condition=True) state = METADATA continue raise ValueError(f'Invalid option spec, unexpected line: {line}') @@ -164,60 +163,61 @@ def parse_option_spec(spec: str | None = None) -> tuple[OptionSpecSeq, OptionSpe m = mpat.match(line) if m is None: state = HELP - current_cmd['help'] += line + current_cmd = current_cmd._replace(help=current_cmd.help + line) else: k, v = m.group(1), m.group(2) if k == 'choices': vals = tuple(x.strip() for x in v.split(',')) - if not current_cmd['type']: - current_cmd['type'] = 'choices' - if current_cmd['type'] != 'choices': - raise ValueError(f'Cannot specify choices for an option of type: {current_cmd["type"]}') - current_cmd['choices'] = tuple(vals) - if current_cmd['default'] is None: - current_cmd['default'] = vals[0] + if not current_cmd.type: + current_cmd = current_cmd._replace(type='choices') + if current_cmd.type != 'choices': + raise ValueError(f'Cannot specify choices for an option of type: {current_cmd.type}') + current_cmd = current_cmd._replace(choices=tuple(vals)) + if current_cmd.default is None: + current_cmd = current_cmd._replace(default=vals[0]) else: if k == 'default': - current_cmd['default'] = v + current_cmd = current_cmd._replace(default=v) elif k == 'type': if v == 'choice': v = 'choices' - current_cmd['type'] = v + current_cmd = current_cmd._replace(type=v) elif k == 'dest': - current_cmd['dest'] = v + current_cmd = current_cmd._replace(dest=v) elif k == 'condition': - current_cmd['condition'] = bool(eval(v)) + current_cmd = current_cmd._replace(condition=bool(eval(v))) elif k == 'completion': - current_cmd['completion'] = CompletionSpec.from_string(v) + current_cmd = current_cmd._replace(completion=CompletionSpec.from_string(v)) elif state is HELP: if line: current_indent = indent_of_line(line) if current_indent > 1: if prev_indent == 0: - current_cmd['help'] += '\n' + current_cmd = current_cmd._replace(help=current_cmd.help + '\n') else: line = line.strip() prev_indent = current_indent - spc = '' if current_cmd['help'].endswith('\n') else ' ' - current_cmd['help'] += spc + line + spc = '' if current_cmd.help.endswith('\n') else ' ' + current_cmd = current_cmd._replace(help=current_cmd.help + spc + line) else: prev_indent = 0 if prev_line: - current_cmd['help'] += '\n' if current_cmd['help'].endswith('::') else '\n\n' + h = '\n' if current_cmd.help.endswith('::') else '\n\n' + current_cmd = current_cmd._replace(help=current_cmd.help + h) else: state = NORMAL - (seq if current_cmd.get('condition', True) else disabled).append(current_cmd) + (seq if current_cmd.condition else disabled).append(current_cmd) current_cmd = empty_cmd prev_line = line if current_cmd is not empty_cmd: - (seq if current_cmd.get('condition', True) else disabled).append(current_cmd) + (seq if current_cmd.condition else disabled).append(current_cmd) return seq, disabled -def defval_for_opt(opt: OptionDict) -> Any: - dv: Any = opt.get('default') - typ = opt.get('type', '') +def defval_for_opt(opt: OptionDefinition) -> Any: + dv: Any = opt.default + typ = opt.type if typ.startswith('bool-'): if dv is None: dv = False if typ == 'bool-set' else True @@ -230,16 +230,16 @@ def defval_for_opt(opt: OptionDict) -> Any: return dv -def get_option_maps(seq: OptionSpecSeq) -> tuple[dict[str, OptionDict], dict[str, OptionDict], dict[str, Any]]: - names_map: dict[str, OptionDict] = {} - alias_map: dict[str, OptionDict] = {} +def get_option_maps(seq: OptionSpecSeq) -> tuple[dict[str, OptionDefinition], dict[str, OptionDefinition], dict[str, Any]]: + names_map: dict[str, OptionDefinition] = {} + alias_map: dict[str, OptionDefinition] = {} values_map: dict[str, Any] = {} for opt in seq: if isinstance(opt, str): continue - for alias in opt['aliases']: + for alias in opt.aliases: alias_map[alias] = opt - name = opt['dest'] + name = opt.dest names_map[name] = opt values_map[name] = defval_for_opt(opt) return names_map, alias_map, values_map @@ -259,9 +259,9 @@ def add_list_values(*values: str) -> Iterator[str]: yield f'\tflag.defval.listval.items[{n}] = {c_str(value)};' -def generate_c_for_opt(name: str, defval: Any, opt: OptionDict) -> Iterator[str]: +def generate_c_for_opt(name: str, defval: Any, opt: OptionDefinition) -> Iterator[str]: yield f'\tflag = (FlagSpec){{.dest={c_str(name)},}};' - match opt['type']: + match opt.type: case 'bool-set' | 'bool-reset': yield '\tflag.defval.type = CLI_VALUE_BOOL;' yield f'\tflag.defval.boolval = {"true" if defval else "false"};' @@ -278,7 +278,7 @@ def generate_c_for_opt(name: str, defval: Any, opt: OptionDict) -> Iterator[str] case 'choices': yield '\tflag.defval.type = CLI_VALUE_CHOICE;' yield f'\tflag.defval.strval = {c_str(defval)};' - yield from add_list_values(*opt['choices']) + yield from add_list_values(*opt.choices) case _: yield '\tflag.defval.type = CLI_VALUE_STRING;' if defval is not None: @@ -289,22 +289,22 @@ def generate_c_parser_for(funcname: str, spec: str) -> Iterator[str]: seq, disabled = parse_option_spec(spec) names_map, _, defaults_map = get_option_maps(seq) if 'help' not in names_map: - names_map['help'] = {'type': 'bool-set', 'aliases': ('--help', '-h')} # type: ignore + names_map['help'] = OptionDefinition(type='bool-set', aliases=('--help', '-h')) defaults_map['help'] = False if 'version' not in names_map: - names_map['version'] = {'type': 'bool-set', 'aliases': ('--version', '-v')} # type: ignore + names_map['version'] = OptionDefinition(type='bool-set', aliases=('--version', '-v')) defaults_map['version'] = False yield f'static void\nparse_cli_for_{funcname}(CLISpec *spec, int argc, char **argv) {{' # }} yield '\tFlagSpec flag;' for name, opt in names_map.items(): - for alias in opt['aliases']: + for alias in opt.aliases: yield f'\tif (vt_is_end(vt_insert(&spec->alias_map, {c_str(alias)}, {c_str(name)}))) OOM;' yield from generate_c_for_opt(name, defaults_map[name], opt) yield '\tif (vt_is_end(vt_insert(&spec->flag_map, flag.dest, flag))) OOM;' for d in disabled: if not isinstance(d, str): - yield from generate_c_for_opt(d['dest'], defval_for_opt(d), d) + yield from generate_c_for_opt(d.dest, defval_for_opt(d), d) yield '\tif (vt_is_end(vt_insert(&spec->disabled_map, flag.dest, flag))) OOM;' yield '\tparse_cli_loop(spec, true, argc, argv);'