def test_create_from_all_supported_data_types_correctly(self): class _Config(object): DEFAULT_BOOL = True def __init__(self): self._bool = self.DEFAULT_BOOL self._float = None self._int = None self._str = None @property def bool(self) -> bool: return self._bool @bool.setter def bool(self, x: bool) -> None: self._bool = x @property def float(self) -> float: return self._float @float.setter def float(self, x: float) -> None: self._float = x @property def int(self) -> int: return self._int @int.setter def int(self, x: int) -> None: self._int = x @property def str(self) -> str: return self._str @str.setter def str(self, x: str) -> None: self._str = x spec = config_spec.ConfigSpec.create_from(_Config) self.assertEqual(4, len(spec)) self.assertEqual( value_spec.ValueSpec("bool", "No description available.", bool, False, True), spec.get_value_by_name("bool")) self.assertEqual( value_spec.ValueSpec("float", "No description available.", float, True, None), spec.get_value_by_name("float")) self.assertEqual( value_spec.ValueSpec("int", "No description available.", int, True, None), spec.get_value_by_name("int")) self.assertEqual( value_spec.ValueSpec("str", "No description available.", str, True, None), spec.get_value_by_name("str"))
def setUp(self): self.spec = config_spec.ConfigSpec() self.spec.add_value( value_spec.ValueSpec("conf_1", "No description available.", bool, False, False)) self.spec.add_value( value_spec.ValueSpec("conf_2", "No description available.", int, True, None)) self.parser = magiq_parser.MagiqParser(_TestConfig, "name", "description")
def test_add_value_stores_value_specs_as_expected(self): spec = config_spec.ConfigSpec() value_1 = value_spec.ValueSpec("val-1", "val-1", str, False, None) value_2 = value_spec.ValueSpec("val-2", "val-2", str, False, None) self.assertEqual(0, len(spec)) spec.add_value(value_1) self.assertEqual(1, len(spec)) self.assertEqual({value_1}, set(spec)) spec.add_value(value_2) self.assertEqual(2, len(spec)) self.assertEqual({value_1, value_2}, set(spec))
def test_parse_raises_a_value_error_if_an_illegal_value_is_provided(self): parser = float_parser.FloatParser(value_spec.ValueSpec("some_config", "Just a test", float, True, None)) argv = "--some-config", "not-a-number" self.assertTrue(parser.fires(argv)) with self.assertRaises(ValueError): parser._parse(argv)
def test_get_value_by_name_retrieves_existing_config_values_as_expected( self): spec = config_spec.ConfigSpec() value = value_spec.ValueSpec("val", "val", str, False, None) spec.add_value(value) self.assertIs(value, spec.get_value_by_name("val"))
def test_parse_json_returns_the_provided_value_if_its_of_the_correct_type( self): parser = _DummyParser( value_spec.ValueSpec("some_config", "Just a test", str, True, None)) self.assertEqual("works", parser.parse_json("works"))
def test_parse_raises_a_value_error_if_an_expected_value_is_missing(self): parser = float_parser.FloatParser(value_spec.ValueSpec("some_config", "Just a test", float, True, None)) argv = "--some-config", self.assertTrue(parser.fires(argv)) with self.assertRaises(ValueError): parser._parse(argv)
def test_parse_json_raises_a_type_error_if_the_provided_value_does_not_comply_with_the_spec( self): parser = _DummyParser( value_spec.ValueSpec("some_config", "Just a test", str, True, None)) with self.assertRaises(TypeError): parser.parse_json(123)
def test_parse_raises_a_value_error_if_the_parser_does_not_fire(self): parser = _DummyParser( value_spec.ValueSpec("some_config", "Just a test", str, True, None)) with self.assertRaises(ValueError): parser.parse(("--smth-else-first", "--some-config", "value")) with self.assertRaises(ValueError): parser.parse(("smth different entirely", ))
def test_parse_raises_a_value_error_if_an_expected_value_is_missing(self): parser = str_parser.StrParser( value_spec.ValueSpec("some_config", "Just a test", str, True, None)) with self.assertRaises(ValueError): self.assertEqual(("value", tuple()), parser._parse(("--some-config", )))
def test_parse_extracts_provided_args_as_expected(self): parser = str_parser.StrParser( value_spec.ValueSpec("some_config", "Just a test", str, True, None)) self.assertEqual(("value", ("--another-config", "another value")), parser._parse(("--some-config", "value", "--another-config", "another value"))) self.assertEqual(("value", tuple()), parser._parse(("--some-config", "value")))
def test_parse_json_processes_values_correctly(self): parser = float_parser.FloatParser(value_spec.ValueSpec("some_config", "Just a test", float, True, None)) value = parser.parse_json(666.666) self.assertIsInstance(value, float) self.assertEqual(666.666, value) value = parser.parse_json(666) self.assertIsInstance(value, float) self.assertEqual(666.0, value)
def test_fires_yields_the_expected_values(self): parser = _DummyParser( value_spec.ValueSpec("some_config", "Just a test", str, True, None)) self.assertTrue(parser.fires(("--some-config", ))) self.assertTrue(parser.fires(("--some-config", "value"))) self.assertFalse( parser.fires(("--smth-else-first", "--some-config", "value"))) self.assertFalse(parser.fires(("smth different entirely", )))
def test_parse_extracts_legal_integer_args_as_expected(self): parser = int_parser.IntParser( value_spec.ValueSpec("some_config", "Just a test", int, True, None)) value, argv = parser._parse( ("--some-config", "666", "--another-config", "another value")) self.assertIsInstance(value, int) self.assertEqual((666, ("--another-config", "another value")), (value, argv)) value, argv = parser._parse(("--some-config", "666")) self.assertIsInstance(value, int) self.assertEqual((666, tuple()), (value, argv))
def test_parse_extracts_legal_values_as_expected(self): parser = float_parser.FloatParser(value_spec.ValueSpec("some_config", "Just a test", float, True, None)) value, argv = parser._parse(("--some-config", "666.666", "--another-config", "another value")) self.assertIsInstance(value, float) self.assertEqual((666.666, ("--another-config", "another value")), (value, argv)) value, argv = parser._parse(("--some-config", "666.666")) self.assertIsInstance(value, float) self.assertEqual((666.666, tuple()), (value, argv)) value, argv = parser._parse(("--some-config", "666")) self.assertIsInstance(value, float) self.assertEqual((666.0, tuple()), (value, argv))
def test_create_from_the_generic_optional_alias_correctly(self): class _Config(object): DEFAULT_CONF_WITH_DEFAULT_VALUE = 123 def __init__(self): self._value = None @property def value(self) -> typing.Optional[int]: return self._value @value.setter def value(self, value: int) -> None: self._value = value spec = config_spec.ConfigSpec.create_from(_Config) self.assertEqual(1, len(spec)) self.assertEqual( value_spec.ValueSpec("value", "No description available.", int, True, None), spec.get_value_by_name("value"))
def create_from(cls, config_cls: type): """A factory method for creating a configuration specification based on the provided class. The created specification defines one option for each property (i.e., methods annotated with ``@property``) of the given class except those that start with an underscore (i.e., private properties). Class-level fields whose names start with :attr:`argmagiq.DEFAULT_PREFIX` are assumed to define default values for options. Type and description for each of the options are extracted from the according property's type annotations and docstring. Args: config_cls (type): The class that the configuration is based on. """ spec = ConfigSpec() # load all default values default_values = {} for name, field in inspect.getmembers( config_cls, lambda f: not inspect.isroutine(f)): if name.startswith(argmagiq.DEFAULT_PREFIX): field_name = name[len(argmagiq.DEFAULT_PREFIX):].lower() default_values[field_name] = field # find all public mutable properties -> these are considered as config values for name, field in inspect.getmembers( config_cls, lambda f: isinstance(f, property)): # only consider public properties that have an according setter method if name.startswith("_") or field.fset is None: continue # check if the config has a default value specified default_value = None if name in default_values: default_value = default_values[name] # check if the option is required required = (default_value is None and not (argmagiq.OPTIONAL_KEY in field.fget.__dict__)) # fetch docstring as description of the config if field.__doc__ is None: description = "No description available." else: m = re.match( cls.DOC_REGEX, field.__doc__ ) # -> this is done to remove any type specifications description = m.group( "doc") if m else "No description available." # determine the option's data type from the properties type annotation sig = inspect.signature(field.fget) if sig.return_annotation is None: raise ValueError( f"Property <{name}> does not have a return-type annotation" ) data_type = sig.return_annotation # if the datatype is a generic alias, then we have to extract the actual data type from it if not isinstance(data_type, type): # -> the type is a generic alias # check if the data type is a union # -> this also covers typing.Optional[X], which is a shorthand for typing.Union[X, None] if "__origin__" in data_type.__dict__ and data_type.__dict__[ "__origin__"] == typing.Union: # gather all types in the union except None all_types = [ t for t in data_type.__dict__["__args__"] if t is not type(None) ] # ensure that there is just one type left if len(all_types) == 1: data_type = all_types[0] else: raise ValueError( f"Property <{name}> has an ambiguous data type") else: # since no other generic aliases are supported, raise an error raise ValueError( f"Property <{name}> has an unsupported generic-alias type: {data_type}" ) # add the parsed information to the config spec spec.add_value( value_spec.ValueSpec(name, description, data_type, required, default_value)) return spec