Merge pull request 'clan config: support new types nullOr and passwdEntry' (#338) from DavHau-dave into main
All checks were successful
checks-impure / test (push) Successful in 8s
checks / test (push) Successful in 20s
assets1 / test (push) Successful in 6s

This commit is contained in:
clan-bot 2023-09-24 13:30:35 +00:00
commit 01441b1f5a
2 changed files with 49 additions and 12 deletions

View File

@ -7,7 +7,7 @@ import shlex
import subprocess
import sys
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.errors import ClanError
@ -19,7 +19,7 @@ script_dir = Path(__file__).parent
# nixos option type description to python type
def map_type(type: str) -> Type:
def map_type(type: str) -> Any:
if type == "boolean":
return bool
elif type in [
@ -30,6 +30,12 @@ def map_type(type: str) -> Type:
return int
elif type == "string":
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"):
subtype = type.removeprefix("attribute set of ")
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
# 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:
# handle bools
if isinstance(type(), bool):
if isinstance(type, bool):
if value[0] in ["true", "True", "yes", "y", "1"]:
return True
elif value[0] in ["false", "False", "no", "n", "0"]:
@ -76,17 +82,21 @@ def cast(value: Any, type: Type, opt_description: str) -> Any:
else:
raise ClanError(f"Invalid value {value} for boolean")
# handle lists
elif isinstance(type(), list):
elif get_origin(type) == list:
subtype = type.__args__[0]
return [cast([x], subtype, opt_description) for x in value]
# handle dicts
elif isinstance(type(), dict):
elif get_origin(type) == dict:
if not isinstance(value, dict):
raise ClanError(
f"Cannot set {opt_description} directly. Specify a suboption like {opt_description}.<name>"
)
subtype = type.__args__[1]
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:
if len(value) > 1:
raise ClanError(f"Too many values for {opt_description}")
@ -241,6 +251,11 @@ def set_option(
option_description: str = "",
show_trace: bool = False,
) -> None:
option_path_orig = option.split(".")
# returns for example:
# option: "users.users.<name>.name"
# value: "my-name"
option, value = find_option(
option=option,
value=value,
@ -249,18 +264,20 @@ def set_option(
)
option_path = option.split(".")
option_path_store = option_path_orig[: len(option_path)]
target_type = map_type(options[option]["type"])
casted = cast(value, target_type, option)
# construct a nested dict from the option path and set the value
result: dict[str, Any] = {}
current = result
for part in option_path[:-1]:
for part in option_path_store[:-1]:
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
if os.path.exists(settings_file):

View File

@ -1,13 +1,14 @@
import json
import tempfile
from pathlib import Path
from typing import Any
from typing import Any, Optional
import pytest
from cli import Cli
from clan_cli import config
from clan_cli.config import parsing
from clan_cli.errors import ClanError
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
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
def test_cast_simple() -> None:
assert config.cast(["true"], bool, "foo-option") is True
def test_cast() -> None:
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(