Proper handling of disabled cli flags

This commit is contained in:
Kovid Goyal
2025-04-29 10:26:28 +05:30
parent b950cb85ce
commit 2f921d6dec
3 changed files with 66 additions and 46 deletions

View File

@@ -6,9 +6,6 @@ from collections.abc import Sequence
class CLIOptions:
# these are present here because the parser doesn't set them on macOS
cls: str = ''
name: str = ''
def __repr__(self) -> str:
return repr(vars(self))

View File

@@ -64,7 +64,7 @@ typedef struct FlagSpec {
typedef struct CLISpec {
cli_hash value_map;
alias_hash alias_map;
flag_hash flag_map;
flag_hash flag_map, disabled_map;
char **argv; int argc; // leftover args
char **original_argv; int original_argc; // original args
const char* errmsg;
@@ -274,6 +274,7 @@ alloc_cli_spec(CLISpec *spec) {
vt_init(&spec->value_map);
vt_init(&spec->alias_map);
vt_init(&spec->flag_map);
vt_init(&spec->disabled_map);
}
static void
@@ -284,6 +285,7 @@ dealloc_cli_spec(void *v) {
vt_cleanup(&spec->value_map);
vt_cleanup(&spec->alias_map);
vt_cleanup(&spec->flag_map);
vt_cleanup(&spec->disabled_map);
}
#define RAII_CLISpec(name) __attribute__((cleanup(dealloc_cli_spec))) CLISpec name = {0}; alloc_cli_spec(&name)
@@ -428,6 +430,31 @@ get_string_cli_val(CLISpec *spec, const char *name) {
}
#endif
static bool
clival_as_python(const CLIValue *v, PyObject *is_seen, const char *dest, PyObject *ans) {
#define S(fv) { \
RAII_PyObject(temp, Py_BuildValue("NO", fv, is_seen)); if (!temp) return false; \
if (PyDict_SetItemString(ans, dest, temp) != 0) return false; \
}
switch (v->type) {
case CLI_VALUE_BOOL: S(PyBool_FromLong((long)v->boolval)); break;
case CLI_VALUE_STRING: if (v->strval) { S(PyUnicode_FromString(v->strval)); } else { S(Py_NewRef(Py_None)); } break;
case CLI_VALUE_CHOICE: S(PyUnicode_FromString(v->strval)); break;
case CLI_VALUE_INT: S(PyLong_FromLongLong(v->intval)); break;
case CLI_VALUE_FLOAT: S(PyFloat_FromDouble(v->floatval)); break;
case CLI_VALUE_LIST: {
RAII_PyObject(l, PyList_New(v->listval.count)); if (!l) return false;
for (size_t i = 0; i < v->listval.count; i++) {
PyObject *x = PyUnicode_FromString(v->listval.items[i]); if (!x) return false;
PyList_SET_ITEM(l, i, x);
}
S(Py_NewRef(l));
} break;
}
#undef S
return true;
}
static PyObject*
cli_parse_result_as_python(CLISpec *spec) {
if (PyErr_Occurred()) return NULL;
@@ -440,25 +467,12 @@ cli_parse_result_as_python(CLISpec *spec) {
cli_hash_itr i = vt_get(&spec->value_map, flag->dest);
PyObject *is_seen = vt_is_end(i) ? Py_False : Py_True;
const CLIValue *v = is_seen == Py_True ? &i.data->val : &flag->defval;
#define S(fv) { RAII_PyObject(temp, Py_BuildValue("NO", fv, is_seen)); if (!temp) return NULL; \
if (PyDict_SetItemString(ans, flag->dest, temp) != 0) return NULL;}
switch (v->type) {
case CLI_VALUE_BOOL: S(PyBool_FromLong((long)v->boolval)); break;
case CLI_VALUE_STRING: if (v->strval) { S(PyUnicode_FromString(v->strval)); } else { S(Py_NewRef(Py_None)); } break;
case CLI_VALUE_CHOICE: S(PyUnicode_FromString(v->strval)); break;
case CLI_VALUE_INT: S(PyLong_FromLongLong(v->intval)); break;
case CLI_VALUE_FLOAT: S(PyFloat_FromDouble(v->floatval)); break;
case CLI_VALUE_LIST: {
RAII_PyObject(l, PyList_New(v->listval.count)); if (!l) return NULL;
for (size_t i = 0; i < v->listval.count; i++) {
PyObject *x = PyUnicode_FromString(v->listval.items[i]); if (!x) return NULL;
PyList_SET_ITEM(l, i, x);
}
S(Py_NewRef(l));
} break;
}
if (!clival_as_python(v, is_seen, flag->dest, ans)) return NULL;
}
flag_map_for_loop(&spec->disabled_map) {
const FlagSpec *flag = &itr.data->val;
if (!clival_as_python(&flag->defval, Py_False, flag->dest, ans)) return NULL;
}
#undef S
RAII_PyObject(leftover_args, PyList_New(spec->argc)); if (!leftover_args) return NULL;
for (int i = 0; i < spec->argc; i++) {
PyObject *t = PyUnicode_FromString(spec->argv[i]);

View File

@@ -259,8 +259,34 @@ def add_list_values(*values: str) -> Iterator[str]:
yield f'\tflag.defval.listval.items[{n}] = {c_str(value)};'
def generate_c_for_opt(name: str, defval: Any, opt: OptionDict) -> Iterator[str]:
yield f'\tflag = (FlagSpec){{.dest={c_str(name)},}};'
match opt['type']:
case 'bool-set' | 'bool-reset':
yield '\tflag.defval.type = CLI_VALUE_BOOL;'
yield f'\tflag.defval.boolval = {"true" if defval else "false"};'
case 'int':
yield '\tflag.defval.type = CLI_VALUE_INT;'
yield f'\tflag.defval.intval = {defval};'
case 'float':
yield '\tflag.defval.type = CLI_VALUE_FLOAT;'
yield f'\tflag.defval.floatval = {defval};'
case 'list':
yield '\tflag.defval.type = CLI_VALUE_LIST;'
if defval:
yield from add_list_values(*defval)
case 'choices':
yield '\tflag.defval.type = CLI_VALUE_CHOICE;'
yield f'\tflag.defval.strval = {c_str(defval)};'
yield from add_list_values(*opt['choices'])
case _:
yield '\tflag.defval.type = CLI_VALUE_STRING;'
yield f'\tflag.defval.strval = {"NULL" if defval is None else c_str(defval)};'
def generate_c_parser_for(funcname: str, spec: str) -> Iterator[str]:
names_map, _, defaults_map = get_option_maps(parse_option_spec(spec)[0])
seq, disabled = parse_option_spec(spec)
names_map, _, defaults_map = get_option_maps(seq)
if 'help' not in names_map:
names_map['help'] = {'type': 'bool-set', 'aliases': ('--help', '-h')} # type: ignore
defaults_map['help'] = False
@@ -273,30 +299,13 @@ def generate_c_parser_for(funcname: str, spec: str) -> Iterator[str]:
for name, opt in names_map.items():
for alias in opt['aliases']:
yield f'\tif (vt_is_end(vt_insert(&spec->alias_map, {c_str(alias)}, {c_str(name)}))) OOM;'
yield f'\tflag = (FlagSpec){{.dest={c_str(name)},}};'
defval = defaults_map[name]
match opt['type']:
case 'bool-set' | 'bool-reset':
yield '\tflag.defval.type = CLI_VALUE_BOOL;'
yield f'\tflag.defval.boolval = {"true" if defval else "false"};'
case 'int':
yield '\tflag.defval.type = CLI_VALUE_INT;'
yield f'\tflag.defval.intval = {defval};'
case 'float':
yield '\tflag.defval.type = CLI_VALUE_FLOAT;'
yield f'\tflag.defval.floatval = {defval};'
case 'list':
yield '\tflag.defval.type = CLI_VALUE_LIST;'
if defval:
yield from add_list_values(*defval)
case 'choices':
yield '\tflag.defval.type = CLI_VALUE_CHOICE;'
yield f'\tflag.defval.strval = {c_str(defval)};'
yield from add_list_values(*opt['choices'])
case _:
yield '\tflag.defval.type = CLI_VALUE_STRING;'
yield f'\tflag.defval.strval = {"NULL" if defval is None else c_str(defval)};'
yield from generate_c_for_opt(name, defaults_map[name], opt)
yield '\tif (vt_is_end(vt_insert(&spec->flag_map, flag.dest, flag))) OOM;'
for d in disabled:
if not isinstance(d, str):
yield from generate_c_for_opt(d['dest'], defval_for_opt(d), d)
yield '\tif (vt_is_end(vt_insert(&spec->disabled_map, flag.dest, flag))) OOM;'
yield '\tparse_cli_loop(spec, true, argc, argv);'
yield '}'