| # Copyright 2023 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| from __future__ import annotations |
| |
| import argparse |
| import dataclasses |
| import enum |
| import json |
| import pathlib |
| import unittest |
| from typing import Any, Optional, Self |
| from unittest import mock |
| |
| from immutabledict import immutabledict |
| from typing_extensions import override |
| |
| from crossbench.config import (ConfigEnum, ConfigObject, ConfigParser, |
| UnusedPropertiesMode) |
| from crossbench.exception import MultiException |
| from crossbench.parse import NumberParser, ObjectParser |
| from crossbench.str_enum_with_help import StrEnumWithHelp |
| from tests import test_helper |
| from tests.crossbench.base import CrossbenchFakeFsTestCase |
| |
| |
| @enum.unique |
| class GenericEnum(StrEnumWithHelp): |
| A = ("a", "A Help") |
| B = ("b", "B Help") |
| C = ("c", "C Help") |
| |
| |
| @enum.unique |
| class CustomConfigEnum(ConfigEnum): |
| A = ("a", "A Help") |
| B = ("b", "B Help") |
| C = ("c", "C Help") |
| |
| class CustomValueEnum(enum.Enum): |
| |
| @classmethod |
| def _missing_(cls, value: Any) -> Optional[CustomValueEnum]: |
| if value is True: |
| return CustomValueEnum.A_OR_TRUE |
| if value is False: |
| return CustomValueEnum.B_OR_FALSE |
| return super()._missing_(value) |
| |
| DEFAULT = "default" |
| A_OR_TRUE = "a" |
| B_OR_FALSE = "b" |
| |
| |
| @dataclasses.dataclass(frozen=True) |
| class CustomBoolConfigObject(ConfigObject): |
| boolean: bool |
| |
| @classmethod |
| @override |
| def parse_str(cls, value: str) -> Self: |
| raise ValueError("Only bool values are supported") |
| |
| @classmethod |
| @override |
| def parse_other(cls, value: Any) -> Self: |
| if not isinstance(value, bool): |
| raise ValueError("Only bool values are supported") |
| return cls(boolean=value) |
| |
| @classmethod |
| @override |
| def config_parser(cls) -> ConfigParser[Self]: |
| parser = ConfigParser(cls) |
| parser.add_argument("boolean", type=ObjectParser.bool, required=True) |
| return parser |
| |
| |
| @dataclasses.dataclass(frozen=True) |
| class CustomNestedConfigObject(ConfigObject): |
| name: str |
| option: str | None = None |
| array: list[str] | None = None |
| |
| @classmethod |
| @override |
| def parse_str(cls, value: str) -> Self: |
| if ":" in value: |
| raise ValueError("Invalid Config") |
| if not value: |
| raise ValueError("Got empty input") |
| return cls(name=value) |
| |
| @classmethod |
| @override |
| def config_parser(cls) -> ConfigParser[Self]: |
| parser = ConfigParser(cls) |
| parser.add_argument("name", type=str, required=True) |
| parser.add_argument("option", type=str, required=False) |
| parser.add_argument("array", type=list) |
| return parser |
| |
| |
| @dataclasses.dataclass(frozen=True) |
| class CustomConfigObject(ConfigObject): |
| |
| name: str |
| array: list[str] | None = None |
| integer: int | None = None |
| float_field: float | None = None |
| nested: CustomNestedConfigObject | None = None |
| choices: str = "" |
| generic_enum: GenericEnum = GenericEnum.A |
| config_enum: CustomConfigEnum = CustomConfigEnum.A |
| custom_value_enum: CustomValueEnum = CustomValueEnum.DEFAULT |
| depending_nested: Optional[dict[str, Any]] = None |
| depending_many: Optional[dict[str, Any]] = None |
| |
| @classmethod |
| def default(cls) -> CustomConfigObject: |
| return cls("default") |
| |
| @classmethod |
| @override |
| def parse_str(cls, value: str) -> CustomConfigObject: |
| if ":" in value: |
| raise ValueError("Invalid Config") |
| if not value: |
| raise ValueError("Got empty input") |
| return cls(name=value) |
| |
| @classmethod |
| @override |
| def parse_path_like(cls, original_value: str, path: pathlib.Path, |
| **kwargs) -> Self: |
| return super().parse_path(path, **kwargs) |
| |
| @classmethod |
| def parse_depending_nested( |
| cls, value: Optional[str], |
| nested: CustomNestedConfigObject) -> Optional[dict]: |
| if not value: |
| return None |
| return { |
| "value": ObjectParser.non_empty_str(value), |
| "nested": ObjectParser.not_none(nested, "nested") |
| } |
| |
| @classmethod |
| def parse_depending_many(cls, value: Optional[str], array: list[Any], |
| integer: int, |
| nested: CustomNestedConfigObject) -> Optional[dict]: |
| if not value: |
| return None |
| return { |
| "value": ObjectParser.non_empty_str(value), |
| "nested": ObjectParser.not_none(nested, "nested"), |
| "array": ObjectParser.not_none(array, "array"), |
| "integer": NumberParser.positive_int(integer, "integer"), |
| } |
| |
| @classmethod |
| @override |
| def config_parser(cls) -> ConfigParser[CustomConfigObject]: |
| parser = cls.base_config_parser() |
| parser.add_argument( |
| "name", aliases=("name_alias", "name_alias2"), type=str, required=True) |
| parser.add_argument("array", type=list) |
| parser.add_argument("integer", type=NumberParser.positive_int) |
| parser.add_argument("float_field", type=NumberParser.any_float) |
| parser.add_argument("nested", type=CustomNestedConfigObject) |
| parser.add_argument("generic_enum", type=GenericEnum) |
| parser.add_argument("config_enum", type=CustomConfigEnum) |
| parser.add_argument( |
| "custom_value_enum", |
| type=CustomValueEnum, |
| default=CustomValueEnum.DEFAULT) |
| parser.add_argument("choices", type=str, choices=("x", "y", "z")) |
| parser.add_argument( |
| "depending_nested", |
| type=CustomConfigObject.parse_depending_nested, |
| depends_on=("nested",)) |
| parser.add_argument( |
| "depending_many", |
| type=CustomConfigObject.parse_depending_many, |
| depends_on=("array", "integer", "nested")) |
| return parser |
| |
| @classmethod |
| def base_config_parser(cls) -> ConfigParser[CustomConfigObject]: |
| return ConfigParser(cls) |
| |
| |
| class CustomConfigObjectStrict(CustomConfigObject): |
| |
| @classmethod |
| def base_config_parser(cls) -> ConfigParser[CustomConfigObjectStrict]: |
| return ConfigParser(cls, unused_properties_mode=UnusedPropertiesMode.ERROR) |
| |
| |
| class CustomConfigObjectWithDefault(CustomConfigObject): |
| |
| @classmethod |
| def base_config_parser(cls) -> ConfigParser[CustomConfigObjectWithDefault]: |
| return ConfigParser(cls, default=cls.default()) |
| |
| |
| class CustomConfigObjectToArgumentValue(CustomConfigObject): |
| |
| def to_argument_value(self): |
| return (self.name, self.array, self.integer) |
| |
| |
| class ConfigParserTestCase(unittest.TestCase): |
| |
| @override |
| def setUp(self): |
| super().setUp() |
| self.parser = ConfigParser(CustomConfigObject) |
| |
| def test_invalid_type(self): |
| with self.assertRaises(TypeError): |
| self.parser.add_argument("foo", type="something") # pytype: disable=wrong-arg-types |
| |
| def test_invalid_alias(self): |
| with self.assertRaises(ValueError): |
| self.parser.add_argument("foo", aliases=("foo",), type=str) |
| with self.assertRaises(ValueError): |
| self.parser.add_argument( |
| "foo", aliases=("foo_alias", "foo_alias"), type=str) |
| |
| def test_duplicate(self): |
| self.parser.add_argument("foo", type=str) |
| with self.assertRaises(ValueError): |
| self.parser.add_argument("foo", type=str) |
| with self.assertRaises(ValueError): |
| self.parser.add_argument("foo2", aliases=("foo",), type=str) |
| |
| def test_invalid_string_depends_on(self): |
| with self.assertRaises(TypeError): |
| self.parser.add_argument( |
| "custom", |
| type=CustomConfigObject.parse_depending_nested, |
| depends_on="other") # pytype: disable=wrong-arg-types |
| |
| def test_invalid_depends_on_nof_arguments(self): |
| with self.assertRaises(TypeError) as cm: |
| self.parser.add_argument("any", type=lambda x: x, depends_on=("other",)) |
| self.assertIn("arguments", str(cm.exception)) |
| |
| def test_invalid_depends_on(self): |
| with self.assertRaises(ValueError): |
| self.parser.add_argument("any", type=None, depends_on=("other",)) |
| |
| with self.assertRaises((ValueError, TypeError)): |
| # Raises ValueError on Python 3.11 because depends_on is not allowed. |
| # Raises TypeError on Python 3.12 because GenericEnum can't be called |
| # with multiple parameters. |
| self.parser.add_argument("enum", type=GenericEnum, depends_on=("other",)) |
| with self.assertRaises(ValueError): |
| self.parser.add_argument("enum", type=ConfigEnum, depends_on=("other",)) |
| |
| for primitive_type in (bool, float, int, str): |
| with self.assertRaises(TypeError): |
| self.parser.add_argument( |
| "param", type=primitive_type, depends_on=("other",)) |
| |
| def test_recursive_depends_on(self): |
| self.parser.add_argument( |
| "x", type=lambda value, y: value + y, depends_on=("y",)) |
| self.parser.add_argument( |
| "y", type=lambda value, x: value + x, depends_on=("x",)) |
| with self.assertRaises(argparse.ArgumentTypeError) as cm: |
| self.parser.parse({"x": 1, "y": 100}) |
| self.assertIn("Recursive", str(cm.exception)) |
| |
| def test_invalid_default_arg(self): |
| with self.assertRaisesRegex(ValueError, "default"): |
| self.parser.add_argument("name_1", type=str, default=None, required=False) |
| with self.assertRaisesRegex(ValueError, "default"): |
| self.parser.add_argument("name_1", type=str, default=None, required=True) |
| with self.assertRaisesRegex(ValueError, "default"): |
| self.parser.add_argument("name_2", type=str, default="", required=True) |
| with self.assertRaisesRegex(ValueError, "default"): |
| self.parser.add_argument("name_3", type=str, default=123, required=False) |
| |
| def test_default_str_arg(self): |
| self.parser.add_argument("name_1", type=str, default="", required=False) |
| |
| def test_default(self): |
| self.parser.add_argument("name", type=str, required=True) |
| with self.assertRaises(argparse.ArgumentTypeError) as cm: |
| self.parser.parse({}) |
| self.assertIn("no value", str(cm.exception).lower()) |
| parser = ConfigParser( |
| CustomConfigObject, default=CustomConfigObject.default()) |
| config = parser.parse({}) |
| self.assertEqual(config, CustomConfigObject.default()) |
| |
| def test_empty_title(self): |
| with self.assertRaisesRegex(ValueError, "title"): |
| ConfigParser(CustomConfigObject, title="") |
| |
| def test_empty_key(self): |
| with self.assertRaisesRegex(ValueError, "key"): |
| ConfigParser(CustomConfigObject, key="") |
| |
| def test_title(self): |
| parser = ConfigParser(CustomConfigObject, title=None) |
| self.assertEqual(parser.title, "CustomConfigObject parser") |
| parser = ConfigParser(CustomConfigObject) |
| self.assertEqual(parser.title, "CustomConfigObject parser") |
| parser = ConfigParser(CustomConfigObject, title="ParsyMcParser") |
| self.assertEqual(parser.title, "ParsyMcParser") |
| |
| def test_key(self): |
| parser = ConfigParser(CustomConfigObject, None) |
| self.assertEqual(parser.key, "CustomConfigObject") |
| parser = ConfigParser(CustomConfigObject) |
| self.assertEqual(parser.key, "CustomConfigObject") |
| parser = ConfigParser(CustomConfigObject, "ParsyMcParser") |
| self.assertEqual(parser.key, "ParsyMcParser") |
| parser = ConfigParser(CustomConfigObject, "ParsyMcParser", |
| "Parsy parser for McParser") |
| self.assertEqual(parser.key, "ParsyMcParser") |
| self.assertEqual(parser.title, "Parsy parser for McParser") |
| |
| def test_invalid_default(self): |
| with self.assertRaises(TypeError) as cm: |
| ConfigParser( # pytype: disable=wrong-arg-types |
| CustomConfigObject, |
| default="something else") |
| self.assertIn("instance", str(cm.exception)) |
| |
| def test_config_object_to_argument_value(self): |
| result = CustomConfigObjectToArgumentValue.config_parser().parse( |
| {"name": "custom-name"}) |
| self.assertIsInstance(result, CustomConfigObjectToArgumentValue) |
| parser = ConfigParser(dict) |
| parser.add_argument("data", type=CustomConfigObjectToArgumentValue) |
| |
| result = parser.parse({}) |
| self.assertDictEqual(result, {"data": None}) |
| result = parser.parse({"data": {"name": "a name"}}) |
| self.assertDictEqual(result, {"data": ("a name", None, None)}) |
| result = parser.parse( |
| {"data": { |
| "name": "a name", |
| "integer": 1, |
| "array": [1, 2] |
| }}) |
| self.assertDictEqual(result, {"data": ("a name", [1, 2], 1)}) |
| |
| def test_has_all_required_args(self): |
| config_parser = CustomConfigObjectToArgumentValue.config_parser() |
| self.assertTrue(config_parser.has_all_required_args({"name": "a name"})) |
| self.assertTrue( |
| config_parser.has_all_required_args({"name_alias": "a name"})) |
| self.assertFalse(config_parser.has_all_required_args({"integer": 1})) |
| |
| def test_has_any_args(self): |
| config_parser = CustomConfigObjectToArgumentValue.config_parser() |
| self.assertTrue(config_parser.has_any_args({"name": "a name"})) |
| self.assertTrue( |
| config_parser.has_any_args({"name_alias": "a name"})) |
| self.assertTrue(config_parser.has_any_args({"integer": 1})) |
| self.assertFalse(config_parser.has_any_args({"invalid": 1})) |
| |
| def test_parse_bool_false(self): |
| config = CustomBoolConfigObject.parse(False) |
| assert isinstance(config, CustomBoolConfigObject) |
| self.assertFalse(config.boolean) |
| |
| |
| class ConfigObjectTestCase(CrossbenchFakeFsTestCase): |
| |
| def test_help(self): |
| help_text = CustomConfigObject.config_parser().help |
| self.assertIn("name", help_text) |
| self.assertIn("array", help_text) |
| self.assertIn("integer", help_text) |
| self.assertIn("nested", help_text) |
| self.assertIn("generic_enum", help_text) |
| self.assertIn("config_enum", help_text) |
| self.assertIn("custom_value_enum", help_text) |
| self.assertIn("choices", help_text) |
| self.assertIn("depending_nested", help_text) |
| self.assertIn("depending_many", help_text) |
| |
| def test_has_path_prefix(self): |
| for value in ("/foo/bar", "~/foo/bar", "../foo/bar", "..\\foo\\bar", |
| "./foo/bar", "C:\\foo\\bar", "C:/foo/bar"): |
| with self.subTest(value=value): |
| self.assertTrue(CustomConfigObject.has_path_prefix(value)) |
| self.assertTrue(CustomConfigObject.is_path_like(value)) |
| for value in ("foo/bar", "foo:bar", "foo", "{foo:'/foo/bar'}", "http://foo", |
| "c://", "c://bar", "C:../bar", "..//foo", "..//foo/bar", |
| "~:bar", "~.bar", "~//df", "foo/~bar", "foo~bar/foo", |
| "http://someurl.com/~myproject/index.html"): |
| with self.subTest(value=value): |
| self.assertFalse(CustomConfigObject.has_path_prefix(value)) |
| |
| def test_is_path_like(self): |
| for value in ("foo/bar", "foo\\bar"): |
| with self.subTest(value=value): |
| self.assertFalse(CustomConfigObject.has_path_prefix(value)) |
| self.assertTrue(CustomConfigObject.is_path_like(value)) |
| for value in ("adb:foo/bar", "adb:foo\\bar:local", "http://foo/bar"): |
| with self.subTest(value=value): |
| self.assertFalse(CustomConfigObject.has_path_prefix(value)) |
| self.assertFalse(CustomConfigObject.is_path_like(value)) |
| |
| def test_is_hjson_like(self): |
| for value in ("{}", " {}", " { foo: 2} "): |
| with self.subTest(value=value): |
| self.assertTrue(CustomConfigObject.is_hjson_like(value)) |
| for value in ("{", "2", " bar/foo{}asdf"): |
| with self.subTest(value=value): |
| self.assertFalse(CustomConfigObject.is_hjson_like(value)) |
| |
| def test_parse_invalid_str(self): |
| invalid: Any |
| for invalid in ("", None, 1, []): |
| with self.assertRaises(argparse.ArgumentTypeError): |
| CustomConfigObject.parse(invalid) |
| |
| def test_parse_dict_invalid(self): |
| with self.assertRaises(argparse.ArgumentTypeError): |
| CustomConfigObject.parse({}) |
| with self.assertRaises(argparse.ArgumentTypeError): |
| CustomConfigObject.parse({"name": "foo", "array": 1}) |
| with self.assertRaises(argparse.ArgumentTypeError): |
| CustomConfigObject.parse({"name": "foo", "name_alias": "foo"}) |
| with self.assertRaises(argparse.ArgumentTypeError): |
| CustomConfigObject.parse({"name": "foo", "array": [], "integer": "a"}) |
| with self.assertRaises(argparse.ArgumentTypeError): |
| CustomConfigObject.parse_dict({ |
| "name": "foo", |
| "array": [], |
| "integer": "a" |
| }) |
| |
| def test_parse_dict(self): |
| config = CustomConfigObject.parse({"name": "foo"}) |
| assert isinstance(config, CustomConfigObject) |
| self.assertEqual(config.name, "foo") |
| config = CustomConfigObject.parse({"name": "foo", "array": []}) |
| self.assertEqual(config.name, "foo") |
| self.assertListEqual(config.array, []) |
| data = {"name": "foo", "array": [1, 2, 3], "integer": 153} |
| config = CustomConfigObject.parse(dict(data)) |
| assert isinstance(config, CustomConfigObject) |
| self.assertEqual(config.name, "foo") |
| assert config.array |
| self.assertListEqual(config.array, [1, 2, 3]) |
| self.assertEqual(config.integer, 153) |
| config_2 = CustomConfigObject.parse_dict(dict(data)) |
| assert isinstance(config, CustomConfigObject) |
| self.assertEqual(config, config_2) |
| |
| def test_load_dict_extra_kwargs(self): |
| config = CustomConfigObject.parse({ |
| "name": "foo", |
| }, array=[], integer=123) |
| self.assertEqual(config.name, "foo") |
| self.assertListEqual(config.array, []) |
| self.assertEqual(config.integer, 123) |
| |
| def test_load_dict_extra_kwargs_invalid(self): |
| with self.assertRaises(argparse.ArgumentTypeError) as cm: |
| CustomConfigObject.parse({ |
| "name": "foo", |
| }, array=123, integer=[]) |
| self.assertIn("array", str(cm.exception)) |
| |
| def test_load_dict_extra_kwargs_duplicate_invalid(self): |
| with self.assertRaises(argparse.ArgumentTypeError) as cm: |
| CustomConfigObject.parse({ |
| "name": "foo", |
| }, name="bar") |
| self.assertIn("name", str(cm.exception)) |
| |
| def test_load_dict_extra_kwargs_duplicate(self): |
| config = CustomConfigObject.parse({ |
| "name": "foo", |
| }, name="foo", integer=123) |
| self.assertEqual(config.name, "foo") |
| self.assertEqual(config.integer, 123) |
| config = CustomConfigObject.parse({ |
| "name": "foo", |
| }, name=None, integer=999) |
| self.assertEqual(config.name, "foo") |
| self.assertEqual(config.integer, 999) |
| |
| def test_load_dict_unused(self): |
| config_data = {"name": "foo", "unused_data": 666} |
| config = CustomConfigObject.parse(config_data) |
| self.assertTrue(config_data) |
| assert isinstance(config, CustomConfigObject) |
| self.assertEqual(config.name, "foo") |
| with self.assertRaises(argparse.ArgumentTypeError) as cm: |
| CustomConfigObjectStrict.parse(config_data) |
| self.assertIn("unused_data", str(cm.exception)) |
| self.assertTrue(config_data) |
| |
| def test_load_dict_unused_extra_kwargs(self): |
| config_data = {"name": "foo", "unused_data": 666} |
| config = CustomConfigObject.parse(config_data, other_unused=999) |
| self.assertTrue(config_data) |
| assert isinstance(config, CustomConfigObject) |
| self.assertEqual(config.name, "foo") |
| with self.assertRaises(argparse.ArgumentTypeError) as cm: |
| CustomConfigObjectStrict.parse(config_data, other_unused=999) |
| self.assertIn("unused_data", str(cm.exception)) |
| self.assertIn("other_unused", str(cm.exception)) |
| self.assertTrue(config_data) |
| |
| def test_load_dict_default(self): |
| self.assertIsNone(CustomConfigObject.config_parser().default) |
| with self.assertRaises(argparse.ArgumentTypeError): |
| CustomConfigObject.parse({}) |
| self.assertIsNone(CustomConfigObject.config_parser().default, |
| CustomConfigObjectWithDefault.default()) |
| config = CustomConfigObjectWithDefault.parse({}) |
| self.assertEqual(config, CustomConfigObjectWithDefault.default()) |
| |
| def test_parse_dict_alias(self): |
| config = CustomConfigObject.parse({"name_alias": "foo"}) |
| assert isinstance(config, CustomConfigObject) |
| self.assertEqual(config.name, "foo") |
| |
| def test_parse_dict_custom_value_enum(self): |
| config = CustomConfigObject.parse({"name_alias": "foo"}) |
| assert isinstance(config, CustomConfigObject) |
| self.assertIs(config.custom_value_enum, CustomValueEnum.DEFAULT) |
| for config_value, result in ((CustomValueEnum.A_OR_TRUE, |
| CustomValueEnum.A_OR_TRUE), |
| ("a", CustomValueEnum.A_OR_TRUE), |
| (True, CustomValueEnum.A_OR_TRUE), |
| (CustomValueEnum.B_OR_FALSE, |
| CustomValueEnum.B_OR_FALSE), |
| ("b", CustomValueEnum.B_OR_FALSE), |
| (False, CustomValueEnum.B_OR_FALSE), |
| ("default", CustomValueEnum.DEFAULT)): |
| config = CustomConfigObject.parse({ |
| "name_alias": "foo", |
| "custom_value_enum": config_value |
| }) |
| self.assertIs(config.custom_value_enum, result) |
| |
| def test_parse_dict_custom_value_enum_invalid(self): |
| invalid: Any |
| for invalid in (1, 2, {}, "A", "B"): |
| with self.assertRaises(argparse.ArgumentTypeError) as cm: |
| CustomConfigObject.parse({ |
| "name_alias": "foo", |
| "custom_value_enum": invalid |
| }) |
| self.assertIn(f"{invalid}", str(cm.exception)) |
| |
| def test_parse_str(self): |
| config = CustomConfigObject.parse("a name") |
| assert isinstance(config, CustomConfigObject) |
| self.assertEqual(config.name, "a name") |
| |
| def test_parse_path_missing_file_str(self): |
| path = pathlib.Path("/invalid.file") |
| self.assertFalse(path.exists()) |
| with self.assertRaises(argparse.ArgumentTypeError): |
| CustomConfigObject.parse(str(path)) |
| with self.assertRaises(argparse.ArgumentTypeError): |
| CustomConfigObject.parse_path(path) |
| |
| def test_parse_path_missing_file(self): |
| path = pathlib.Path("/invalid.file") |
| self.assertFalse(path.exists()) |
| with self.assertRaises(argparse.ArgumentTypeError): |
| CustomConfigObject.parse(path) |
| with self.assertRaises(argparse.ArgumentTypeError): |
| CustomConfigObject.parse_path(path) |
| |
| def test_parse_path_missing_file_by_type(self): |
| path = pathlib.Path("invalid.file") |
| self.assertFalse(path.exists()) |
| with self.assertRaises(argparse.ArgumentTypeError): |
| CustomConfigObject.parse(path) |
| with self.assertRaises(argparse.ArgumentTypeError): |
| CustomConfigObject.parse_path(path) |
| |
| def test_parse_path_empty_file(self): |
| path = pathlib.Path("test_file.json") |
| self.assertFalse(path.exists()) |
| path.touch() |
| with self.assertRaises(argparse.ArgumentTypeError): |
| CustomConfigObject.parse(path) |
| with self.assertRaises(argparse.ArgumentTypeError): |
| CustomConfigObject.parse_path(path) |
| |
| def test_parse_path_invalid_json_file(self): |
| path = pathlib.Path("test_file.json") |
| path.write_text("{{", encoding="utf-8") |
| with self.assertRaises(argparse.ArgumentTypeError): |
| CustomConfigObject.parse(path) |
| with self.assertRaises(argparse.ArgumentTypeError): |
| CustomConfigObject.parse_path(path) |
| |
| def test_parse_path_empty_json_object(self): |
| path = pathlib.Path("test_file.json") |
| with path.open("w", encoding="utf-8") as f: |
| json.dump({}, f) |
| with self.assertRaises(argparse.ArgumentTypeError) as cm: |
| CustomConfigObject.parse(path) |
| self.assertIn("non-empty data", str(cm.exception)) |
| |
| def test_parse_path_invalid_json_array(self): |
| path = pathlib.Path("test_file.json") |
| with path.open("w", encoding="utf-8") as f: |
| json.dump([], f) |
| with self.assertRaises(argparse.ArgumentTypeError) as cm: |
| CustomConfigObject.parse(path) |
| self.assertIn("non-empty data", str(cm.exception)) |
| |
| def test_parse_path_minimal(self): |
| path = pathlib.Path("test_file.json") |
| with path.open("w", encoding="utf-8") as f: |
| json.dump({"name": "Config Name"}, f) |
| config = CustomConfigObject.parse(path) |
| assert isinstance(config, CustomConfigObject) |
| self.assertEqual(config.name, "Config Name") |
| self.assertIsNone(config.array) |
| self.assertIsNone(config.integer) |
| self.assertIsNone(config.nested) |
| config_2 = CustomConfigObject.parse(str(path)) |
| self.assertEqual(config, config_2) |
| |
| TEST_DICT: immutabledict[str, Any] = immutabledict({ |
| "name": "Config Name", |
| "array": [1, 3], |
| "integer": 166 |
| }) |
| |
| def test_parse_path_full(self): |
| path = pathlib.Path("test_file.json") |
| with path.open("w", encoding="utf-8") as f: |
| json.dump(dict(self.TEST_DICT), f) |
| config = CustomConfigObject.parse(path) |
| assert isinstance(config, CustomConfigObject) |
| self.assertEqual(config.name, "Config Name") |
| assert config.array |
| self.assertListEqual(config.array, [1, 3]) |
| self.assertEqual(config.integer, 166) |
| self.assertIsNone(config.nested) |
| config_2 = CustomConfigObject.parse(str(path)) |
| self.assertEqual(config, config_2) |
| |
| def test_parse_dict_full(self): |
| config = CustomConfigObject.parse_dict(dict(self.TEST_DICT)) |
| assert isinstance(config, CustomConfigObject) |
| self.assertEqual(config.name, "Config Name") |
| assert config.array |
| self.assertListEqual(config.array, [1, 3]) |
| self.assertEqual(config.integer, 166) |
| self.assertIsNone(config.nested) |
| |
| TEST_DICT_NESTED: immutabledict[str, str] = immutabledict( |
| {"name": "a nested name"}) |
| |
| def test_parse_dict_nested(self): |
| test_dict = dict(self.TEST_DICT) |
| test_dict["nested"] = dict(self.TEST_DICT_NESTED) |
| config = CustomConfigObject.parse_dict(test_dict) |
| assert isinstance(config, CustomConfigObject) |
| self.assertEqual(config.name, "Config Name") |
| assert config.array |
| self.assertListEqual(config.array, [1, 3]) |
| self.assertEqual(config.integer, 166) |
| self.assertEqual(config.nested, |
| CustomNestedConfigObject(name="a nested name")) |
| |
| def test_parse_dict_nested_file(self): |
| path = pathlib.Path("nested.json") |
| self.assertFalse(path.exists()) |
| with path.open("w", encoding="utf-8") as f: |
| json.dump(dict(self.TEST_DICT_NESTED), f) |
| test_dict = dict(self.TEST_DICT) |
| test_dict["nested"] = str(path) |
| config = CustomConfigObject.parse_dict(test_dict) |
| assert isinstance(config, CustomConfigObject) |
| self.assertEqual(config.nested, |
| CustomNestedConfigObject(name="a nested name")) |
| |
| def test_parse_nested_long(self): |
| test_dict = dict(self.TEST_DICT) |
| long_string = "abcd" * 1_000 |
| test_dict["nested"] = long_string |
| config = CustomConfigObject.parse_dict(test_dict) |
| assert isinstance(config, CustomConfigObject) |
| self.assertEqual(config.nested.name, long_string) |
| |
| def test_parse_nested_long_os_error(self): |
| test_dict = dict(self.TEST_DICT) |
| long_string = "abcd" * 100 |
| test_dict["nested"] = long_string |
| |
| def raise_os_error(self): |
| raise OSError("Invalid file name") |
| |
| with mock.patch.object(pathlib.Path, "is_file", raise_os_error): |
| config = CustomConfigObject.parse_dict(test_dict) |
| assert isinstance(config, CustomConfigObject) |
| self.assertEqual(config.nested.name, long_string) |
| |
| def test_parse_missing_depending(self): |
| with self.assertRaises(argparse.ArgumentTypeError) as cm: |
| CustomConfigObject.parse({"name": "foo", "depending_nested": "a value"}) |
| self.assertIn("depending_nested", str(cm.exception)) |
| self.assertIn("Expected nested", str(cm.exception)) |
| with self.assertRaises(argparse.ArgumentTypeError) as cm: |
| CustomConfigObject.parse({ |
| "name": "foo", |
| "depending_nested": "a value", |
| "nested": None |
| }) |
| self.assertIn("depending_nested", str(cm.exception)) |
| self.assertIn("Expected nested", str(cm.exception)) |
| |
| def test_parse_depending_simple(self): |
| config = CustomConfigObject.parse({ |
| "name": "foo", |
| "nested": "nested string value", |
| "depending_nested": "a value" |
| }) |
| self.assertDictEqual(config.depending_nested, { |
| "value": "a value", |
| "nested": config.nested |
| }) |
| |
| def test_parse_generic_enum(self): |
| test_dict = dict(self.TEST_DICT) |
| test_dict["generic_enum"] = "b" |
| config = CustomConfigObject.parse_dict(test_dict) |
| self.assertIs(config.generic_enum, GenericEnum.B) |
| test_dict = dict(self.TEST_DICT) |
| test_dict["generic_enum"] = "c" |
| config = CustomConfigObject.parse_dict(test_dict) |
| self.assertIs(config.generic_enum, GenericEnum.C) |
| |
| def test_parse_generic_enum_invalid(self): |
| test_dict = dict(self.TEST_DICT) |
| test_dict["generic_enum"] = "unknown value" |
| with self.assertRaises(argparse.ArgumentTypeError) as cm: |
| CustomConfigObject.parse_dict(test_dict) |
| error_message = str(cm.exception).lower() |
| self.assertIn("choices are", error_message) |
| self.assertIn("generic_enum", error_message) |
| |
| def test_parse_config_enum(self): |
| test_dict = dict(self.TEST_DICT) |
| test_dict["config_enum"] = "b" |
| config = CustomConfigObject.parse_dict(test_dict) |
| self.assertIs(config.config_enum, CustomConfigEnum.B) |
| test_dict = dict(self.TEST_DICT) |
| test_dict["config_enum"] = "c" |
| config = CustomConfigObject.parse_dict(test_dict) |
| self.assertIs(config.config_enum, CustomConfigEnum.C) |
| |
| def test_parse_custom_enum_invalid(self): |
| test_dict = dict(self.TEST_DICT) |
| test_dict["config_enum"] = "unknown value" |
| with self.assertRaises(argparse.ArgumentTypeError) as cm: |
| CustomConfigObject.parse_dict(test_dict) |
| error_message = str(cm.exception).lower() |
| self.assertIn("choices are", error_message) |
| self.assertIn("config_enum", error_message) |
| |
| def test_parse_templated_config_missing_arg_name_throws(self): |
| config = {"template": {"name": "$[ARG]"}, "args": {"": ""}} |
| |
| with self.assertRaisesRegex(MultiException, |
| "Template args must only contain"): |
| CustomConfigObject.parse(config) |
| |
| def test_parse_templated_config_lowercase_arg_name_throws(self): |
| config = {"template": {"name": "$[arg]"}, "args": {"arg": "my name"}} |
| |
| with self.assertRaisesRegex(MultiException, |
| "Template args must only contain"): |
| CustomConfigObject.parse(config) |
| |
| def test_parse_templated_config_space_beginning_arg_name_throws(self): |
| config = {"template": {"name": "$[ ARG]"}, "args": {" ARG": "my name"}} |
| |
| with self.assertRaisesRegex(MultiException, |
| "Template args must only contain"): |
| CustomConfigObject.parse(config) |
| |
| def test_parse_templated_config_space_end_arg_name_throws(self): |
| config = {"template": {"name": "$[ARG ]"}, "args": {"ARG ": "my name"}} |
| |
| with self.assertRaisesRegex(MultiException, |
| "Template args must only contain"): |
| CustomConfigObject.parse(config) |
| |
| def test_parse_templated_config_missing_arg_throws(self): |
| config = { |
| "template": { |
| "name": "$[MISSING_ARG]" |
| }, |
| "args": { |
| "ARG": "arg_value" |
| } |
| } |
| |
| with self.assertRaisesRegex(MultiException, "MISSING_ARG"): |
| config = CustomConfigObject.parse(config) |
| |
| def test_parse_templated_config_multiple_missing_args_throws(self): |
| config = { |
| "template": { |
| "name": "$[MISSING_ARG] $[MISSING_ARG2]" |
| }, |
| "args": { |
| "ARG": "arg_value" |
| } |
| } |
| |
| with self.assertRaises(MultiException) as cm: |
| CustomConfigObject.parse(config) |
| self.assertIn("'MISSING_ARG'", str(cm.exception)) |
| self.assertIn("'MISSING_ARG2'", str(cm.exception)) |
| |
| def test_parse_templated_config_unsupported_arg_throws(self): |
| config = { |
| "template": { |
| "name": "text and $[DICT_ARG]" |
| }, |
| "args": { |
| "DICT_ARG": { |
| "key": "value" |
| } |
| } |
| } |
| |
| with self.assertRaisesRegex(argparse.ArgumentTypeError, |
| "can not be substituted"): |
| CustomConfigObject.parse(config) |
| |
| def test_parse_templated_config_dict_arg(self): |
| config = { |
| "template": { |
| "name": "top level", |
| "nested": "$[ARG]" |
| }, |
| "args": { |
| "ARG": { |
| "name": "nested" |
| } |
| } |
| } |
| |
| config = CustomConfigObject.parse(config) |
| self.assertIsInstance(config, CustomConfigObject) |
| |
| self.assertEqual(config.nested.name, "nested") |
| |
| def test_parse_templated_config_empty_arg(self): |
| config = {"template": {"name": "$[ARG]"}, "args": {"ARG": ""}} |
| |
| config = CustomConfigObject.parse(config) |
| self.assertIsInstance(config, CustomConfigObject) |
| |
| self.assertEqual(config.name, "") |
| |
| def test_parse_templated_config_string_only(self): |
| config = {"template": {"name": "$[ARG]"}, "args": {"ARG": "arg_value"}} |
| |
| config = CustomConfigObject.parse(config) |
| self.assertIsInstance(config, CustomConfigObject) |
| |
| self.assertEqual(config.name, "arg_value") |
| |
| def test_parse_templated_config_unused_arg(self): |
| config = { |
| "template": { |
| "name": "$[ARG]" |
| }, |
| "args": { |
| "ARG": "arg_value", |
| "UNUSED_ARG": "unused" |
| } |
| } |
| |
| config = CustomConfigObject.parse(config) |
| self.assertIsInstance(config, CustomConfigObject) |
| |
| self.assertEqual(config.name, "arg_value") |
| |
| def test_parse_templated_config_string_multiple(self): |
| config = { |
| "template": { |
| "name": "$[ONE]$[TWO]$[THREE]$[FOUR]", |
| }, |
| "args": { |
| "ONE": "1", |
| "TWO": "2", |
| "THREE": "3", |
| "FOUR": "4" |
| } |
| } |
| |
| config = CustomConfigObject.parse(config) |
| self.assertIsInstance(config, CustomConfigObject) |
| |
| self.assertEqual(config.name, "1234") |
| |
| def test_parse_templated_config_string_multiple_mixed(self): |
| config = { |
| "template": { |
| "name": "[$[ONE]_$[TWO]_$[THREE]_$[FOUR]]", |
| }, |
| "args": { |
| "ONE": "1", |
| "TWO": 2, |
| "THREE": 3.0, |
| "FOUR": 4.56 |
| } |
| } |
| |
| config = CustomConfigObject.parse(config) |
| self.assertIsInstance(config, CustomConfigObject) |
| |
| self.assertEqual(config.name, "[1_2_3.0_4.56]") |
| |
| def test_parse_templated_config_string_nested_matches(self): |
| config = { |
| "template": { |
| "name": "$[$[$[ARG]]]", |
| }, |
| "args": { |
| "ARG": "ARG2", |
| "ARG2": "ARG3", |
| "ARG3": "the true arg" |
| } |
| } |
| |
| config = CustomConfigObject.parse(config) |
| self.assertIsInstance(config, CustomConfigObject) |
| |
| self.assertEqual(config.name, "the true arg") |
| |
| def test_parse_template_full_string_substitute_finishes_substitution(self): |
| config = { |
| "template": { |
| "name": "$[ARG]" |
| }, |
| "args": { |
| "ARG": "prefix$[ARG2]", |
| "ARG2": "name" |
| } |
| } |
| |
| config = CustomConfigObject.parse(config) |
| |
| self.assertEqual(config.name, "prefixname") |
| |
| def test_parse_templated_config_int(self): |
| config = { |
| "template": { |
| "name": "name", |
| "integer": "$[ARG]" |
| }, |
| "args": { |
| "ARG": 4 |
| } |
| } |
| |
| config = CustomConfigObject.parse(config) |
| self.assertIsInstance(config, CustomConfigObject) |
| |
| self.assertEqual(config.integer, 4) |
| |
| def test_parse_templated_config_float(self): |
| config = { |
| "template": { |
| "name": "name", |
| "float_field": "$[ARG]" |
| }, |
| "args": { |
| "ARG": 1.3 |
| } |
| } |
| |
| config = CustomConfigObject.parse(config) |
| self.assertIsInstance(config, CustomConfigObject) |
| |
| self.assertEqual(config.float_field, 1.3) |
| |
| def test_parse_templated_config_filepath(self): |
| template_path_str = "template_file.hjson" |
| template = { |
| "name": "$[ARG]", |
| } |
| |
| path = pathlib.Path(template_path_str) |
| with path.open("w", encoding="utf-8") as f: |
| json.dump(template, f) |
| |
| args = {"template": template_path_str, "args": {"ARG": "arg_value"}} |
| |
| config = CustomConfigObject.parse(args) |
| self.assertIsInstance(config, CustomConfigObject) |
| |
| self.assertEqual(config.name, "arg_value") |
| |
| def test_parse_templated_config_two_layer_filepath(self): |
| nested_path_str = "nested.hjson" |
| nested = {"name": "$[ARG]"} |
| |
| path = pathlib.Path(nested_path_str) |
| with path.open("w", encoding="utf-8") as f: |
| json.dump(nested, f) |
| |
| template_path_str = "template_file.hjson" |
| template = { |
| "name": "top level", |
| "nested": nested_path_str, |
| } |
| |
| path = pathlib.Path(template_path_str) |
| with path.open("w", encoding="utf-8") as f: |
| json.dump(template, f) |
| |
| args = {"template": template_path_str, "args": {"ARG": "arg_value"}} |
| |
| config = CustomConfigObject.parse(args) |
| self.assertIsInstance(config, CustomConfigObject) |
| |
| self.assertEqual(config.nested.name, "arg_value") |
| |
| def test_parse_templated_config_two_level_template(self): |
| config = { |
| "template": { |
| "name": "$[TOP_LEVEL_ARG]", |
| "nested": { |
| "template": { |
| "name": "$[SECOND_LEVEL_ARG]" |
| }, |
| "args": { |
| "SECOND_LEVEL_ARG": "second-level-name" |
| } |
| } |
| }, |
| "args": { |
| "TOP_LEVEL_ARG": "top-level-name" |
| } |
| } |
| |
| config = CustomConfigObject.parse(config) |
| self.assertIsInstance(config, CustomConfigObject) |
| |
| self.assertEqual(config.name, "top-level-name") |
| self.assertEqual(config.nested.name, "second-level-name") |
| |
| def test_parse_templated_config_two_level_template_files(self): |
| nested_path_str = "nested.hjson" |
| nested = { |
| "template": { |
| "name": "$[NESTED_NAME]" |
| }, |
| "args": { |
| "NESTED_NAME": "nested" |
| } |
| } |
| |
| path = pathlib.Path(nested_path_str) |
| with path.open("w", encoding="utf-8") as f: |
| json.dump(nested, f) |
| |
| config = { |
| "template": { |
| "name": "$[TOP_LEVEL_ARG]", |
| "nested": nested_path_str |
| }, |
| "args": { |
| "TOP_LEVEL_ARG": "top-level-name" |
| } |
| } |
| |
| config = CustomConfigObject.parse(config) |
| self.assertIsInstance(config, CustomConfigObject) |
| |
| self.assertEqual(config.name, "top-level-name") |
| self.assertEqual(config.nested.name, "nested") |
| |
| def test_parse_templated_config_single_escaped_value(self): |
| config = { |
| "template": { |
| "name": "some text $[[ARG] on either side", |
| }, |
| "args": { |
| "PLACEHOLDER": "nothing" |
| } |
| } |
| |
| config = CustomConfigObject.parse(config) |
| self.assertIsInstance(config, CustomConfigObject) |
| |
| self.assertEqual(config.name, "some text $[ARG] on either side") |
| |
| def test_parse_templated_config_nested_escaped_value(self): |
| config = { |
| "template": { |
| "name": "$[[$[[ARG]]", |
| }, |
| "args": { |
| "PLACEHOLDER": "nothing" |
| } |
| } |
| |
| config = CustomConfigObject.parse(config) |
| self.assertIsInstance(config, CustomConfigObject) |
| |
| self.assertEqual(config.name, "$[[$[ARG]]") |
| |
| def test_parse_templated_config_escaped_value_and_non_escaped(self): |
| config = { |
| "template": { |
| "name": "$[[ARG] $[ARG]", |
| }, |
| "args": { |
| "ARG": "arg_value" |
| } |
| } |
| |
| config = CustomConfigObject.parse(config) |
| self.assertIsInstance(config, CustomConfigObject) |
| |
| self.assertEqual(config.name, "$[ARG] arg_value") |
| |
| def test_parse_template_single_unbound_arg(self): |
| config = { |
| "template": { |
| "name": "$[ARG]", |
| "nested": { |
| "template": { |
| "name": "$[ARG]" |
| }, |
| "unbound_args": ["ARG"] |
| } |
| }, |
| "args": { |
| "ARG": "from-top-level" |
| } |
| } |
| |
| config = CustomConfigObject.parse(config) |
| self.assertIsInstance(config, CustomConfigObject) |
| |
| self.assertEqual(config.name, "from-top-level") |
| self.assertEqual(config.nested.name, "from-top-level") |
| |
| def test_parse_template_multiple_unbound_arg(self): |
| config = { |
| "template": { |
| "name": "$[ARG]", |
| "nested": { |
| "template": { |
| "name": "$[ARG] $[ARG2]" |
| }, |
| "unbound_args": ["ARG", "ARG2"] |
| } |
| }, |
| "args": { |
| "ARG": "hello", |
| "ARG2": "world" |
| } |
| } |
| |
| config = CustomConfigObject.parse(config) |
| self.assertIsInstance(config, CustomConfigObject) |
| |
| self.assertEqual(config.name, "hello") |
| self.assertEqual(config.nested.name, "hello world") |
| |
| def test_parse_template_unbound_arg_undefined(self): |
| config = { |
| "template": { |
| "name": "$[ARG]", |
| "nested": { |
| "template": { |
| "name": "$[NOT_AN_ARG]" |
| }, |
| "unbound_args": ["NOT_AN_ARG"] |
| } |
| }, |
| "args": { |
| "ARG": "hello", |
| } |
| } |
| with self.assertRaisesRegex(MultiException, "'NOT_AN_ARG'"): |
| config = CustomConfigObject.parse(config) |
| |
| def test_self_referencing_arg_throws(self): |
| config = { |
| "template": { |
| "name": "$[ARG]", |
| }, |
| "args": { |
| "ARG": "some other $[ARG] text" |
| } |
| } |
| with self.assertRaisesRegex(MultiException, "self-referencing"): |
| config = CustomConfigObject.parse(config) |
| |
| def test_self_referencing_detection_escaped_arg(self): |
| config = { |
| "template": { |
| "name": "$[ARG]", |
| }, |
| "args": { |
| "ARG": "some other $[[ARG] text" |
| } |
| } |
| config = CustomConfigObject.parse(config) |
| |
| def test_self_referencing_detection_arg_name_no_arg_sequence(self): |
| config = { |
| "template": { |
| "name": "$[ARG]", |
| }, |
| "args": { |
| "ARG": "some other arg text" |
| } |
| } |
| config = CustomConfigObject.parse(config) |
| |
| def test_parse_nested_templated_config_urls(self): |
| config = { |
| "template": { |
| "name": "name", |
| "nested": "$[ARG]" |
| }, |
| "args": { |
| "ARG": { |
| "name": "https://www.google.com" |
| } |
| } |
| } |
| |
| config = CustomConfigObject.parse(config) |
| self.assertIsInstance(config, CustomConfigObject) |
| |
| self.assertEqual(config.nested.name, "https://www.google.com") |
| |
| def test_parse_templated_config_filepaths_in_template_list(self): |
| template_dir = pathlib.Path("/templates") |
| template_dir.mkdir() |
| (template_dir / "test_file").write_text("test file") |
| template_path = template_dir / "template.hjson" |
| template = {"name": "$[NAME]", "array": ["./test_file"]} |
| |
| with template_path.open("w", encoding="utf-8") as f: |
| json.dump(template, f) |
| |
| config_dict = {"template": str(template_path), "args": {"NAME": "name"}} |
| config = CustomConfigObject.parse(config_dict) |
| self.assertIsInstance(config, CustomConfigObject) |
| self.assertEqual(config.name, "name") |
| self.assertEqual(config.array[0], "/templates/test_file") |
| |
| def test_parse_templated_config_relative_filepaths_as_str_preserved(self): |
| config = {"template": "./templates/template.hjson", "args": {"UNUSED": "",}} |
| config_file = pathlib.Path("/config.hjson") |
| config_file.write_text(json.dumps(config, indent=2), encoding="utf-8") |
| |
| template_dir = pathlib.Path("/templates") |
| template_dir.mkdir() |
| |
| name_file = template_dir / "name.weird_extension" |
| name_file.write_text("name") |
| |
| template_file = template_dir / "template.hjson" |
| template = {"name": "./name.weird_extension"} |
| template_file.write_text(json.dumps(template, indent=2), encoding="utf-8") |
| |
| config = CustomConfigObject.parse("/config.hjson") |
| self.assertIsInstance(config, CustomConfigObject) |
| self.assertEqual(config.name, "/templates/name.weird_extension") |
| |
| def test_template_list_spread_in_non_list_does_nothing(self): |
| config = { |
| "template": { |
| "name": "$[...NAME]" |
| }, |
| "args": { |
| "NAME": ["my name",] |
| } |
| } |
| config = CustomConfigObject.parse(config) |
| self.assertEqual(config.name, "$[...NAME]") |
| |
| def test_template_list_spread_non_list_value_throws(self): |
| config = { |
| "template": { |
| "array": ["some", "string", "values", "$[...ARG]"] |
| }, |
| "args": { |
| "ARG": "arg_value" |
| } |
| } |
| with self.assertRaisesRegex(MultiException, "is not a list"): |
| config = CustomConfigObject.parse(config) |
| |
| def test_template_list_spread_end(self): |
| config = { |
| "template": { |
| "name": "name", |
| "array": ["some", "string", "values", "$[...ARG]"] |
| }, |
| "args": { |
| "ARG": ["arg_value"] |
| } |
| } |
| |
| config = CustomConfigObject.parse(config) |
| self.assertEqual(len(config.array), 4) |
| self.assertEqual(config.array[3], "arg_value") |
| |
| def test_template_list_spread_beginning(self): |
| config = { |
| "template": { |
| "name": "name", |
| "array": [ |
| "$[...ARG]", |
| "some", |
| "string", |
| "values", |
| ] |
| }, |
| "args": { |
| "ARG": ["arg_value"] |
| } |
| } |
| |
| config = CustomConfigObject.parse(config) |
| self.assertEqual(len(config.array), 4) |
| self.assertEqual(config.array[0], "arg_value") |
| |
| def test_template_list_spread_middle(self): |
| config = { |
| "template": { |
| "name": "name", |
| "array": [ |
| "some", |
| "string", |
| "$[...ARG]", |
| "values", |
| ] |
| }, |
| "args": { |
| "ARG": ["arg_value"] |
| } |
| } |
| |
| config = CustomConfigObject.parse(config) |
| self.assertEqual(len(config.array), 4) |
| self.assertEqual(config.array[2], "arg_value") |
| |
| def test_template_list_spread_multiple_middle(self): |
| config = { |
| "template": { |
| "name": "name", |
| "array": [ |
| "some", |
| "string", |
| "$[...ARG]", |
| "values", |
| ] |
| }, |
| "args": { |
| "ARG": ["arg_value", "another arg value"] |
| } |
| } |
| |
| config = CustomConfigObject.parse(config) |
| self.assertEqual(len(config.array), 5) |
| self.assertEqual(config.array[2], "arg_value") |
| self.assertEqual(config.array[3], "another arg value") |
| |
| def test_template_list_spread_multi_level_substitution(self): |
| config = { |
| "template": { |
| "name": "name", |
| "array": [ |
| "some", |
| "string", |
| "$[...ARG]", |
| "values", |
| ] |
| }, |
| "args": { |
| "ARG": { |
| "template": ["$[ARG2]"], |
| "args": { |
| "ARG2": "list entry" |
| } |
| }, |
| } |
| } |
| |
| config = CustomConfigObject.parse(config) |
| self.assertEqual(len(config.array), 4) |
| self.assertEqual(config.array[2], "list entry") |
| |
| def test_template_list_spread_empty_substitution(self): |
| config = { |
| "template": { |
| "name": "name", |
| "array": [ |
| "some", |
| "string", |
| "$[...ARG]", |
| "values", |
| ] |
| }, |
| "args": { |
| "ARG": [] |
| } |
| } |
| |
| config = CustomConfigObject.parse(config) |
| self.assertListEqual(config.array, ["some", "string", "values"]) |
| |
| def test_parse_template_unbound_list_spread_arg(self): |
| config = { |
| "template": { |
| "name": "name", |
| "nested": { |
| "template": { |
| "name": "name", |
| "array": [ |
| "first", |
| "$[...ARG]", |
| "third", |
| ] |
| }, |
| "unbound_args": ["ARG"] |
| } |
| }, |
| "args": { |
| "ARG": ["second"] |
| } |
| } |
| |
| config = CustomConfigObject.parse(config) |
| self.assertIsInstance(config, CustomConfigObject) |
| self.assertListEqual(config.nested.array, ["first", "second", "third"]) |
| |
| |
| class ConfigEnumTestCase(unittest.TestCase): |
| |
| def test_parse_invalid(self): |
| for invalid in ("", None): |
| with self.assertRaises(argparse.ArgumentTypeError) as cm: |
| CustomConfigEnum.parse(invalid) |
| error_message = str(cm.exception) |
| self.assertIn("Choices are", error_message) |
| self.assertIn("CustomConfigEnum", error_message) |
| |
| def test_parse(self): |
| for value, result in ((CustomConfigEnum.A, |
| CustomConfigEnum.A), ("a", CustomConfigEnum.A), |
| (CustomConfigEnum.B, |
| CustomConfigEnum.B), ("c", CustomConfigEnum.C)): |
| self.assertIs(CustomConfigEnum.parse(value), result) |
| |
| |
| if __name__ == "__main__": |
| test_helper.run_pytest(__file__) |