Work on porting config file parsing to Go

This commit is contained in:
Kovid Goyal 2023-02-17 19:50:57 +05:30
parent 6f63d9c5d4
commit 5822bb23f0
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
10 changed files with 162 additions and 162 deletions

View File

@ -525,9 +525,9 @@ def write_conf_docs(app: Any, all_kitten_names: Iterable[str]) -> None:
from kittens.runner import get_kitten_conf_docs from kittens.runner import get_kitten_conf_docs
for kitten in all_kitten_names: for kitten in all_kitten_names:
definition = get_kitten_conf_docs(kitten) defn = get_kitten_conf_docs(kitten)
if definition: if defn is not None:
generate_default_config(definition, f'kitten-{kitten}') generate_default_config(defn, f'kitten-{kitten}')
from kitty.actions import as_rst from kitty.actions import as_rst
with open('generated/actions.rst', 'w', encoding='utf-8') as f: with open('generated/actions.rst', 'w', encoding='utf-8') as f:

View File

@ -51,8 +51,6 @@ def main() -> None:
from kittens.diff.options.definition import definition as kd from kittens.diff.options.definition import definition as kd
write_output('kittens.diff', kd) write_output('kittens.diff', kd)
from kittens.ssh.options.definition import definition as sd
write_output('kittens.ssh', sd)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -22,6 +22,8 @@ from kitty.cli import (
parse_option_spec, parse_option_spec,
serialize_as_go_string, serialize_as_go_string,
) )
from kitty.conf.generate import gen_go_code
from kitty.conf.types import Definition
from kitty.guess_mime_type import text_mimes from kitty.guess_mime_type import text_mimes
from kitty.key_encoding import config_mod_map from kitty.key_encoding import config_mod_map
from kitty.key_names import character_key_name_aliases, functional_key_name_aliases from kitty.key_names import character_key_name_aliases, functional_key_name_aliases
@ -318,8 +320,18 @@ def wrapped_kittens() -> Sequence[str]:
raise Exception('Failed to read wrapped kittens from kitty wrapper script') raise Exception('Failed to read wrapped kittens from kitty wrapper script')
def generate_conf_parser(kitten: str, defn: Definition) -> None:
with replace_if_needed(f'tools/cmd/{kitten}/conf_generated.go'):
print(f'package {kitten}')
print(gen_go_code(defn))
def kitten_clis() -> None: def kitten_clis() -> None:
from kittens.runner import get_kitten_conf_docs
for kitten in wrapped_kittens(): for kitten in wrapped_kittens():
defn = get_kitten_conf_docs(kitten)
if defn is not None:
generate_conf_parser(kitten, defn)
with replace_if_needed(f'tools/cmd/{kitten}/cli_generated.go'): with replace_if_needed(f'tools/cmd/{kitten}/cli_generated.go'):
od = [] od = []
kcd = kitten_cli_docs(kitten) kcd = kitten_cli_docs(kitten)

View File

@ -7,7 +7,7 @@ import os
import sys import sys
from contextlib import contextmanager from contextlib import contextmanager
from functools import partial from functools import partial
from typing import TYPE_CHECKING, Any, Dict, FrozenSet, Generator, List, cast from typing import TYPE_CHECKING, Any, Dict, FrozenSet, Generator, List, Optional, cast
from kitty.constants import list_kitty_resources from kitty.constants import list_kitty_resources
from kitty.types import run_once from kitty.types import run_once
@ -171,7 +171,7 @@ def get_kitten_completer(kitten: str) -> Any:
return ans return ans
def get_kitten_conf_docs(kitten: str) -> Definition: def get_kitten_conf_docs(kitten: str) -> Optional[Definition]:
setattr(sys, 'options_definition', None) setattr(sys, 'options_definition', None)
run_kitten(kitten, run_name='__conf__') run_kitten(kitten, run_name='__conf__')
ans = getattr(sys, 'options_definition') ans = getattr(sys, 'options_definition')

View File

@ -36,7 +36,6 @@ from ..tui.utils import kitty_opts, running_in_tmux
from .config import init_config from .config import init_config
from .copy import CopyInstruction from .copy import CopyInstruction
from .options.types import Options as SSHOptions from .options.types import Options as SSHOptions
from .options.utils import DELETE_ENV_VAR
from .utils import create_shared_memory, get_ssh_cli, is_extra_arg, passthrough_args from .utils import create_shared_memory, get_ssh_cli, is_extra_arg, passthrough_args

View File

@ -13,7 +13,7 @@ remote. Directories are copied recursively. If absolute paths are used, they are
copied as is.''' copied as is.'''
definition = Definition( definition = Definition(
'kittens.ssh', '!kittens.ssh',
) )
agr = definition.add_group agr = definition.add_group
@ -22,7 +22,7 @@ opt = definition.add_option
agr('bootstrap', 'Host bootstrap configuration') # {{{ agr('bootstrap', 'Host bootstrap configuration') # {{{
opt('hostname', '*', option_type='hostname', long_text=''' opt('hostname', '*', long_text='''
The hostname that the following options apply to. A glob pattern to match The hostname that the following options apply to. A glob pattern to match
multiple hosts can be used. Multiple hostnames can also be specified, separated multiple hosts can be used. Multiple hostnames can also be specified, separated
by spaces. The hostname can include an optional username in the form by spaces. The hostname can include an optional username in the form
@ -44,7 +44,7 @@ The location on the remote host where the files needed for this kitten are
installed. Relative paths are resolved with respect to :code:`$HOME`. installed. Relative paths are resolved with respect to :code:`$HOME`.
''') ''')
opt('+copy', '', option_type='copy', add_to_default=False, long_text=f''' opt('+copy', '', add_to_default=False, long_text=f'''
{copy_message} For example:: {copy_message} For example::
copy .vimrc .zshrc .config/some-dir copy .vimrc .zshrc .config/some-dir
@ -80,7 +80,7 @@ The login shell to execute on the remote host. By default, the remote user
account's login shell is used. account's login shell is used.
''') ''')
opt('+env', '', option_type='env', add_to_default=False, long_text=''' opt('+env', '', add_to_default=False, long_text='''
Specify the environment variables to be set on the remote host. Using the Specify the environment variables to be set on the remote host. Using the
name with an equal sign (e.g. :code:`env VAR=`) will set it to the empty string. name with an equal sign (e.g. :code:`env VAR=`) will set it to the empty string.
Specifying only the name (e.g. :code:`env VAR`) will remove the variable from Specifying only the name (e.g. :code:`env VAR`) will remove the variable from

View File

@ -1,93 +0,0 @@
# generated by gen-config.py DO NOT edit
# isort: skip_file
import typing
import kittens.ssh.copy
if typing.TYPE_CHECKING:
choices_for_askpass = typing.Literal['unless-set', 'ssh', 'native']
choices_for_remote_kitty = typing.Literal['if-needed', 'no', 'yes']
else:
choices_for_askpass = str
choices_for_remote_kitty = str
option_names = ( # {{{
'askpass',
'color_scheme',
'copy',
'cwd',
'env',
'hostname',
'interpreter',
'login_shell',
'remote_dir',
'remote_kitty',
'share_connections',
'shell_integration') # }}}
class Options:
askpass: choices_for_askpass = 'unless-set'
color_scheme: str = ''
cwd: str = ''
hostname: str = '*'
interpreter: str = 'sh'
login_shell: str = ''
remote_dir: str = '.local/share/kitty-ssh-kitten'
remote_kitty: choices_for_remote_kitty = 'if-needed'
share_connections: bool = True
shell_integration: str = 'inherited'
copy: typing.Dict[str, kittens.ssh.copy.CopyInstruction] = {}
env: typing.Dict[str, str] = {}
config_paths: typing.Tuple[str, ...] = ()
config_overrides: typing.Tuple[str, ...] = ()
def __init__(self, options_dict: typing.Optional[typing.Dict[str, typing.Any]] = None) -> None:
if options_dict is not None:
null = object()
for key in option_names:
val = options_dict.get(key, null)
if val is not null:
setattr(self, key, val)
@property
def _fields(self) -> typing.Tuple[str, ...]:
return option_names
def __iter__(self) -> typing.Iterator[str]:
return iter(self._fields)
def __len__(self) -> int:
return len(self._fields)
def _copy_of_val(self, name: str) -> typing.Any:
ans = getattr(self, name)
if isinstance(ans, dict):
ans = ans.copy()
elif isinstance(ans, list):
ans = ans[:]
return ans
def _asdict(self) -> typing.Dict[str, typing.Any]:
return {k: self._copy_of_val(k) for k in self}
def _replace(self, **kw: typing.Any) -> "Options":
ans = Options()
for name in self:
setattr(ans, name, self._copy_of_val(name))
for name, val in kw.items():
setattr(ans, name, val)
return ans
def __getitem__(self, key: typing.Union[int, str]) -> typing.Any:
k = option_names[key] if isinstance(key, int) else key
try:
return getattr(self, k)
except AttributeError:
pass
raise KeyError(f"No option named: {k}")
defaults = Options()
defaults.copy = {}
defaults.env = {}

View File

@ -1,56 +0,0 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
from typing import Any, Dict, Iterable, Optional, Tuple
from ..copy import CopyInstruction, parse_copy_instructions
DELETE_ENV_VAR = '_delete_this_env_var_'
def env(val: str, current_val: Dict[str, str]) -> Iterable[Tuple[str, str]]:
val = val.strip()
if val:
if '=' in val:
key, v = val.split('=', 1)
key, v = key.strip(), v.strip()
if key:
yield key, v
else:
yield val, DELETE_ENV_VAR
def copy(val: str, current_val: Dict[str, str]) -> Iterable[Tuple[str, CopyInstruction]]:
yield from parse_copy_instructions(val, current_val)
def init_results_dict(ans: Dict[str, Any]) -> Dict[str, Any]:
ans['hostname'] = '*'
ans['per_host_dicts'] = {}
return ans
def get_per_hosts_dict(results_dict: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
ans: Dict[str, Dict[str, Any]] = results_dict.get('per_host_dicts', {}).copy()
h = results_dict['hostname']
hd = {k: v for k, v in results_dict.items() if k != 'per_host_dicts'}
ans[h] = hd
return ans
first_seen_positions: Dict[str, int] = {}
def hostname(val: str, dict_with_parse_results: Optional[Dict[str, Any]] = None) -> str:
if dict_with_parse_results is not None:
ch = dict_with_parse_results['hostname']
if val != ch:
from .parse import create_result_dict
phd = get_per_hosts_dict(dict_with_parse_results)
dict_with_parse_results.clear()
dict_with_parse_results.update(phd.pop(val, create_result_dict()))
dict_with_parse_results['per_host_dicts'] = phd
dict_with_parse_results['hostname'] = val
if val not in first_seen_positions:
first_seen_positions[val] = len(first_seen_positions)
return val

View File

@ -9,7 +9,7 @@ import re
import textwrap import textwrap
from typing import Any, Callable, Dict, Iterator, List, Set, Tuple, Union, get_type_hints from typing import Any, Callable, Dict, Iterator, List, Set, Tuple, Union, get_type_hints
from kitty.conf.types import Definition, MultiOption, Option, unset from kitty.conf.types import Definition, MultiOption, Option, ParserFuncType, unset
from kitty.types import _T from kitty.types import _T
@ -442,6 +442,114 @@ def write_output(loc: str, defn: Definition) -> None:
f.write(f'{c}\n') f.write(f'{c}\n')
def go_type_data(parser_func: ParserFuncType, type_defs: List[str]) -> Tuple[str, str]:
p = parser_func.__name__
if p == 'int':
return 'int64', 'strconv.ParseInt(val, 10, 64)'
if p == 'str':
return 'string', 'val, nil'
if p == 'float':
return 'float64', 'strconv.ParseFloat(val, 10, 64)'
if p == 'to_bool':
return 'bool', 'utils.StringToBool(val), nil'
th = get_type_hints(parser_func)
rettype = th['return']
return {int: 'int64', str: 'string', float: 'float64'}[rettype], f'{p}(val)'
def gen_go_code(defn: Definition) -> str:
lines = ['import "fmt"', 'import "strconv"', 'import "kitty/tools/utils"', 'var _ = fmt.Println', 'var _ = utils.StringToBool', 'var _ = strconv.Atoi']
a = lines.append
choices = {}
go_types = {}
go_parsers = {}
defaults = {}
multiopts = {''}
type_defs = ['']
for option in sorted(defn.iter_all_options(), key=lambda a: natural_keys(a.name)):
name = option.name.capitalize()
if isinstance(option, MultiOption):
go_types[name], go_parsers[name] = go_type_data(option.parser_func, type_defs)
multiopts.add(name)
else:
defaults[name] = option.defval_as_string
if option.choices:
choices[name] = option.choices
go_types[name] = f'{name}_Choice_Type'
go_parsers[name] = f'Parse_{name}(val)'
continue
go_types[name], go_parsers[name] = go_type_data(option.parser_func, type_defs)
for oname in choices:
a(f'type {go_types[oname]} int')
for td in type_defs:
a(td)
a('type Config struct {')
for name, gotype in go_types.items():
if name in multiopts:
a(f'{name} []{gotype}')
else:
a(f'{name} {gotype}')
a('}')
a('func NewConfig() *Config {')
a('ans := Config{}')
a('var err error')
a('var val string')
for name, pname in go_parsers.items():
if name in multiopts:
a(f'ans.{name} = make([]{go_types[name]}, 0, 8)')
continue
a(f'val = `{defaults[name]}`')
a(f'ans.{name}, err = {pname}')
a('if err != nil { panic(err) }')
a('return &ans')
a('}')
def cval(x: str) -> str:
return x.replace('-', '_')
for oname, choice_vals in choices.items():
a('const (')
for i, c in enumerate(choice_vals):
c = cval(c)
if i == 0:
a(f'{oname}_{c} {oname}_Choice_Type = iota')
else:
a(f'{oname}_{c}')
a(')')
a(f'func (x {oname}_Choice_Type) String() string'' {')
a('switch x {')
a('default: return ""')
for c in choice_vals:
a(f'case {oname}_{cval(c)}: return "{c}"')
a('}''}')
a(f'func {go_parsers[oname].split("(")[0]}(val string) (ans {go_types[oname]}, err error) ''{')
a('switch val {')
for c in choice_vals:
a(f'case "{c}": return {oname}_{cval(c)}, nil')
vals = ', '.join(choice_vals)
a(f'default: return ans, fmt.Errorf("%#v is not a valid value for %s. Valid values are: %s", val, "{c}", "{vals}")')
a('}''}')
a('func (c *Config) Parse(key, val string) (err error) {')
a('switch key {')
a('default: return fmt.Errorf("Unknown configuration key: %#v", key)')
for oname, pname in go_parsers.items():
ol = oname.lower()
a(f'case "{ol}":')
a(f'var temp_val {go_types[oname]}')
a(f'temp_val, err = {pname}')
a(f'if err != nil {{ return fmt.Errorf("Failed to parse {ol} = %#v with error: %w", val, err) }}')
if oname in multiopts:
a(f'c.{oname} = append(c.{oname}, temp_val)')
else:
a(f'c.{oname} = temp_val')
a('}')
a('return}')
return '\n'.join(lines)
def main() -> None: def main() -> None:
# To use run it as: # To use run it as:
# kitty +runpy 'from kitty.conf.generate import main; main()' /path/to/kitten/file.py # kitty +runpy 'from kitty.conf.generate import main; main()' /path/to/kitten/file.py

32
tools/utils/config.go Normal file
View File

@ -0,0 +1,32 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package utils
import (
"bufio"
"fmt"
"io"
"strings"
)
var _ = fmt.Print
func StringToBool(x string) bool {
x = strings.ToLower(x)
return x == "y" || x == "yes" || x == "true"
}
func ParseConfData(src io.Reader, callback func(key, val string, line int)) error {
scanner := bufio.NewScanner(src)
lnum := 0
for scanner.Scan() {
line := strings.TrimLeft(scanner.Text(), " ")
lnum++
if line == "" || strings.HasPrefix(line, "#") {
continue
}
key, val, _ := strings.Cut(line, " ")
callback(key, val, lnum)
}
return scanner.Err()
}