clan config: support new types nullOr and passwdEntry

This commit is contained in:
DavHau
2023-09-24 14:24:32 +01:00
parent 851e33d794
commit 3783359f08
2 changed files with 49 additions and 12 deletions

View File

@@ -7,7 +7,7 @@ import shlex
import subprocess import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
from typing import Any, Optional, Tuple, Type from typing import Any, Optional, Tuple, get_origin
from clan_cli.dirs import get_clan_flake_toplevel from clan_cli.dirs import get_clan_flake_toplevel
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
@@ -19,7 +19,7 @@ script_dir = Path(__file__).parent
# nixos option type description to python type # nixos option type description to python type
def map_type(type: str) -> Type: def map_type(type: str) -> Any:
if type == "boolean": if type == "boolean":
return bool return bool
elif type in [ elif type in [
@@ -30,6 +30,12 @@ def map_type(type: str) -> Type:
return int return int
elif type == "string": elif type == "string":
return str return str
# lib.type.passwdEntry
elif type == "string, not containing newlines or colons":
return str
elif type.startswith("null or "):
subtype = type.removeprefix("null or ")
return Optional[map_type(subtype)]
elif type.startswith("attribute set of"): elif type.startswith("attribute set of"):
subtype = type.removeprefix("attribute set of ") subtype = type.removeprefix("attribute set of ")
return dict[str, map_type(subtype)] # type: ignore return dict[str, map_type(subtype)] # type: ignore
@@ -65,10 +71,10 @@ class AllContainer(list):
# value is always a list, as the arg parser cannot know the type upfront # value is always a list, as the arg parser cannot know the type upfront
# and therefore always allows multiple arguments. # and therefore always allows multiple arguments.
def cast(value: Any, type: Type, opt_description: str) -> Any: def cast(value: Any, type: Any, opt_description: str) -> Any:
try: try:
# handle bools # handle bools
if isinstance(type(), bool): if isinstance(type, bool):
if value[0] in ["true", "True", "yes", "y", "1"]: if value[0] in ["true", "True", "yes", "y", "1"]:
return True return True
elif value[0] in ["false", "False", "no", "n", "0"]: elif value[0] in ["false", "False", "no", "n", "0"]:
@@ -76,17 +82,21 @@ def cast(value: Any, type: Type, opt_description: str) -> Any:
else: else:
raise ClanError(f"Invalid value {value} for boolean") raise ClanError(f"Invalid value {value} for boolean")
# handle lists # handle lists
elif isinstance(type(), list): elif get_origin(type) == list:
subtype = type.__args__[0] subtype = type.__args__[0]
return [cast([x], subtype, opt_description) for x in value] return [cast([x], subtype, opt_description) for x in value]
# handle dicts # handle dicts
elif isinstance(type(), dict): elif get_origin(type) == dict:
if not isinstance(value, dict): if not isinstance(value, dict):
raise ClanError( raise ClanError(
f"Cannot set {opt_description} directly. Specify a suboption like {opt_description}.<name>" f"Cannot set {opt_description} directly. Specify a suboption like {opt_description}.<name>"
) )
subtype = type.__args__[1] subtype = type.__args__[1]
return {k: cast(v, subtype, opt_description) for k, v in value.items()} return {k: cast(v, subtype, opt_description) for k, v in value.items()}
elif str(type) == "typing.Optional[str]":
if value[0] in ["null", "None"]:
return None
return value[0]
else: else:
if len(value) > 1: if len(value) > 1:
raise ClanError(f"Too many values for {opt_description}") raise ClanError(f"Too many values for {opt_description}")
@@ -241,6 +251,11 @@ def set_option(
option_description: str = "", option_description: str = "",
show_trace: bool = False, show_trace: bool = False,
) -> None: ) -> None:
option_path_orig = option.split(".")
# returns for example:
# option: "users.users.<name>.name"
# value: "my-name"
option, value = find_option( option, value = find_option(
option=option, option=option,
value=value, value=value,
@@ -249,18 +264,20 @@ def set_option(
) )
option_path = option.split(".") option_path = option.split(".")
option_path_store = option_path_orig[: len(option_path)]
target_type = map_type(options[option]["type"]) target_type = map_type(options[option]["type"])
casted = cast(value, target_type, option) casted = cast(value, target_type, option)
# construct a nested dict from the option path and set the value # construct a nested dict from the option path and set the value
result: dict[str, Any] = {} result: dict[str, Any] = {}
current = result current = result
for part in option_path[:-1]: for part in option_path_store[:-1]:
current[part] = {} current[part] = {}
current = current[part] current = current[part]
current[option_path[-1]] = value current[option_path_store[-1]] = value
current[option_path[-1]] = casted current[option_path_store[-1]] = casted
# check if there is an existing config file # check if there is an existing config file
if os.path.exists(settings_file): if os.path.exists(settings_file):

View File

@@ -1,13 +1,14 @@
import json import json
import tempfile import tempfile
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any, Optional
import pytest import pytest
from cli import Cli from cli import Cli
from clan_cli import config from clan_cli import config
from clan_cli.config import parsing from clan_cli.config import parsing
from clan_cli.errors import ClanError
example_options = f"{Path(config.__file__).parent}/jsonschema/options.json" example_options = f"{Path(config.__file__).parent}/jsonschema/options.json"
@@ -174,9 +175,28 @@ def test_type_from_schema_path_dynamic_attrs() -> None:
assert parsing.type_from_schema_path(schema, ["users", "foo"]) == str assert parsing.type_from_schema_path(schema, ["users", "foo"]) == str
def test_map_type() -> None:
with pytest.raises(ClanError):
config.map_type("foo")
assert config.map_type("string") == str
assert config.map_type("integer") == int
assert config.map_type("boolean") == bool
assert config.map_type("attribute set of string") == dict[str, str]
assert config.map_type("attribute set of integer") == dict[str, int]
assert config.map_type("null or string") == Optional[str]
# test the cast function with simple types # test the cast function with simple types
def test_cast_simple() -> None: def test_cast() -> None:
assert config.cast(["true"], bool, "foo-option") is True assert config.cast(value=["true"], type=bool, opt_description="foo-option") is True
assert (
config.cast(value=["null"], type=Optional[str], opt_description="foo-option")
is None
)
assert (
config.cast(value=["bar"], type=Optional[str], opt_description="foo-option")
== "bar"
)
@pytest.mark.parametrize( @pytest.mark.parametrize(