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
for kitten in all_kitten_names:
definition = get_kitten_conf_docs(kitten)
if definition:
generate_default_config(definition, f'kitten-{kitten}')
defn = get_kitten_conf_docs(kitten)
if defn is not None:
generate_default_config(defn, f'kitten-{kitten}')
from kitty.actions import as_rst
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
write_output('kittens.diff', kd)
from kittens.ssh.options.definition import definition as sd
write_output('kittens.ssh', sd)
if __name__ == '__main__':

View File

@ -22,6 +22,8 @@ from kitty.cli import (
parse_option_spec,
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.key_encoding import config_mod_map
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')
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:
from kittens.runner import get_kitten_conf_docs
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'):
od = []
kcd = kitten_cli_docs(kitten)

View File

@ -7,7 +7,7 @@ import os
import sys
from contextlib import contextmanager
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.types import run_once
@ -171,7 +171,7 @@ def get_kitten_completer(kitten: str) -> Any:
return ans
def get_kitten_conf_docs(kitten: str) -> Definition:
def get_kitten_conf_docs(kitten: str) -> Optional[Definition]:
setattr(sys, 'options_definition', None)
run_kitten(kitten, run_name='__conf__')
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 .copy import CopyInstruction
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

View File

@ -13,7 +13,7 @@ remote. Directories are copied recursively. If absolute paths are used, they are
copied as is.'''
definition = Definition(
'kittens.ssh',
'!kittens.ssh',
)
agr = definition.add_group
@ -22,7 +22,7 @@ opt = definition.add_option
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
multiple hosts can be used. Multiple hostnames can also be specified, separated
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`.
''')
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 .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.
''')
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
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

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
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
@ -442,6 +442,114 @@ def write_output(loc: str, defn: Definition) -> None:
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:
# To use run it as:
# 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()
}