Merge pull request 'clan config: support new types nullOr and passwdEntry' (#338) from DavHau-dave into main
This commit is contained in:
@@ -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):
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user