mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-10 22:24:24 +02:00
Add type checking for the various CLI options objects
This commit is contained in:
235
kitty/cli.py
235
kitty/cli.py
@@ -6,9 +6,30 @@ import os
|
||||
import re
|
||||
import sys
|
||||
from collections import deque
|
||||
from typing import (
|
||||
Any, Callable, Dict, FrozenSet, Iterator, List, Optional, Tuple, Type,
|
||||
TypeVar, Union, cast
|
||||
)
|
||||
|
||||
from .conf.utils import resolve_config
|
||||
from .constants import appname, defconf, is_macos, is_wayland, str_version
|
||||
from .cli_stub import CLIOptions
|
||||
from .options_stub import Options as OptionsStub
|
||||
|
||||
try:
|
||||
from typing import TypedDict
|
||||
|
||||
class OptionDict(TypedDict):
|
||||
dest: str
|
||||
aliases: FrozenSet[str]
|
||||
help: str
|
||||
choices: FrozenSet[str]
|
||||
type: str
|
||||
default: Optional[str]
|
||||
condition: bool
|
||||
except ImportError:
|
||||
OptionDict = Dict[str, Any] # type: ignore
|
||||
|
||||
|
||||
CONFIG_HELP = '''\
|
||||
Specify a path to the configuration file(s) to use. All configuration files are
|
||||
@@ -34,49 +55,49 @@ defaults for all users.
|
||||
)
|
||||
|
||||
|
||||
def surround(x, start, end):
|
||||
def surround(x: str, start: int, end: int) -> str:
|
||||
if sys.stdout.isatty():
|
||||
x = '\033[{}m{}\033[{}m'.format(start, x, end)
|
||||
return x
|
||||
|
||||
|
||||
def emph(x):
|
||||
def emph(x: str) -> str:
|
||||
return surround(x, 91, 39)
|
||||
|
||||
|
||||
def cyan(x):
|
||||
def cyan(x: str) -> str:
|
||||
return surround(x, 96, 39)
|
||||
|
||||
|
||||
def green(x):
|
||||
def green(x: str) -> str:
|
||||
return surround(x, 32, 39)
|
||||
|
||||
|
||||
def blue(x):
|
||||
def blue(x: str) -> str:
|
||||
return surround(x, 34, 39)
|
||||
|
||||
|
||||
def yellow(x):
|
||||
def yellow(x: str) -> str:
|
||||
return surround(x, 93, 39)
|
||||
|
||||
|
||||
def italic(x):
|
||||
def italic(x: str) -> str:
|
||||
return surround(x, 3, 23)
|
||||
|
||||
|
||||
def bold(x):
|
||||
def bold(x: str) -> str:
|
||||
return surround(x, 1, 22)
|
||||
|
||||
|
||||
def title(x):
|
||||
def title(x: str) -> str:
|
||||
return blue(bold(x))
|
||||
|
||||
|
||||
def opt(text):
|
||||
def opt(text: str) -> str:
|
||||
return text
|
||||
|
||||
|
||||
def option(x):
|
||||
def option(x: str) -> str:
|
||||
idx = x.find('-')
|
||||
if idx > -1:
|
||||
x = x[idx:]
|
||||
@@ -84,33 +105,39 @@ def option(x):
|
||||
return ' '.join(parts)
|
||||
|
||||
|
||||
def code(x):
|
||||
def code(x: str) -> str:
|
||||
return x
|
||||
|
||||
|
||||
def kbd(x):
|
||||
def kbd(x: str) -> str:
|
||||
return x
|
||||
|
||||
|
||||
def env(x):
|
||||
def env(x: str) -> str:
|
||||
return italic(x)
|
||||
|
||||
|
||||
def file(x):
|
||||
def file(x: str) -> str:
|
||||
return italic(x)
|
||||
|
||||
|
||||
def parse_option_spec(spec=None):
|
||||
OptionSpecSeq = List[Union[str, OptionDict]]
|
||||
|
||||
|
||||
def parse_option_spec(spec: Optional[str] = None) -> Tuple[OptionSpecSeq, OptionSpecSeq]:
|
||||
if spec is None:
|
||||
spec = options_spec()
|
||||
NORMAL, METADATA, HELP = 'NORMAL', 'METADATA', 'HELP'
|
||||
state = NORMAL
|
||||
lines = spec.splitlines()
|
||||
prev_line = ''
|
||||
seq = []
|
||||
disabled = []
|
||||
seq: OptionSpecSeq = []
|
||||
disabled: OptionSpecSeq = []
|
||||
mpat = re.compile('([a-z]+)=(.+)')
|
||||
current_cmd = None
|
||||
current_cmd: OptionDict = {
|
||||
'dest': '', 'aliases': frozenset(), 'help': '', 'choices': frozenset(), 'type': '', 'condition': False, 'default': None
|
||||
}
|
||||
empty_cmd = current_cmd
|
||||
|
||||
for line in lines:
|
||||
line = line.rstrip()
|
||||
@@ -122,7 +149,10 @@ def parse_option_spec(spec=None):
|
||||
continue
|
||||
if line.startswith('--'):
|
||||
parts = line.split(' ')
|
||||
current_cmd = {'dest': parts[0][2:].replace('-', '_'), 'aliases': frozenset(parts), 'help': ''}
|
||||
current_cmd = {
|
||||
'dest': parts[0][2:].replace('-', '_'), 'aliases': frozenset(parts), 'help': '', 'choices': frozenset(), 'type': '',
|
||||
'default': None, 'condition': True
|
||||
}
|
||||
state = METADATA
|
||||
continue
|
||||
raise ValueError('Invalid option spec, unexpected line: {}'.format(line))
|
||||
@@ -133,11 +163,17 @@ def parse_option_spec(spec=None):
|
||||
current_cmd['help'] += line
|
||||
else:
|
||||
k, v = m.group(1), m.group(2)
|
||||
if k == 'condition':
|
||||
v = eval(v)
|
||||
current_cmd[k] = v
|
||||
if k == 'choices':
|
||||
current_cmd['choices'] = {x.strip() for x in current_cmd['choices'].split(',')}
|
||||
current_cmd['choices'] = frozenset(x.strip() for x in v.split(','))
|
||||
else:
|
||||
if k == 'default':
|
||||
current_cmd['default'] = v
|
||||
elif k == 'type':
|
||||
current_cmd['type'] = v
|
||||
elif k == 'dest':
|
||||
current_cmd['dest'] = v
|
||||
elif k == 'condition':
|
||||
current_cmd['condition'] = bool(eval(v))
|
||||
elif state is HELP:
|
||||
if line:
|
||||
spc = '' if current_cmd['help'].endswith('\n') else ' '
|
||||
@@ -148,15 +184,15 @@ def parse_option_spec(spec=None):
|
||||
else:
|
||||
state = NORMAL
|
||||
(seq if current_cmd.get('condition', True) else disabled).append(current_cmd)
|
||||
current_cmd = None
|
||||
current_cmd = empty_cmd
|
||||
prev_line = line
|
||||
if current_cmd is not None:
|
||||
if current_cmd is not empty_cmd:
|
||||
(seq if current_cmd.get('condition', True) else disabled).append(current_cmd)
|
||||
|
||||
return seq, disabled
|
||||
|
||||
|
||||
def prettify(text):
|
||||
def prettify(text: str) -> str:
|
||||
role_map = globals()
|
||||
|
||||
def sub(m):
|
||||
@@ -167,11 +203,11 @@ def prettify(text):
|
||||
return text
|
||||
|
||||
|
||||
def prettify_rst(text):
|
||||
def prettify_rst(text: str) -> str:
|
||||
return re.sub(r':([a-z]+):`([^`]+)`(=[^\s.]+)', r':\1:`\2`:code:`\3`', text)
|
||||
|
||||
|
||||
def version(add_rev=False):
|
||||
def version(add_rev: bool = False) -> str:
|
||||
rev = ''
|
||||
from . import fast_data_types
|
||||
if add_rev and hasattr(fast_data_types, 'KITTY_VCS_REV'):
|
||||
@@ -179,7 +215,7 @@ def version(add_rev=False):
|
||||
return '{} {}{} created by {}'.format(italic(appname), green(str_version), rev, title('Kovid Goyal'))
|
||||
|
||||
|
||||
def wrap(text, limit=80):
|
||||
def wrap(text: str, limit: int = 80) -> Iterator[str]:
|
||||
NORMAL, IN_FORMAT = 'NORMAL', 'IN_FORMAT'
|
||||
state = NORMAL
|
||||
last_space_at = None
|
||||
@@ -203,7 +239,7 @@ def wrap(text, limit=80):
|
||||
last_space_at = None
|
||||
chars_in_line = i - breaks[-1]
|
||||
|
||||
lines = []
|
||||
lines: List[str] = []
|
||||
for b in reversed(breaks):
|
||||
lines.append(text[b:].lstrip())
|
||||
text = text[:b]
|
||||
@@ -212,8 +248,8 @@ def wrap(text, limit=80):
|
||||
return reversed(lines)
|
||||
|
||||
|
||||
def get_defaults_from_seq(seq):
|
||||
ans = {}
|
||||
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)
|
||||
@@ -232,21 +268,21 @@ class PrintHelpForSeq:
|
||||
|
||||
allow_pager = True
|
||||
|
||||
def __call__(self, seq, usage, message, appname):
|
||||
def __call__(self, seq: OptionSpecSeq, usage: Optional[str], message: Optional[str], appname: str) -> None:
|
||||
from kitty.utils import screen_size_function
|
||||
screen_size = screen_size_function()
|
||||
try:
|
||||
linesz = min(screen_size().cols, 76)
|
||||
except OSError:
|
||||
linesz = 76
|
||||
blocks = []
|
||||
blocks: List[str] = []
|
||||
a = blocks.append
|
||||
|
||||
def wa(text, indent=0, leading_indent=None):
|
||||
if leading_indent is None:
|
||||
leading_indent = indent
|
||||
j = '\n' + (' ' * indent)
|
||||
lines = []
|
||||
lines: List[str] = []
|
||||
for l in text.splitlines():
|
||||
if l:
|
||||
lines.extend(wrap(l, limit=linesz - indent))
|
||||
@@ -279,7 +315,7 @@ class PrintHelpForSeq:
|
||||
wa(prettify(t.strip()), indent=4)
|
||||
if defval is not None:
|
||||
wa('Default: {}'.format(defval), indent=4)
|
||||
if 'choices' in opt:
|
||||
if opt.get('choices'):
|
||||
wa('Choices: {}'.format(', '.join(opt['choices'])), indent=4)
|
||||
a('')
|
||||
|
||||
@@ -299,9 +335,9 @@ class PrintHelpForSeq:
|
||||
print_help_for_seq = PrintHelpForSeq()
|
||||
|
||||
|
||||
def seq_as_rst(seq, usage, message, appname, heading_char='-'):
|
||||
def seq_as_rst(seq: OptionSpecSeq, usage: Optional[str], message: Optional[str], appname: Optional[str], heading_char: str = '-') -> str:
|
||||
import textwrap
|
||||
blocks = []
|
||||
blocks: List[str] = []
|
||||
a = blocks.append
|
||||
|
||||
usage = '[program-to-run ...]' if usage is None else usage
|
||||
@@ -338,7 +374,7 @@ def seq_as_rst(seq, usage, message, appname, heading_char='-'):
|
||||
a(textwrap.indent(prettify_rst(t), ' ' * 4))
|
||||
if defval is not None:
|
||||
a(textwrap.indent('Default: :code:`{}`'.format(defval), ' ' * 4))
|
||||
if 'choices' in opt:
|
||||
if opt.get('choices'):
|
||||
a(textwrap.indent('Choices: :code:`{}`'.format(', '.join(sorted(opt['choices']))), ' ' * 4))
|
||||
a('')
|
||||
|
||||
@@ -346,8 +382,30 @@ def seq_as_rst(seq, usage, message, appname, heading_char='-'):
|
||||
return text
|
||||
|
||||
|
||||
def defval_for_opt(opt):
|
||||
dv = opt.get('default')
|
||||
def as_type_stub(seq: OptionSpecSeq, disabled: OptionSpecSeq, class_name: str) -> str:
|
||||
from itertools import chain
|
||||
ans: List[str] = ['class {}:'.format(class_name)]
|
||||
for opt in chain(seq, disabled):
|
||||
if isinstance(opt, str):
|
||||
continue
|
||||
name = opt['dest']
|
||||
otype = opt['type'] or 'str'
|
||||
if otype in ('str', 'int', 'float'):
|
||||
t = otype
|
||||
elif otype == 'list':
|
||||
t = 'typing.Sequence[str]'
|
||||
elif otype in ('choice', 'choices'):
|
||||
t = 'str'
|
||||
elif otype.startswith('bool-'):
|
||||
t = 'bool'
|
||||
else:
|
||||
raise ValueError('Unknown CLI option type: {}'.format(otype))
|
||||
ans.append(' {}: {}'.format(name, t))
|
||||
return '\n'.join(ans) + '\n\n\n'
|
||||
|
||||
|
||||
def defval_for_opt(opt: OptionDict) -> Any:
|
||||
dv: Any = opt.get('default')
|
||||
typ = opt.get('type', '')
|
||||
if typ.startswith('bool-'):
|
||||
if dv is None:
|
||||
@@ -363,11 +421,11 @@ def defval_for_opt(opt):
|
||||
|
||||
class Options:
|
||||
|
||||
def __init__(self, seq, usage, message, appname):
|
||||
def __init__(self, seq: OptionSpecSeq, usage: Optional[str], message: Optional[str], appname: Optional[str]):
|
||||
self.alias_map = {}
|
||||
self.seq = seq
|
||||
self.names_map = {}
|
||||
self.values_map = {}
|
||||
self.names_map: Dict[str, OptionDict] = {}
|
||||
self.values_map: Dict[str, Any] = {}
|
||||
self.usage, self.message, self.appname = usage, message, appname
|
||||
for opt in seq:
|
||||
if isinstance(opt, str):
|
||||
@@ -378,13 +436,13 @@ class Options:
|
||||
self.names_map[name] = opt
|
||||
self.values_map[name] = defval_for_opt(opt)
|
||||
|
||||
def opt_for_alias(self, alias):
|
||||
def opt_for_alias(self, alias: str) -> OptionDict:
|
||||
opt = self.alias_map.get(alias)
|
||||
if opt is None:
|
||||
raise SystemExit('Unknown option: {}'.format(emph(alias)))
|
||||
return opt
|
||||
|
||||
def needs_arg(self, alias):
|
||||
def needs_arg(self, alias: str) -> bool:
|
||||
if alias in ('-h', '--help'):
|
||||
print_help_for_seq(self.seq, self.usage, self.message, self.appname or appname)
|
||||
raise SystemExit(0)
|
||||
@@ -395,7 +453,7 @@ class Options:
|
||||
typ = opt.get('type', '')
|
||||
return not typ.startswith('bool-')
|
||||
|
||||
def process_arg(self, alias, val=None):
|
||||
def process_arg(self, alias: str, val: Any = None) -> None:
|
||||
opt = self.opt_for_alias(alias)
|
||||
typ = opt.get('type', '')
|
||||
name = opt['dest']
|
||||
@@ -424,23 +482,15 @@ class Options:
|
||||
self.values_map[name] = val
|
||||
|
||||
|
||||
class Namespace:
|
||||
|
||||
def __init__(self, kwargs):
|
||||
for name, val in kwargs.items():
|
||||
setattr(self, name, val)
|
||||
|
||||
|
||||
def parse_cmdline(oc, disabled, args=None):
|
||||
def parse_cmdline(oc: Options, disabled: OptionSpecSeq, ans: Any, args: Optional[List[str]] = None) -> List[str]:
|
||||
NORMAL, EXPECTING_ARG = 'NORMAL', 'EXPECTING_ARG'
|
||||
state = NORMAL
|
||||
if args is None:
|
||||
args = sys.argv[1:]
|
||||
args = deque(args)
|
||||
dargs = deque(sys.argv[1:] if args is None else args)
|
||||
leftover_args: List[str] = []
|
||||
current_option = None
|
||||
|
||||
while args:
|
||||
arg = args.popleft()
|
||||
while dargs:
|
||||
arg = dargs.popleft()
|
||||
if state is NORMAL:
|
||||
if arg.startswith('-'):
|
||||
if arg == '--':
|
||||
@@ -458,21 +508,23 @@ def parse_cmdline(oc, disabled, args=None):
|
||||
continue
|
||||
oc.process_arg(parts[0], parts[1])
|
||||
else:
|
||||
args = [arg] + list(args)
|
||||
leftover_args = [arg] + list(dargs)
|
||||
break
|
||||
else:
|
||||
elif current_option is not None:
|
||||
oc.process_arg(current_option, arg)
|
||||
current_option, state = None, NORMAL
|
||||
if state is EXPECTING_ARG:
|
||||
raise SystemExit('An argument is required for the option: {}'.format(emph(arg)))
|
||||
|
||||
ans = Namespace(oc.values_map)
|
||||
for key, val in oc.values_map.items():
|
||||
setattr(ans, key, val)
|
||||
for opt in disabled:
|
||||
setattr(ans, opt['dest'], defval_for_opt(opt))
|
||||
return ans, list(args)
|
||||
if not isinstance(opt, str):
|
||||
setattr(ans, opt['dest'], defval_for_opt(opt))
|
||||
return leftover_args
|
||||
|
||||
|
||||
def options_spec():
|
||||
def options_spec() -> str:
|
||||
if not hasattr(options_spec, 'ans'):
|
||||
OPTIONS = '''
|
||||
--class
|
||||
@@ -618,31 +670,49 @@ Print out information about the system and kitty configuration.
|
||||
type=bool-set
|
||||
!
|
||||
'''
|
||||
options_spec.ans = OPTIONS.format(
|
||||
setattr(options_spec, 'ans', OPTIONS.format(
|
||||
appname=appname, config_help=CONFIG_HELP.format(appname=appname, conf_name=appname)
|
||||
|
||||
)
|
||||
return options_spec.ans
|
||||
))
|
||||
return getattr(options_spec, 'ans')
|
||||
|
||||
|
||||
def options_for_completion():
|
||||
def options_for_completion() -> OptionSpecSeq:
|
||||
raw = '--help -h\ntype=bool-set\nShow help for {appname} command line options\n\n{raw}'.format(
|
||||
appname=appname, raw=options_spec())
|
||||
return parse_option_spec(raw)[0]
|
||||
|
||||
|
||||
def option_spec_as_rst(ospec=options_spec, usage=None, message=None, appname=None, heading_char='-'):
|
||||
def option_spec_as_rst(
|
||||
ospec: Callable[[], str] = options_spec,
|
||||
usage: Optional[str] = None, message: Optional[str] = None, appname: Optional[str] = None,
|
||||
heading_char='-'
|
||||
) -> str:
|
||||
options = parse_option_spec(ospec())
|
||||
seq, disabled = options
|
||||
oc = Options(seq, usage, message, appname)
|
||||
return seq_as_rst(oc.seq, oc.usage, oc.message, oc.appname, heading_char=heading_char)
|
||||
|
||||
|
||||
def parse_args(args=None, ospec=options_spec, usage=None, message=None, appname=None):
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
def parse_args(
|
||||
args: Optional[List[str]] = None,
|
||||
ospec: Callable[[], str] = options_spec,
|
||||
usage: Optional[str] = None,
|
||||
message: Optional[str] = None,
|
||||
appname: Optional[str] = None,
|
||||
result_class: Type[T] = None,
|
||||
) -> Tuple[T, List[str]]:
|
||||
options = parse_option_spec(ospec())
|
||||
seq, disabled = options
|
||||
oc = Options(seq, usage, message, appname)
|
||||
return parse_cmdline(oc, disabled, args=args)
|
||||
if result_class is not None:
|
||||
ans = result_class()
|
||||
else:
|
||||
ans = cast(T, CLIOptions())
|
||||
return ans, parse_cmdline(oc, disabled, ans, args=args)
|
||||
|
||||
|
||||
SYSTEM_CONF = '/etc/xdg/kitty/kitty.conf'
|
||||
@@ -655,8 +725,8 @@ def print_shortcut(key_sequence, action):
|
||||
mmap = {m[len('GLFW_MOD_'):].lower(): x for m, x in v.items() if m.startswith('GLFW_MOD_')}
|
||||
kmap = {k[len('GLFW_KEY_'):].lower(): x for k, x in v.items() if k.startswith('GLFW_KEY_')}
|
||||
krmap = {v: k for k, v in kmap.items()}
|
||||
print_shortcut.maps = mmap, krmap
|
||||
mmap, krmap = print_shortcut.maps
|
||||
setattr(print_shortcut, 'maps', (mmap, krmap))
|
||||
mmap, krmap = getattr(print_shortcut, 'maps')
|
||||
keys = []
|
||||
for key in key_sequence:
|
||||
names = []
|
||||
@@ -667,7 +737,8 @@ def print_shortcut(key_sequence, action):
|
||||
if key:
|
||||
if is_native:
|
||||
from .fast_data_types import GLFW_KEY_UNKNOWN, glfw_get_key_name
|
||||
names.append(glfw_get_key_name(GLFW_KEY_UNKNOWN, key))
|
||||
kn = glfw_get_key_name(GLFW_KEY_UNKNOWN, key) or 'Unknown key'
|
||||
names.append(kn)
|
||||
else:
|
||||
names.append(krmap[key])
|
||||
keys.append('+'.join(names))
|
||||
@@ -700,12 +771,12 @@ def flatten_sequence_map(m):
|
||||
return ans
|
||||
|
||||
|
||||
def compare_opts(opts):
|
||||
def compare_opts(opts: OptionsStub) -> None:
|
||||
from .config import defaults, load_config
|
||||
print('\nConfig options different from defaults:')
|
||||
default_opts = load_config()
|
||||
changed_opts = [
|
||||
f for f in sorted(defaults._fields)
|
||||
f for f in sorted(defaults._fields) # type: ignore
|
||||
if f not in ('key_definitions', 'keymap', 'sequence_map') and getattr(opts, f) != getattr(defaults, f)
|
||||
]
|
||||
field_len = max(map(len, changed_opts)) if changed_opts else 20
|
||||
@@ -713,9 +784,9 @@ def compare_opts(opts):
|
||||
for f in changed_opts:
|
||||
print(title(fmt.format(f)), getattr(opts, f))
|
||||
|
||||
final, initial = opts.keymap, default_opts.keymap
|
||||
final = {(k,): v for k, v in final.items()}
|
||||
initial = {(k,): v for k, v in initial.items()}
|
||||
final_, initial_ = opts.keymap, default_opts.keymap
|
||||
final = {(k,): v for k, v in final_.items()}
|
||||
initial = {(k,): v for k, v in initial_.items()}
|
||||
final_s, initial_s = map(flatten_sequence_map, (opts.sequence_map, default_opts.sequence_map))
|
||||
final.update(final_s)
|
||||
initial.update(initial_s)
|
||||
|
||||
Reference in New Issue
Block a user