From 1438c64b9ef07e26cd00ecbe3ef7f40be6c6c03c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 21 Jun 2021 04:25:04 +0530 Subject: [PATCH] Move the config generation code into kitty proper --- gen-config.py | 420 +--------------------------------------- kitty/conf/generate.py | 425 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 426 insertions(+), 419 deletions(-) create mode 100644 kitty/conf/generate.py diff --git a/gen-config.py b/gen-config.py index 1f53ea562..e2cc725ce 100755 --- a/gen-config.py +++ b/gen-config.py @@ -2,426 +2,8 @@ # vim:fileencoding=utf-8 # License: GPLv3 Copyright: 2021, Kovid Goyal -import inspect -import os -import pprint -import re -import textwrap -from typing import ( - Any, Callable, Dict, Iterator, List, Set, Tuple, Union, get_type_hints -) -from kitty.conf.types import Definition, MultiOption, Option, unset - - -def chunks(lst: List, n: int) -> Iterator[List]: - for i in range(0, len(lst), n): - yield lst[i:i + n] - - -def atoi(text: str) -> str: - return f'{int(text):08d}' if text.isdigit() else text - - -def natural_keys(text: str) -> Tuple[str, ...]: - return tuple(atoi(c) for c in re.split(r'(\d+)', text)) - - -def generate_class(defn: Definition, loc: str) -> Tuple[str, str]: - class_lines: List[str] = [] - tc_lines: List[str] = [] - a = class_lines.append - t = tc_lines.append - a('class Options:') - t('class Parser:') - choices = {} - imports: Set[Tuple[str, str]] = set() - tc_imports: Set[Tuple[str, str]] = set() - - def type_name(x: type) -> str: - ans = x.__name__ - if x.__module__ and x.__module__ != 'builtins': - imports.add((x.__module__, x.__name__)) - return ans - - def option_type_as_str(x: Any) -> str: - if hasattr(x, '__name__'): - return type_name(x) - ans = repr(x) - ans = ans.replace('NoneType', 'None') - return ans - - def option_type_data(option: Union[Option, MultiOption]) -> Tuple[Callable, str]: - func = option.parser_func - if func.__module__ == 'builtins': - return func, func.__name__ - th = get_type_hints(func) - rettype = th['return'] - typ = option_type_as_str(rettype) - if isinstance(option, MultiOption): - typ = typ[typ.index('[') + 1:-1] - typ = typ.replace('Tuple', 'Dict', 1) - return func, typ - - is_mutiple_vars = {} - option_names = set() - color_table = list(map(str, range(256))) - - def parser_function_declaration(option_name: str) -> None: - t('') - t(f' def {option_name}(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:') - - for option in sorted(defn.iter_all_options(), key=lambda a: natural_keys(a.name)): - option_names.add(option.name) - parser_function_declaration(option.name) - if isinstance(option, MultiOption): - mval: Dict[str, Dict[str, Any]] = {'macos': {}, 'linux': {}, '': {}} - func, typ = option_type_data(option) - for val in option: - if val.add_to_default: - gr = mval[val.only] - for k, v in func(val.defval_as_str): - gr[k] = v - is_mutiple_vars[option.name] = typ, mval - sig = inspect.signature(func) - tc_imports.add((func.__module__, func.__name__)) - if len(sig.parameters) == 1: - t(f' for k, v in {func.__name__}(val):') - t(f' ans["{option.name}"][k] = v') - else: - t(f' for k, v in {func.__name__}(val, ans["{option.name}"]):') - t(f' ans["{option.name}"][k] = v') - continue - - if option.choices: - typ = 'typing.Literal[{}]'.format(', '.join(repr(x) for x in option.choices)) - ename = f'choices_for_{option.name}' - choices[ename] = typ - typ = ename - func = str - elif defn.has_color_table and option.is_color_table_color: - func, typ = option_type_data(option) - t(f' ans[{option.name!r}] = {func.__name__}(val)') - tc_imports.add((func.__module__, func.__name__)) - cnum = int(option.name[5:]) - color_table[cnum] = '0x{:06x}'.format(func(option.defval_as_string).__int__()) - continue - else: - func, typ = option_type_data(option) - try: - params = inspect.signature(func).parameters - except Exception: - params = {} - if 'dict_with_parse_results' in params: - t(f' {func.__name__}(val, ans)') - else: - t(f' ans[{option.name!r}] = {func.__name__}(val)') - if func.__module__ != 'builtins': - tc_imports.add((func.__module__, func.__name__)) - - defval = repr(func(option.defval_as_string)) - if option.macos_defval is not unset: - md = repr(func(option.macos_defval)) - defval = f'{md} if is_macos else {defval}' - imports.add(('kitty.constants', 'is_macos')) - a(f' {option.name}: {typ} = {defval}') - if option.choices: - t(' val = val.lower()') - t(f' if val not in self.choices_for_{option.name}:') - t(f' raise ValueError(f"The value {{val}} is not a valid choice for {option.name}")') - t(f' ans["{option.name}"] = val') - t('') - t(f' choices_for_{option.name} = frozenset({option.choices!r})') - - for option_name, (typ, mval) in is_mutiple_vars.items(): - a(f' {option_name}: {typ} = ' '{}') - - for parser, aliases in defn.deprecations.items(): - for alias in aliases: - parser_function_declaration(alias) - tc_imports.add((parser.__module__, parser.__name__)) - t(f' {parser.__name__}({alias!r}, val, ans)') - - action_parsers = {} - - def resolve_import(ftype: str) -> str: - if '.' in ftype: - fmod, ftype = ftype.rpartition('.')[::2] - else: - fmod = f'{loc}.options.utils' - imports.add((fmod, ftype)) - return ftype - - for aname, action in defn.actions.items(): - option_names.add(aname) - action_parsers[aname] = func = action.parser_func - th = get_type_hints(func) - rettype = th['return'] - typ = option_type_as_str(rettype) - typ = typ[typ.index('[') + 1:-1] - a(f' {aname}: typing.List[{typ}] = []') - for imp in action.imports: - resolve_import(imp) - for fname, ftype in action.fields.items(): - ftype = resolve_import(ftype) - a(f' {fname}: {ftype} = ' '{}') - parser_function_declaration(aname) - t(f' for k in {func.__name__}(val):') - t(f' ans[{aname!r}].append(k)') - tc_imports.add((func.__module__, func.__name__)) - - if defn.has_color_table: - imports.add(('array', 'array')) - a(' color_table: array = array("L", (') - for grp in chunks(color_table, 8): - a(' ' + ', '.join(grp) + ',') - a(' ))') - - a(' config_paths: typing.Tuple[str, ...] = ()') - a(' config_overrides: typing.Tuple[str, ...] = ()') - a('') - a(' def __init__(self, options_dict: typing.Optional[typing.Dict[str, typing.Any]] = None) -> None:') - a(' if options_dict is not None:') - a(' for key in option_names:') - a(' setattr(self, key, options_dict[key])') - - a('') - a(' @property') - a(' def _fields(self) -> typing.Tuple[str, ...]:') - a(' return option_names') - - a('') - a(' def __iter__(self) -> typing.Iterator[str]:') - a(' return iter(self._fields)') - - a('') - a(' def __len__(self) -> int:') - a(' return len(self._fields)') - - a('') - a(' def _copy_of_val(self, name: str) -> typing.Any:') - a(' ans = getattr(self, name)') - a(' if isinstance(ans, dict):\n ans = ans.copy()') - a(' elif isinstance(ans, list):\n ans = ans[:]') - a(' return ans') - - a('') - a(' def _asdict(self) -> typing.Dict[str, typing.Any]:') - a(' return {k: self._copy_of_val(k) for k in self}') - - a('') - a(' def _replace(self, **kw: typing.Any) -> "Options":') - a(' ans = Options()') - a(' for name in self:') - a(' setattr(ans, name, self._copy_of_val(name))') - a(' for name, val in kw.items():') - a(' setattr(ans, name, val)') - a(' return ans') - - a('') - a(' def __getitem__(self, key: typing.Union[int, str]) -> typing.Any:') - a(' k = option_names[key] if isinstance(key, int) else key') - a(' try:') - a(' return getattr(self, k)') - a(' except AttributeError:') - a(' pass') - a(' raise KeyError(f"No option named: {k}")') - - if defn.has_color_table: - a('') - a(' def __getattr__(self, key: str) -> typing.Any:') - a(' if key.startswith("color"):') - a(' q = key[5:]') - a(' if q.isdigit():') - a(' k = int(q)') - a(' if 0 <= k <= 255:') - a(' x = self.color_table[k]') - a(' return Color((x >> 16) & 255, (x >> 8) & 255, x & 255)') - a(' raise AttributeError(key)') - a('') - a(' def __setattr__(self, key: str, val: typing.Any) -> typing.Any:') - a(' if key.startswith("color"):') - a(' q = key[5:]') - a(' if q.isdigit():') - a(' k = int(q)') - a(' if 0 <= k <= 255:') - a(' self.color_table[k] = int(val)') - a(' return') - a(' object.__setattr__(self, key, val)') - - a('') - a('') - a('defaults = Options()') - for option_name, (typ, mval) in is_mutiple_vars.items(): - a(f'defaults.{option_name} = {mval[""]!r}') - if mval['macos']: - imports.add(('kitty.constants', 'is_macos')) - a('if is_macos:') - a(f' defaults.{option_name}.update({mval["macos"]!r}') - if mval['macos']: - imports.add(('kitty.constants', 'is_macos')) - a('if not is_macos:') - a(f' defaults.{option_name}.update({mval["linux"]!r}') - - for aname, func in action_parsers.items(): - a(f'defaults.{aname} = [') - only: Dict[str, List[Tuple[str, Callable]]] = {} - for sc in defn.iter_all_maps(aname): - if not sc.add_to_default: - continue - text = sc.parseable_text - if sc.only: - only.setdefault(sc.only, []).append((text, func)) - else: - for val in func(text): - a(f' # {sc.name}') - a(f' {val!r},') - a(']') - if only: - imports.add(('kitty.constants', 'is_macos')) - for cond, items in only.items(): - cond = 'is_macos' if cond == 'macos' else 'not is_macos' - a(f'if {cond}:') - for (text, func) in items: - for val in func(text): - a(f' defaults.{aname}.append({val!r})') - - t('') - t('') - t('def create_result_dict() -> typing.Dict[str, typing.Any]:') - t(' return {') - for oname in is_mutiple_vars: - t(f' {oname!r}: {{}},') - for aname in defn.actions: - t(f' {aname!r}: [],') - t(' }') - - t('') - t('') - t(f'actions = frozenset({tuple(defn.actions)!r})') - t('') - t('') - t('def merge_result_dicts(defaults: typing.Dict[str, typing.Any], vals: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:') - t(' ans = {}') - t(' for k, v in defaults.items():') - t(' if isinstance(v, dict):') - t(' ans[k] = merge_dicts(v, vals.get(k, {}))') - t(' elif k in actions:') - t(' ans[k] = v + vals.get(k, [])') - t(' else:') - t(' ans[k] = vals.get(k, v)') - t(' return ans') - tc_imports.add(('kitty.conf.utils', 'merge_dicts')) - - t('') - t('') - t('parser = Parser()') - t('') - t('') - t('def parse_conf_item(key: str, val: str, ans: typing.Dict[str, typing.Any]) -> bool:') - t(' func = getattr(parser, key, None)') - t(' if func is not None:') - t(' func(val, ans)') - t(' return True') - t(' return False') - - preamble = ['# generated by gen-config.py DO NOT edit', '# vim:fileencoding=utf-8', ''] - a = preamble.append - - def output_imports(imports: Set, add_module_imports: bool = True) -> None: - a('import typing') - seen_mods = {'typing'} - mmap: Dict[str, List[str]] = {} - for mod, name in imports: - mmap.setdefault(mod, []).append(name) - for mod in sorted(mmap): - names = sorted(mmap[mod]) - lines = textwrap.wrap(', '.join(names), 100) - if len(lines) == 1: - s = lines[0] - else: - s = '\n '.join(lines) - s = f'(\n {s}\n)' - a(f'from {mod} import {s}') - if add_module_imports and mod not in seen_mods and mod != s: - a(f'import {mod}') - seen_mods.add(mod) - - output_imports(imports) - a('') - if choices: - a('if typing.TYPE_CHECKING:') - for name, cdefn in choices.items(): - a(f' {name} = {cdefn}') - a('else:') - for name in choices: - a(f' {name} = str') - - a('') - a('option_names = ( # {{''{') - a(' ' + pprint.pformat(tuple(sorted(option_names, key=natural_keys)))[1:] + ' # }}''}') - class_def = '\n'.join(preamble + ['', ''] + class_lines) - - preamble = ['# generated by gen-config.py DO NOT edit', '# vim:fileencoding=utf-8', ''] - a = preamble.append - output_imports(tc_imports, False) - - return class_def, '\n'.join(preamble + ['', ''] + tc_lines) - - -def generate_c_conversion(loc: str, ctypes: List[Option]) -> str: - lines: List[str] = [] - basic_converters = { - 'int': 'PyLong_AsLong', 'uint': 'PyLong_AsUnsignedLong', 'bool': 'PyObject_IsTrue', - 'float': 'PyFloat_AsFloat', 'double': 'PyFloat_AsDouble', - 'time': 'parse_s_double_to_monotonic_t', 'time-ms': 'parse_ms_long_to_monotonic_t' - } - - for opt in ctypes: - lines.append('') - lines.append(f'static void\nconvert_from_python_{opt.name}(PyObject *val, Options *opts) ''{') - is_special = opt.ctype.startswith('!') - if is_special: - func = opt.ctype[1:] - lines.append(f' {func}(val, opts);') - else: - func = basic_converters.get(opt.ctype, opt.ctype) - lines.append(f' opts->{opt.name} = {func}(val);') - lines.append('}') - lines.append('') - lines.append(f'static void\nconvert_from_opts_{opt.name}(PyObject *py_opts, Options *opts) ''{') - lines.append(f' PyObject *ret = PyObject_GetAttrString(py_opts, "{opt.name}");') - lines.append(' if (ret == NULL) return;') - lines.append(f' convert_from_python_{opt.name}(ret, opts);') - lines.append(' Py_DECREF(ret);') - lines.append('}') - - lines.append('') - lines.append('static bool\nconvert_opts_from_python_opts(PyObject *py_opts, Options *opts) ''{') - for opt in ctypes: - lines.append(f' convert_from_opts_{opt.name}(py_opts, opts);') - lines.append(' if (PyErr_Occurred()) return false;') - lines.append(' return true;') - lines.append('}') - - preamble = ['// generated by gen-config.py DO NOT edit', '// vim:fileencoding=utf-8', '#pragma once', '#include "to-c.h"'] - return '\n'.join(preamble + ['', ''] + lines) - - -def write_output(loc: str, defn: Definition) -> None: - cls, tc = generate_class(defn, loc) - with open(os.path.join(*loc.split('.'), 'options', 'types.py'), 'w') as f: - f.write(cls + '\n') - with open(os.path.join(*loc.split('.'), 'options', 'parse.py'), 'w') as f: - f.write(tc + '\n') - ctypes = [] - for opt in defn.root_group.iter_all_non_groups(): - if isinstance(opt, Option) and opt.ctype: - ctypes.append(opt) - if ctypes: - c = generate_c_conversion(loc, ctypes) - with open(os.path.join(*loc.split('.'), 'options', 'to-c-generated.h'), 'w') as f: - f.write(c + '\n') +from kitty.conf.generate import write_output def main() -> None: diff --git a/kitty/conf/generate.py b/kitty/conf/generate.py new file mode 100644 index 000000000..fcbb4d53a --- /dev/null +++ b/kitty/conf/generate.py @@ -0,0 +1,425 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPLv3 Copyright: 2021, Kovid Goyal + + +import inspect +import os +import pprint +import re +import textwrap +from typing import ( + Any, Callable, Dict, Iterator, List, Set, Tuple, Union, get_type_hints +) + +from kitty.conf.types import Definition, MultiOption, Option, unset + + +def chunks(lst: List, n: int) -> Iterator[List]: + for i in range(0, len(lst), n): + yield lst[i:i + n] + + +def atoi(text: str) -> str: + return f'{int(text):08d}' if text.isdigit() else text + + +def natural_keys(text: str) -> Tuple[str, ...]: + return tuple(atoi(c) for c in re.split(r'(\d+)', text)) + + +def generate_class(defn: Definition, loc: str) -> Tuple[str, str]: + class_lines: List[str] = [] + tc_lines: List[str] = [] + a = class_lines.append + t = tc_lines.append + a('class Options:') + t('class Parser:') + choices = {} + imports: Set[Tuple[str, str]] = set() + tc_imports: Set[Tuple[str, str]] = set() + + def type_name(x: type) -> str: + ans = x.__name__ + if x.__module__ and x.__module__ != 'builtins': + imports.add((x.__module__, x.__name__)) + return ans + + def option_type_as_str(x: Any) -> str: + if hasattr(x, '__name__'): + return type_name(x) + ans = repr(x) + ans = ans.replace('NoneType', 'None') + return ans + + def option_type_data(option: Union[Option, MultiOption]) -> Tuple[Callable, str]: + func = option.parser_func + if func.__module__ == 'builtins': + return func, func.__name__ + th = get_type_hints(func) + rettype = th['return'] + typ = option_type_as_str(rettype) + if isinstance(option, MultiOption): + typ = typ[typ.index('[') + 1:-1] + typ = typ.replace('Tuple', 'Dict', 1) + return func, typ + + is_mutiple_vars = {} + option_names = set() + color_table = list(map(str, range(256))) + + def parser_function_declaration(option_name: str) -> None: + t('') + t(f' def {option_name}(self, val: str, ans: typing.Dict[str, typing.Any]) -> None:') + + for option in sorted(defn.iter_all_options(), key=lambda a: natural_keys(a.name)): + option_names.add(option.name) + parser_function_declaration(option.name) + if isinstance(option, MultiOption): + mval: Dict[str, Dict[str, Any]] = {'macos': {}, 'linux': {}, '': {}} + func, typ = option_type_data(option) + for val in option: + if val.add_to_default: + gr = mval[val.only] + for k, v in func(val.defval_as_str): + gr[k] = v + is_mutiple_vars[option.name] = typ, mval + sig = inspect.signature(func) + tc_imports.add((func.__module__, func.__name__)) + if len(sig.parameters) == 1: + t(f' for k, v in {func.__name__}(val):') + t(f' ans["{option.name}"][k] = v') + else: + t(f' for k, v in {func.__name__}(val, ans["{option.name}"]):') + t(f' ans["{option.name}"][k] = v') + continue + + if option.choices: + typ = 'typing.Literal[{}]'.format(', '.join(repr(x) for x in option.choices)) + ename = f'choices_for_{option.name}' + choices[ename] = typ + typ = ename + func = str + elif defn.has_color_table and option.is_color_table_color: + func, typ = option_type_data(option) + t(f' ans[{option.name!r}] = {func.__name__}(val)') + tc_imports.add((func.__module__, func.__name__)) + cnum = int(option.name[5:]) + color_table[cnum] = '0x{:06x}'.format(func(option.defval_as_string).__int__()) + continue + else: + func, typ = option_type_data(option) + try: + params = inspect.signature(func).parameters + except Exception: + params = {} + if 'dict_with_parse_results' in params: + t(f' {func.__name__}(val, ans)') + else: + t(f' ans[{option.name!r}] = {func.__name__}(val)') + if func.__module__ != 'builtins': + tc_imports.add((func.__module__, func.__name__)) + + defval = repr(func(option.defval_as_string)) + if option.macos_defval is not unset: + md = repr(func(option.macos_defval)) + defval = f'{md} if is_macos else {defval}' + imports.add(('kitty.constants', 'is_macos')) + a(f' {option.name}: {typ} = {defval}') + if option.choices: + t(' val = val.lower()') + t(f' if val not in self.choices_for_{option.name}:') + t(f' raise ValueError(f"The value {{val}} is not a valid choice for {option.name}")') + t(f' ans["{option.name}"] = val') + t('') + t(f' choices_for_{option.name} = frozenset({option.choices!r})') + + for option_name, (typ, mval) in is_mutiple_vars.items(): + a(f' {option_name}: {typ} = ' '{}') + + for parser, aliases in defn.deprecations.items(): + for alias in aliases: + parser_function_declaration(alias) + tc_imports.add((parser.__module__, parser.__name__)) + t(f' {parser.__name__}({alias!r}, val, ans)') + + action_parsers = {} + + def resolve_import(ftype: str) -> str: + if '.' in ftype: + fmod, ftype = ftype.rpartition('.')[::2] + else: + fmod = f'{loc}.options.utils' + imports.add((fmod, ftype)) + return ftype + + for aname, action in defn.actions.items(): + option_names.add(aname) + action_parsers[aname] = func = action.parser_func + th = get_type_hints(func) + rettype = th['return'] + typ = option_type_as_str(rettype) + typ = typ[typ.index('[') + 1:-1] + a(f' {aname}: typing.List[{typ}] = []') + for imp in action.imports: + resolve_import(imp) + for fname, ftype in action.fields.items(): + ftype = resolve_import(ftype) + a(f' {fname}: {ftype} = ' '{}') + parser_function_declaration(aname) + t(f' for k in {func.__name__}(val):') + t(f' ans[{aname!r}].append(k)') + tc_imports.add((func.__module__, func.__name__)) + + if defn.has_color_table: + imports.add(('array', 'array')) + a(' color_table: array = array("L", (') + for grp in chunks(color_table, 8): + a(' ' + ', '.join(grp) + ',') + a(' ))') + + a(' config_paths: typing.Tuple[str, ...] = ()') + a(' config_overrides: typing.Tuple[str, ...] = ()') + a('') + a(' def __init__(self, options_dict: typing.Optional[typing.Dict[str, typing.Any]] = None) -> None:') + a(' if options_dict is not None:') + a(' for key in option_names:') + a(' setattr(self, key, options_dict[key])') + + a('') + a(' @property') + a(' def _fields(self) -> typing.Tuple[str, ...]:') + a(' return option_names') + + a('') + a(' def __iter__(self) -> typing.Iterator[str]:') + a(' return iter(self._fields)') + + a('') + a(' def __len__(self) -> int:') + a(' return len(self._fields)') + + a('') + a(' def _copy_of_val(self, name: str) -> typing.Any:') + a(' ans = getattr(self, name)') + a(' if isinstance(ans, dict):\n ans = ans.copy()') + a(' elif isinstance(ans, list):\n ans = ans[:]') + a(' return ans') + + a('') + a(' def _asdict(self) -> typing.Dict[str, typing.Any]:') + a(' return {k: self._copy_of_val(k) for k in self}') + + a('') + a(' def _replace(self, **kw: typing.Any) -> "Options":') + a(' ans = Options()') + a(' for name in self:') + a(' setattr(ans, name, self._copy_of_val(name))') + a(' for name, val in kw.items():') + a(' setattr(ans, name, val)') + a(' return ans') + + a('') + a(' def __getitem__(self, key: typing.Union[int, str]) -> typing.Any:') + a(' k = option_names[key] if isinstance(key, int) else key') + a(' try:') + a(' return getattr(self, k)') + a(' except AttributeError:') + a(' pass') + a(' raise KeyError(f"No option named: {k}")') + + if defn.has_color_table: + a('') + a(' def __getattr__(self, key: str) -> typing.Any:') + a(' if key.startswith("color"):') + a(' q = key[5:]') + a(' if q.isdigit():') + a(' k = int(q)') + a(' if 0 <= k <= 255:') + a(' x = self.color_table[k]') + a(' return Color((x >> 16) & 255, (x >> 8) & 255, x & 255)') + a(' raise AttributeError(key)') + a('') + a(' def __setattr__(self, key: str, val: typing.Any) -> typing.Any:') + a(' if key.startswith("color"):') + a(' q = key[5:]') + a(' if q.isdigit():') + a(' k = int(q)') + a(' if 0 <= k <= 255:') + a(' self.color_table[k] = int(val)') + a(' return') + a(' object.__setattr__(self, key, val)') + + a('') + a('') + a('defaults = Options()') + for option_name, (typ, mval) in is_mutiple_vars.items(): + a(f'defaults.{option_name} = {mval[""]!r}') + if mval['macos']: + imports.add(('kitty.constants', 'is_macos')) + a('if is_macos:') + a(f' defaults.{option_name}.update({mval["macos"]!r}') + if mval['macos']: + imports.add(('kitty.constants', 'is_macos')) + a('if not is_macos:') + a(f' defaults.{option_name}.update({mval["linux"]!r}') + + for aname, func in action_parsers.items(): + a(f'defaults.{aname} = [') + only: Dict[str, List[Tuple[str, Callable]]] = {} + for sc in defn.iter_all_maps(aname): + if not sc.add_to_default: + continue + text = sc.parseable_text + if sc.only: + only.setdefault(sc.only, []).append((text, func)) + else: + for val in func(text): + a(f' # {sc.name}') + a(f' {val!r},') + a(']') + if only: + imports.add(('kitty.constants', 'is_macos')) + for cond, items in only.items(): + cond = 'is_macos' if cond == 'macos' else 'not is_macos' + a(f'if {cond}:') + for (text, func) in items: + for val in func(text): + a(f' defaults.{aname}.append({val!r})') + + t('') + t('') + t('def create_result_dict() -> typing.Dict[str, typing.Any]:') + t(' return {') + for oname in is_mutiple_vars: + t(f' {oname!r}: {{}},') + for aname in defn.actions: + t(f' {aname!r}: [],') + t(' }') + + t('') + t('') + t(f'actions = frozenset({tuple(defn.actions)!r})') + t('') + t('') + t('def merge_result_dicts(defaults: typing.Dict[str, typing.Any], vals: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:') + t(' ans = {}') + t(' for k, v in defaults.items():') + t(' if isinstance(v, dict):') + t(' ans[k] = merge_dicts(v, vals.get(k, {}))') + t(' elif k in actions:') + t(' ans[k] = v + vals.get(k, [])') + t(' else:') + t(' ans[k] = vals.get(k, v)') + t(' return ans') + tc_imports.add(('kitty.conf.utils', 'merge_dicts')) + + t('') + t('') + t('parser = Parser()') + t('') + t('') + t('def parse_conf_item(key: str, val: str, ans: typing.Dict[str, typing.Any]) -> bool:') + t(' func = getattr(parser, key, None)') + t(' if func is not None:') + t(' func(val, ans)') + t(' return True') + t(' return False') + + preamble = ['# generated by gen-config.py DO NOT edit', '# vim:fileencoding=utf-8', ''] + a = preamble.append + + def output_imports(imports: Set, add_module_imports: bool = True) -> None: + a('import typing') + seen_mods = {'typing'} + mmap: Dict[str, List[str]] = {} + for mod, name in imports: + mmap.setdefault(mod, []).append(name) + for mod in sorted(mmap): + names = sorted(mmap[mod]) + lines = textwrap.wrap(', '.join(names), 100) + if len(lines) == 1: + s = lines[0] + else: + s = '\n '.join(lines) + s = f'(\n {s}\n)' + a(f'from {mod} import {s}') + if add_module_imports and mod not in seen_mods and mod != s: + a(f'import {mod}') + seen_mods.add(mod) + + output_imports(imports) + a('') + if choices: + a('if typing.TYPE_CHECKING:') + for name, cdefn in choices.items(): + a(f' {name} = {cdefn}') + a('else:') + for name in choices: + a(f' {name} = str') + + a('') + a('option_names = ( # {{''{') + a(' ' + pprint.pformat(tuple(sorted(option_names, key=natural_keys)))[1:] + ' # }}''}') + class_def = '\n'.join(preamble + ['', ''] + class_lines) + + preamble = ['# generated by gen-config.py DO NOT edit', '# vim:fileencoding=utf-8', ''] + a = preamble.append + output_imports(tc_imports, False) + + return class_def, '\n'.join(preamble + ['', ''] + tc_lines) + + +def generate_c_conversion(loc: str, ctypes: List[Option]) -> str: + lines: List[str] = [] + basic_converters = { + 'int': 'PyLong_AsLong', 'uint': 'PyLong_AsUnsignedLong', 'bool': 'PyObject_IsTrue', + 'float': 'PyFloat_AsFloat', 'double': 'PyFloat_AsDouble', + 'time': 'parse_s_double_to_monotonic_t', 'time-ms': 'parse_ms_long_to_monotonic_t' + } + + for opt in ctypes: + lines.append('') + lines.append(f'static void\nconvert_from_python_{opt.name}(PyObject *val, Options *opts) ''{') + is_special = opt.ctype.startswith('!') + if is_special: + func = opt.ctype[1:] + lines.append(f' {func}(val, opts);') + else: + func = basic_converters.get(opt.ctype, opt.ctype) + lines.append(f' opts->{opt.name} = {func}(val);') + lines.append('}') + lines.append('') + lines.append(f'static void\nconvert_from_opts_{opt.name}(PyObject *py_opts, Options *opts) ''{') + lines.append(f' PyObject *ret = PyObject_GetAttrString(py_opts, "{opt.name}");') + lines.append(' if (ret == NULL) return;') + lines.append(f' convert_from_python_{opt.name}(ret, opts);') + lines.append(' Py_DECREF(ret);') + lines.append('}') + + lines.append('') + lines.append('static bool\nconvert_opts_from_python_opts(PyObject *py_opts, Options *opts) ''{') + for opt in ctypes: + lines.append(f' convert_from_opts_{opt.name}(py_opts, opts);') + lines.append(' if (PyErr_Occurred()) return false;') + lines.append(' return true;') + lines.append('}') + + preamble = ['// generated by gen-config.py DO NOT edit', '// vim:fileencoding=utf-8', '#pragma once', '#include "to-c.h"'] + return '\n'.join(preamble + ['', ''] + lines) + + +def write_output(loc: str, defn: Definition) -> None: + cls, tc = generate_class(defn, loc) + with open(os.path.join(*loc.split('.'), 'options', 'types.py'), 'w') as f: + f.write(cls + '\n') + with open(os.path.join(*loc.split('.'), 'options', 'parse.py'), 'w') as f: + f.write(tc + '\n') + ctypes = [] + for opt in defn.root_group.iter_all_non_groups(): + if isinstance(opt, Option) and opt.ctype: + ctypes.append(opt) + if ctypes: + c = generate_c_conversion(loc, ctypes) + with open(os.path.join(*loc.split('.'), 'options', 'to-c-generated.h'), 'w') as f: + f.write(c + '\n')