85742ab165
Deploy Documentation / deploy (push) Has been cancelled
CPU Test / Test (Utilities, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (LLM proxy, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Others, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Store, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Utilities, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Weave, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (AgentOps, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (LLM proxy, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Others, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Weave, latest, Python 3.13) (push) Has been cancelled
Dashboard / Chromatic (push) Has been cancelled
CPU Test / Lint - fast (push) Has been cancelled
CPU Test / Lint - next (push) Has been cancelled
CPU Test / Lint - slow (push) Has been cancelled
CPU Test / Lint - JavaScript (push) Has been cancelled
CPU Test / Build documentation (push) Has been cancelled
CPU Test / Test (AgentOps, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (LLM proxy, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Others, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Store, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Weave, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (AgentOps, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Store, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Utilities, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Weave, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (AgentOps, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (LLM proxy, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Others, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Store, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Utilities, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (JavaScript) (push) Has been cancelled
609 lines
23 KiB
Python
609 lines
23 KiB
Python
# Copyright (c) Microsoft. All rights reserved.
|
|
|
|
# type: ignore
|
|
|
|
"""
|
|
This file is not carefully reviewed.
|
|
It is to ensure the *somewhat* correctness of the code in agentlightning/config.py.
|
|
It can contain logically erroroneous expected values.
|
|
Please do not use the file as a reference for the expected behavior of config.
|
|
"""
|
|
|
|
import argparse
|
|
import inspect
|
|
import sys
|
|
from typing import TypeVar # Added for completeness if testing TypeVars directly
|
|
from typing import _GenericAlias # type: ignore
|
|
from typing import (
|
|
Any,
|
|
Callable,
|
|
Dict,
|
|
List,
|
|
Optional,
|
|
Tuple,
|
|
Type,
|
|
Union,
|
|
get_args,
|
|
get_origin,
|
|
)
|
|
from unittest import mock # For mock.patch.object, mock.call, MagicMock etc.
|
|
|
|
import pytest
|
|
|
|
from agentlightning import config
|
|
|
|
# TypeVar as used in the original code
|
|
CliConfigurable = Any
|
|
_C = TypeVar("_C", bound=CliConfigurable)
|
|
|
|
|
|
# --- Helper classes for testing ---
|
|
class SimpleConfig:
|
|
def __init__(self, name: str, value: int = 10):
|
|
self.name = name
|
|
self.value = value
|
|
|
|
|
|
class ComplexConfig:
|
|
def __init__(
|
|
self,
|
|
text: str,
|
|
is_active: bool,
|
|
count: Optional[int] = None,
|
|
items: List[str] = [], # Default mutable is usually discouraged, but for testing CLI it's okay.
|
|
maybe_text: Optional[str] = "default_text",
|
|
optional_list: Optional[List[int]] = None,
|
|
list_of_optionals: List[Optional[str]] = [],
|
|
any_param: Any = None,
|
|
untyped_param="untyped_default", # Parameter without type hint but with a default
|
|
):
|
|
self.text = text
|
|
self.is_active = is_active
|
|
self.count = count
|
|
self.items = items
|
|
self.maybe_text = maybe_text
|
|
self.optional_list = optional_list
|
|
self.list_of_optionals = list_of_optionals
|
|
self.any_param = any_param
|
|
self.untyped_param = untyped_param
|
|
|
|
|
|
class NoInitParamsConfig:
|
|
def __init__(self):
|
|
pass
|
|
|
|
|
|
class OnlySelfConfig: # For completeness, though __init__ without params is similar
|
|
def __init__(self):
|
|
pass
|
|
|
|
|
|
class RequiredOnlyConfig:
|
|
def __init__(self, req_str: str, req_int: int):
|
|
self.req_str = req_str
|
|
self.req_int = req_int
|
|
|
|
|
|
class OptionalNoDefaultConfig:
|
|
def __init__(self, opt_val: Optional[str]): # No __init__ default
|
|
self.opt_val = opt_val
|
|
|
|
|
|
# --- Tests for nullable_str ---
|
|
@pytest.mark.parametrize(
|
|
"input_val, expected_output",
|
|
[
|
|
("none", None),
|
|
("None", None),
|
|
("NONE", None),
|
|
("null", None),
|
|
("Null", None),
|
|
("NULL", None),
|
|
("~", None),
|
|
("nil", None),
|
|
("Nil", None),
|
|
("NIL", None),
|
|
("actual_string", "actual_string"),
|
|
("", ""),
|
|
(" ", " "),
|
|
("NoneValue", "NoneValue"), # Should not convert if not exact keyword
|
|
],
|
|
)
|
|
def test_nullable_str(input_val: str, expected_output: Optional[str]) -> None:
|
|
"""Tests the nullable_str function for various inputs."""
|
|
assert config.nullable_str(input_val) == expected_output
|
|
|
|
|
|
# --- Tests for _str_to_bool ---
|
|
@pytest.mark.parametrize(
|
|
"input_val, expected_output",
|
|
[
|
|
("yes", True),
|
|
("Yes", True),
|
|
("YES", True),
|
|
("true", True),
|
|
("True", True),
|
|
("TRUE", True),
|
|
("t", True),
|
|
("T", True),
|
|
("y", True),
|
|
("Y", True),
|
|
("1", True),
|
|
(True, True), # Direct bool pass-through
|
|
("no", False),
|
|
("No", False),
|
|
("NO", False),
|
|
("false", False),
|
|
("False", False),
|
|
("FALSE", False),
|
|
("f", False),
|
|
("F", False),
|
|
("n", False),
|
|
("N", False),
|
|
("0", False),
|
|
(False, False), # Direct bool pass-through
|
|
],
|
|
)
|
|
def test_str_to_bool_valid(input_val: Union[str, bool], expected_output: bool) -> None:
|
|
"""Tests _str_to_bool with valid boolean string representations."""
|
|
assert config._str_to_bool(input_val) == expected_output
|
|
|
|
|
|
@pytest.mark.parametrize("invalid_input", ["maybe", "2", "", " ", "trueish", "falsey"])
|
|
def test_str_to_bool_invalid(invalid_input: str) -> None:
|
|
"""Tests _str_to_bool with invalid inputs, expecting ArgumentTypeError."""
|
|
with pytest.raises(argparse.ArgumentTypeError):
|
|
config._str_to_bool(invalid_input)
|
|
|
|
|
|
# --- Tests for _get_param_type_details ---
|
|
@pytest.mark.parametrize(
|
|
"annotation, expected_core_type, expected_is_optional, expected_is_list",
|
|
[
|
|
(str, str, False, False),
|
|
(int, int, False, False),
|
|
(bool, bool, False, False),
|
|
(Any, Any, False, False),
|
|
(Optional[str], str, True, False),
|
|
(Union[str, None], str, True, False), # Equivalent to Optional[str]
|
|
(Optional[int], int, True, False),
|
|
(List[str], List[str], False, True), # core_type is List[str]
|
|
(List[int], List[int], False, True),
|
|
(Optional[List[str]], List[str], True, True), # core_type is List[str], outer is Optional
|
|
(Union[List[str], None], List[str], True, True),
|
|
(List[Optional[str]], List[Optional[str]], False, True), # core_type is List[Optional[str]]
|
|
(Optional[List[Optional[int]]], List[Optional[int]], True, True),
|
|
(inspect.Parameter.empty, inspect.Parameter.empty, False, False), # No annotation
|
|
(Union[int, str], Union[int, str], False, False), # Non-optional Union
|
|
(Tuple[str, int], Tuple[str, int], False, False), # Tuple is not a List
|
|
(Callable[[int], str], Callable[[int], str], False, False),
|
|
(Dict[str, int], Dict[str, int], False, False),
|
|
(List[Any], List[Any], False, True),
|
|
(Optional[List[Any]], List[Any], True, True),
|
|
],
|
|
)
|
|
def test_get_param_type_details(
|
|
annotation: Any, expected_core_type: Any, expected_is_optional: bool, expected_is_list: bool
|
|
) -> None:
|
|
"""Tests _get_param_type_details for various type annotations."""
|
|
core_type, is_optional, is_list = config._get_param_type_details(annotation)
|
|
|
|
# Handle comparison for complex types like Union by comparing origin and args
|
|
if get_origin(expected_core_type) is Union and get_origin(core_type) is Union:
|
|
assert get_origin(core_type) == get_origin(expected_core_type)
|
|
# Sort args for comparison as order doesn't matter in Union and get_args might not preserve input order
|
|
assert sorted(map(str, get_args(core_type))) == sorted(map(str, get_args(expected_core_type)))
|
|
else:
|
|
assert core_type == expected_core_type
|
|
assert is_optional == expected_is_optional
|
|
assert is_list == expected_is_list
|
|
|
|
|
|
# --- Tests for _determine_argparse_type_and_nargs ---
|
|
@pytest.mark.parametrize(
|
|
"core_param_type, is_param_list, expected_kwargs",
|
|
[
|
|
# List cases
|
|
(List[str], True, {"nargs": "*", "type": str}),
|
|
(List[int], True, {"nargs": "*", "type": int}),
|
|
(List[float], True, {"nargs": "*", "type": float}),
|
|
(List[bool], True, {"nargs": "*", "type": config._str_to_bool}),
|
|
(List[Optional[str]], True, {"nargs": "*", "type": config.nullable_str}),
|
|
(List[Any], True, {"nargs": "*", "type": str}), # Defaults to str for List[Any] items
|
|
# Non-list cases
|
|
(str, False, {"type": str}),
|
|
(int, False, {"type": int}),
|
|
(float, False, {"type": float}),
|
|
(bool, False, {"type": config._str_to_bool}),
|
|
(Optional[str], False, {"type": config.nullable_str}), # Handled by re-checking details
|
|
(Union[str, None], False, {"type": config.nullable_str}), # Equivalent to Optional[str]
|
|
(Any, False, {"type": str}), # No specific argparse type for Any
|
|
(inspect.Parameter.empty, False, {"type": str}), # No specific type for unannotated
|
|
# List of complex types (e.g. List[Dict]), type of item is treated as string
|
|
(List[Dict[str, int]], True, {"nargs": "*", "type": str}),
|
|
],
|
|
)
|
|
def test_determine_argparse_type_and_nargs(
|
|
core_param_type: Any, is_param_list: bool, expected_kwargs: Dict[str, Any]
|
|
) -> None:
|
|
"""Tests _determine_argparse_type_and_nargs for type and nargs mapping."""
|
|
assert config._determine_argparse_type_and_nargs(core_param_type, is_param_list) == expected_kwargs
|
|
|
|
|
|
# --- Tests for _build_help_string ---
|
|
@pytest.mark.parametrize(
|
|
"cls_name, param_name, core_type, is_optional, is_list, expected_help",
|
|
[
|
|
("C", "p", str, False, False, "For C: 'p'. Inferred type: str."),
|
|
("C", "p", int, True, False, "For C: 'p'. Inferred type: Optional[int]."),
|
|
("C", "p_list", List[str], False, True, "For C: 'p_list'. Inferred type: List[str]."),
|
|
("C", "p_opt_list", List[int], True, True, "For C: 'p_opt_list'. Inferred type: Optional[List[int]]."),
|
|
(
|
|
"C",
|
|
"p_list_opt",
|
|
List[Optional[str]],
|
|
False,
|
|
True,
|
|
"For C: 'p_list_opt'. Inferred type: List[Optional[str]].",
|
|
),
|
|
("C", "p_any", Any, False, False, "For C: 'p_any'. Inferred type: Any."),
|
|
("C", "p_opt_any", Any, True, False, "For C: 'p_opt_any'. Inferred type: Optional[Any]."),
|
|
("C", "p_list_any", List[Any], False, True, "For C: 'p_list_any'. Inferred type: List[Any]."),
|
|
("C", "p_empty", inspect.Parameter.empty, False, False, "For C: 'p_empty'. Inferred type: Any."),
|
|
],
|
|
)
|
|
def test_build_help_string(
|
|
cls_name: str, param_name: str, core_type: Any, is_optional: bool, is_list: bool, expected_help: str
|
|
) -> None:
|
|
"""Tests _build_help_string for generating correct help messages."""
|
|
assert config._build_help_string(cls_name, param_name, core_type, is_optional, is_list) == expected_help
|
|
|
|
|
|
# --- Tests for _add_argument_for_parameter ---
|
|
@pytest.fixture
|
|
def mock_parser():
|
|
"""Fixture to create a mock ArgumentParser."""
|
|
parser = mock.MagicMock(spec=argparse.ArgumentParser)
|
|
parser.add_argument = mock.MagicMock()
|
|
return parser
|
|
|
|
|
|
def get_param_obj(cls, param_name):
|
|
"""Helper to get an inspect.Parameter object from a class's __init__."""
|
|
sig = inspect.signature(cls.__init__)
|
|
return sig.parameters[param_name]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"param_name, cls, expected_cli_name_part, expected_argparse_kwargs_subset",
|
|
[
|
|
("name", SimpleConfig, "simpleconfig.name", {"type": str, "required": True}),
|
|
("value", SimpleConfig, "simpleconfig.value", {"type": int, "default": 10}),
|
|
("text", ComplexConfig, "complexconfig.text", {"type": str, "required": True}),
|
|
("is_active", ComplexConfig, "complexconfig.is-active", {"type": config._str_to_bool, "required": True}),
|
|
(
|
|
"count",
|
|
ComplexConfig,
|
|
"complexconfig.count",
|
|
{"type": config.nullable_int, "default": None},
|
|
), # Optional[int]=None
|
|
("items", ComplexConfig, "complexconfig.items", {"type": str, "nargs": "*", "default": []}),
|
|
(
|
|
"maybe_text",
|
|
ComplexConfig,
|
|
"complexconfig.maybe-text",
|
|
{"type": config.nullable_str, "default": "default_text"},
|
|
),
|
|
("optional_list", ComplexConfig, "complexconfig.optional-list", {"type": int, "nargs": "*", "default": None}),
|
|
(
|
|
"list_of_optionals",
|
|
ComplexConfig,
|
|
"complexconfig.list-of-optionals",
|
|
{"type": config.nullable_str, "nargs": "*", "default": []},
|
|
),
|
|
(
|
|
"any_param",
|
|
ComplexConfig,
|
|
"complexconfig.any-param",
|
|
{"default": None},
|
|
), # Type Any implies no specific argparse type
|
|
(
|
|
"untyped_param",
|
|
ComplexConfig,
|
|
"complexconfig.untyped-param",
|
|
{"type": str, "default": "untyped_default"},
|
|
), # No type hint
|
|
(
|
|
"opt_val",
|
|
OptionalNoDefaultConfig,
|
|
"optionalnodefaultconfig.opt-val",
|
|
{"type": config.nullable_str, "default": None},
|
|
),
|
|
],
|
|
)
|
|
def test_add_argument_for_parameter(
|
|
mock_parser, param_name, cls, expected_cli_name_part, expected_argparse_kwargs_subset
|
|
):
|
|
"""Tests _add_argument_for_parameter for correct argument configuration."""
|
|
param_obj = get_param_obj(cls, param_name)
|
|
dest_name = f"{cls.__name__.lower()}_{param_name}"
|
|
config._add_argument_for_parameter(mock_parser, cls, param_name, param_obj, dest_name)
|
|
|
|
# Build expected CLI argument name (e.g., --simpleconfig.name)
|
|
expected_cli_arg_name = f"--{expected_cli_name_part.replace('_', '-')}"
|
|
|
|
# Check that add_argument was called
|
|
assert mock_parser.add_argument.called
|
|
|
|
# Get the actual call arguments
|
|
actual_call_args, actual_call_kwargs = mock_parser.add_argument.call_args
|
|
assert actual_call_args[0] == expected_cli_arg_name
|
|
assert actual_call_kwargs["dest"] == dest_name
|
|
|
|
# Check for the presence and correctness of specified kwargs
|
|
for key, expected_value in expected_argparse_kwargs_subset.items():
|
|
assert key in actual_call_kwargs, f"Key '{key}' not in argparse kwargs for {param_name}"
|
|
assert actual_call_kwargs[key] == expected_value, f"Value for key '{key}' incorrect for {param_name}"
|
|
|
|
# Check 'required' status carefully
|
|
if expected_argparse_kwargs_subset.get("required", False):
|
|
assert actual_call_kwargs.get("required") is True
|
|
else: # Not required or default makes it not required
|
|
assert not actual_call_kwargs.get("required", False) # Either 'required' is False or not present
|
|
|
|
# Check help string was generated
|
|
assert "help" in actual_call_kwargs
|
|
assert isinstance(actual_call_kwargs["help"], str)
|
|
|
|
|
|
# --- Tests for _add_arguments_for_class ---
|
|
def test_add_arguments_for_class(mock_parser: Any) -> None:
|
|
"""Tests _add_arguments_for_class by checking calls to _add_argument_for_parameter."""
|
|
class_arg_configs_maps = {}
|
|
with mock.patch("agentlightning.config._add_argument_for_parameter") as mock_add_param_func:
|
|
config._add_arguments_for_class(mock_parser, ComplexConfig, class_arg_configs_maps)
|
|
|
|
assert ComplexConfig in class_arg_configs_maps
|
|
init_params = inspect.signature(ComplexConfig.__init__).parameters
|
|
expected_calls = 0
|
|
for p_name, p_obj in init_params.items():
|
|
if p_name == "self":
|
|
continue
|
|
expected_calls += 1
|
|
dest = f"complexconfig_{p_name}"
|
|
assert class_arg_configs_maps[ComplexConfig][p_name] == dest
|
|
# Check if mock_add_param_func was called with these specific arguments
|
|
found_call = any(
|
|
call_args[0] == mock_parser
|
|
and call_args[1] == ComplexConfig
|
|
and call_args[2] == p_name
|
|
and call_args[3] == p_obj
|
|
and call_args[4] == dest
|
|
for call_args, _ in mock_add_param_func.call_args_list
|
|
)
|
|
assert found_call, f"Expected call for {p_name} not found or with wrong arguments."
|
|
assert mock_add_param_func.call_count == expected_calls
|
|
|
|
|
|
def test_add_arguments_for_class_no_init_params(mock_parser: Any) -> None:
|
|
"""Tests _add_arguments_for_class with a class having no __init__ parameters."""
|
|
class_arg_configs_maps = {}
|
|
with mock.patch("agentlightning.config._add_argument_for_parameter") as mock_add_param_func:
|
|
config._add_arguments_for_class(mock_parser, NoInitParamsConfig, class_arg_configs_maps)
|
|
mock_add_param_func.assert_not_called()
|
|
assert class_arg_configs_maps[NoInitParamsConfig] == {}
|
|
|
|
|
|
def test_create_argument_parser() -> None:
|
|
"""Tests _create_argument_parser for basic parser properties."""
|
|
parser = config._create_argument_parser()
|
|
assert isinstance(parser, argparse.ArgumentParser)
|
|
assert parser.description == "CLI configurator for application components."
|
|
# The formatter_class is a type, so compare types
|
|
assert type(parser.formatter_class) == type(argparse.ArgumentDefaultsHelpFormatter)
|
|
|
|
|
|
def test_instantiate_classes() -> None:
|
|
"""Tests _instantiate_classes with various argument types and defaults."""
|
|
parsed_args = argparse.Namespace(
|
|
simpleconfig_name="TestName",
|
|
simpleconfig_value=99,
|
|
complexconfig_text="Hello",
|
|
complexconfig_is_active=True,
|
|
complexconfig_count=None, # Explicitly None
|
|
complexconfig_items=["item1", "item2"],
|
|
# maybe_text not provided, should use its default "default_text" from __init__
|
|
# Argparse default for complexconfig_maybe_text would be "default_text"
|
|
complexconfig_maybe_text="default_text", # Simulate argparse providing the default
|
|
complexconfig_optional_list=[1, 2, 3],
|
|
complexconfig_list_of_optionals=["a", None, "b"],
|
|
complexconfig_any_param=object(),
|
|
complexconfig_untyped_param="custom_untyped",
|
|
)
|
|
# This simulates that 'complexconfig_maybe_text' was given a default by add_argument
|
|
# and thus present in parsed_args.
|
|
# If not provided on CLI and having __init__ default, argparse gives it default.
|
|
|
|
class_arg_configs_maps = {
|
|
SimpleConfig: {"name": "simpleconfig_name", "value": "simpleconfig_value"},
|
|
ComplexConfig: {
|
|
p: f"complexconfig_{p}" for p in inspect.signature(ComplexConfig.__init__).parameters if p != "self"
|
|
},
|
|
}
|
|
|
|
instances = config._instantiate_classes(parsed_args, (SimpleConfig, ComplexConfig), class_arg_configs_maps)
|
|
|
|
assert len(instances) == 2
|
|
sc, cc = instances
|
|
|
|
assert isinstance(sc, SimpleConfig)
|
|
assert sc.name == "TestName"
|
|
assert sc.value == 99
|
|
|
|
assert isinstance(cc, ComplexConfig)
|
|
assert cc.text == "Hello"
|
|
assert cc.is_active is True
|
|
assert cc.count is None
|
|
assert cc.items == ["item1", "item2"]
|
|
assert cc.maybe_text == "default_text" # From __init__ via argparse default
|
|
assert cc.optional_list == [1, 2, 3]
|
|
assert cc.list_of_optionals == ["a", None, "b"]
|
|
assert cc.any_param is parsed_args.complexconfig_any_param
|
|
assert cc.untyped_param == "custom_untyped"
|
|
|
|
|
|
def test_instantiate_classes_error_handling(caplog):
|
|
"""Tests error logging during class instantiation failure."""
|
|
|
|
class FailingConfig:
|
|
def __init__(self, param):
|
|
raise ValueError("Instantiaion failed")
|
|
|
|
parsed_args = argparse.Namespace(failingconfig_param="value")
|
|
class_arg_configs_maps = {FailingConfig: {"param": "failingconfig_param"}}
|
|
|
|
with pytest.raises(ValueError, match="Instantiaion failed"):
|
|
config._instantiate_classes(parsed_args, (FailingConfig,), class_arg_configs_maps)
|
|
|
|
assert "Error instantiating FailingConfig" in caplog.text
|
|
assert "{'param': 'value'}" in caplog.text # constructor_args
|
|
assert "Error: Instantiaion failed" in caplog.text
|
|
|
|
|
|
# --- Integration Tests for lightning_cli ---
|
|
def run_lightning_cli(classes_to_configure, cli_args_list):
|
|
"""Helper to run lightning_cli with mocked sys.argv."""
|
|
# Prepend a dummy program name to cli_args_list for sys.argv
|
|
with mock.patch.object(sys, "argv", ["test_program.py"] + cli_args_list):
|
|
result = config.lightning_cli(*classes_to_configure)
|
|
if not isinstance(result, tuple):
|
|
return (result,)
|
|
return result
|
|
|
|
|
|
def test_lightning_cli_no_classes():
|
|
"""Tests lightning_cli with no classes provided."""
|
|
assert run_lightning_cli([], []) == tuple()
|
|
|
|
|
|
def test_lightning_cli_simple_config():
|
|
"""Tests lightning_cli with a simple class and various argument scenarios."""
|
|
# Only required arg
|
|
(sc1,) = run_lightning_cli([SimpleConfig], ["--simpleconfig.name", "MyName"])
|
|
assert sc1.name == "MyName"
|
|
assert sc1.value == 10 # Default from __init__
|
|
|
|
# All args
|
|
(sc2,) = run_lightning_cli([SimpleConfig], ["--simpleconfig.name", "Another", "--simpleconfig.value", "77"])
|
|
assert sc2.name == "Another"
|
|
assert sc2.value == 77
|
|
|
|
|
|
def test_lightning_cli_complex_config_types():
|
|
"""Tests lightning_cli with ComplexConfig, checking various type conversions."""
|
|
cli_args = [
|
|
"--complexconfig.text",
|
|
"CLI Text",
|
|
"--complexconfig.is-active",
|
|
"yes",
|
|
"--complexconfig.count",
|
|
"123",
|
|
"--complexconfig.items",
|
|
"apple",
|
|
"banana",
|
|
"--complexconfig.maybe-text",
|
|
"None", # Test nullable_str
|
|
"--complexconfig.optional-list",
|
|
"10",
|
|
"20",
|
|
"30",
|
|
"--complexconfig.list-of-optionals",
|
|
"first",
|
|
"nil",
|
|
"third",
|
|
"null",
|
|
"--complexconfig.any-param",
|
|
"AnythingGoes",
|
|
"--complexconfig.untyped-param",
|
|
"FromCLI",
|
|
]
|
|
(cc,) = run_lightning_cli([ComplexConfig], cli_args)
|
|
|
|
assert cc.text == "CLI Text"
|
|
assert cc.is_active is True
|
|
assert cc.count == 123
|
|
assert cc.items == ["apple", "banana"]
|
|
assert cc.maybe_text is None
|
|
assert cc.optional_list == [10, 20, 30]
|
|
assert cc.list_of_optionals == ["first", None, "third", None]
|
|
assert cc.any_param == "AnythingGoes"
|
|
assert cc.untyped_param == "FromCLI"
|
|
|
|
|
|
def test_lightning_cli_complex_config_defaults():
|
|
"""Tests that __init__ defaults are used if CLI args are not provided."""
|
|
# Only provide required arguments for ComplexConfig
|
|
cli_args = ["--complexconfig.text", "Minimal", "--complexconfig.is-active", "f"]
|
|
(cc,) = run_lightning_cli([ComplexConfig], cli_args)
|
|
|
|
assert cc.text == "Minimal"
|
|
assert cc.is_active is False
|
|
assert cc.count is None # Default from __init__
|
|
assert cc.items == [] # Default from __init__
|
|
assert cc.maybe_text == "default_text" # Default from __init__
|
|
assert cc.optional_list is None # Default from __init__
|
|
assert cc.list_of_optionals == [] # Default from __init__
|
|
assert cc.any_param is None # Default from __init__
|
|
assert cc.untyped_param == "untyped_default" # Default from __init__
|
|
|
|
|
|
def test_lightning_cli_multiple_classes():
|
|
"""Tests configuring multiple classes simultaneously."""
|
|
cli_args = [
|
|
"--simpleconfig.name",
|
|
"SC_Multi",
|
|
"--simpleconfig.value",
|
|
"5",
|
|
"--complexconfig.text",
|
|
"CC_Multi",
|
|
"--complexconfig.is-active",
|
|
"true",
|
|
"--complexconfig.maybe-text",
|
|
"NotNone",
|
|
]
|
|
sc, cc = run_lightning_cli([SimpleConfig, ComplexConfig], cli_args)
|
|
|
|
assert isinstance(sc, SimpleConfig)
|
|
assert sc.name == "SC_Multi"
|
|
assert sc.value == 5
|
|
|
|
assert isinstance(cc, ComplexConfig)
|
|
assert cc.text == "CC_Multi"
|
|
assert cc.is_active is True
|
|
assert cc.maybe_text == "NotNone"
|
|
assert cc.items == [] # Default
|
|
|
|
|
|
def test_lightning_cli_missing_required_arg_exits(capsys):
|
|
"""Tests that argparse exits if a required argument is missing."""
|
|
with pytest.raises(SystemExit) as e:
|
|
run_lightning_cli([RequiredOnlyConfig], ["--requiredonlyconfig.req-int", "123"])
|
|
assert e.value.code != 0 # Argparse exits with non-zero for error
|
|
captured = capsys.readouterr() # Capture stderr
|
|
assert "the following arguments are required: --requiredonlyconfig.req-str" in captured.err
|
|
|
|
|
|
def test_lightning_cli_optional_no_default_behavior():
|
|
"""Tests Optional parameter without __init__ default."""
|
|
# Not provided: should become None
|
|
(cfg1,) = run_lightning_cli([OptionalNoDefaultConfig], [])
|
|
assert cfg1.opt_val is None
|
|
|
|
# Provided as "None" via nullable_str
|
|
(cfg2,) = run_lightning_cli([OptionalNoDefaultConfig], ["--optionalnodefaultconfig.opt-val", "None"])
|
|
assert cfg2.opt_val is None
|
|
|
|
# Provided with a value
|
|
(cfg3,) = run_lightning_cli([OptionalNoDefaultConfig], ["--optionalnodefaultconfig.opt-val", "ActualValue"])
|
|
assert cfg3.opt_val == "ActualValue"
|