#!/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, List, Set, Tuple, Union, get_type_hints ) from kitty.conf.types import Definition, MultiOption, Option, unset 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() 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 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__)) 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}")') 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)) for val in func(text): 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, names in mmap.items(): names = sorted(names) 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: 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 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) with open(os.path.join(*loc.split('.'), 'options', 'parse.py'), 'w') as f: f.write(tc) def main() -> None: from kitty.options.definition import definition write_output('kitty', definition) from kittens.diff.options.definition import definition as kd write_output('kittens.diff', kd) if __name__ == '__main__': main()