Skip to content

Commit

Permalink
Merge 0fea13e into 0d4b82a
Browse files Browse the repository at this point in the history
  • Loading branch information
ncilfone committed Mar 14, 2022
2 parents 0d4b82a + 0fea13e commit 0c06821
Show file tree
Hide file tree
Showing 14 changed files with 170 additions and 21 deletions.
2 changes: 1 addition & 1 deletion spock/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def _attribute_name_to_config_name_mapping(
attribute_name_to_config_name_mapping, attr.name, n.__name__
):
raise _SpockDuplicateArgumentError(
f"`{attr.name}` key is located in more than one config and cannot be resolved automatically."
f"`{attr.name}` key is located in more than one config and cannot be resolved automatically. "
f"Either specify the config name (`<config>.{attr.name}`) or change the key name in the config."
)
attribute_name_to_config_name_mapping[attr.name] = n.__name__
Expand Down
4 changes: 3 additions & 1 deletion spock/backend/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,9 @@ def _extract_other_types(self, typed, module_name):
"""
return_list = []
if hasattr(typed, "__args__"):
if hasattr(typed, "__args__") and not isinstance(
typed, _SpockVariadicGenericAlias
):
for val in typed.__args__:
recurse_return = self._extract_other_types(val, module_name)
if isinstance(recurse_return, list):
Expand Down
96 changes: 95 additions & 1 deletion spock/backend/field_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,84 @@ def handle_optional_attribute_type(
_SpockNotOptionalError
"""
print("hi")
raise _SpockNotOptionalError(
f"Parameter `{attr_space.attribute.name}` within `{attr_space.config_space.name}` is of "
f"type `{type(attr_space.attribute.type)}` which seems to be unsupported -- "
f"are you missing an @spock decorator on a base python class?"
)


class RegisterListCallableField(RegisterFieldTemplate):
"""Class that registers callable types
Attributes:
special_keys: dictionary to check special keys
"""

def __init__(self):
"""Init call to RegisterSimpleField
Args:
"""
super(RegisterListCallableField, self).__init__()

def _convert(self, val):
str_field = str(val)
module, fn = str_field.rsplit(".", 1)
try:
call_ref = getattr(importlib.import_module(module), fn)
except Exception as e:
raise _SpockValueError(
f"Attempted to import module {module} and callable {fn} however it could not be found on the current "
f"python path: {e}"
)
return call_ref

def _recurse_callables(self, val: List):
attr_list = []
for sub in val:
if isinstance(sub, list) or isinstance(sub, List):
attr_list.append(self._recurse_callables(sub))
else:
attr_list.append(self._convert(sub))
return attr_list

def handle_attribute_from_config(
self, attr_space: AttributeSpace, builder_space: BuilderSpace
):
"""Handles setting a simple attribute when it is a spock class type
Args:
attr_space: holds information about a single attribute that is mapped to a ConfigSpace
builder_space: named_tuple containing the arguments and spock_space
Returns:
"""
# These are always going to be strings... cast just in case
attr_list = []
for val in builder_space.arguments[attr_space.config_space.name][
attr_space.attribute.name
]:
if isinstance(val, list) or isinstance(val, List):
attr_list.append(self._recurse_callables(val))
else:
attr_list.append(self._convert(val))
attr_space.field = attr_list

def handle_optional_attribute_type(
self, attr_space: AttributeSpace, builder_space: BuilderSpace
):
"""Not implemented for this type
Args:
attr_space: holds information about a single attribute that is mapped to a ConfigSpace
builder_space: named_tuple containing the arguments and spock_space
Raises:
_SpockNotOptionalError
"""
raise _SpockNotOptionalError(
f"Parameter `{attr_space.attribute.name}` within `{attr_space.config_space.name}` is of "
f"type `{type(attr_space.attribute.type)}` which seems to be unsupported -- "
Expand Down Expand Up @@ -640,6 +717,19 @@ def handle_optional_attribute_type(
self._attr_type(attr_space).__name__
] = attr_space.field

@classmethod
def _find_list_callables(cls, typed):
out = False
if hasattr(typed, "__args__") and not isinstance(
typed.__args__[0], _SpockVariadicGenericAlias
):
out = cls._find_list_callables(typed.__args__[0])
elif hasattr(typed, "__args__") and isinstance(
typed.__args__[0], _SpockVariadicGenericAlias
):
out = True
return out

@classmethod
def recurse_generate(cls, spock_cls, builder_space: BuilderSpace):
"""Call on a spock classes to iterate through the attrs attributes and handle each based on type and optionality
Expand Down Expand Up @@ -668,6 +758,10 @@ def recurse_generate(cls, spock_cls, builder_space: BuilderSpace):
(attribute.type is list) or (attribute.type is List)
) and _is_spock_instance(attribute.metadata["type"].__args__[0]):
handler = RegisterList()
elif (
(attribute.type is list) or (attribute.type is List)
) and cls._find_list_callables(attribute.metadata["type"]):
handler = RegisterListCallableField()
# Enums
elif isinstance(attribute.type, EnumMeta) and _check_iterable(
attribute.type
Expand Down
19 changes: 16 additions & 3 deletions spock/backend/saver.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# SPDX-License-Identifier: Apache-2.0

"""Handles prepping and saving the Spock config"""

import typing
from abc import abstractmethod
from uuid import uuid4

Expand Down Expand Up @@ -180,6 +180,18 @@ def _convert_tuples_2_lists(self, clean_inner_dict, inner_val, inner_key):
clean_inner_dict.update({inner_key: inner_val})
return clean_inner_dict

def _callable_2_str(self, val):
"""Converts a callable to a str based on the module and name
Args:
val: callable object
Returns:
string of module.name
"""
return f"{val.__module__}.{val.__name__}"

def _recursive_tuple_to_list(self, value):
"""Recursively turn tuples into lists
Expand Down Expand Up @@ -277,6 +289,8 @@ def _recursively_handle_clean(
# For those that are a spock class and are repeated (cls_name == key) simply convert to dict
if (cls_name in all_cls) and (cls_name == key):
clean_val.append(attr.asdict(l_val))
elif callable(l_val):
clean_val.append(self._callable_2_str(l_val))
# For those whose cls is different than the key just append the cls name
elif cls_name in all_cls:
# Change the flag as this is a repeated class -- which needs to be compressed into a single
Expand All @@ -292,8 +306,7 @@ def _recursively_handle_clean(
out_dict.update({key: clean_val})
# Catch any callables -- convert back to the str representation
elif callable(val):
call_2_str = f"{val.__module__}.{val.__name__}"
out_dict.update({key: call_2_str})
out_dict.update({key: self._callable_2_str(val)})
# If it's a spock class but has a parent then just use the class name to reference the values
elif (val_name in all_cls) and parent_name is not None:
out_dict.update({key: val_name})
Expand Down
6 changes: 3 additions & 3 deletions spock/backend/typed.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,12 @@ def _extract_base_type(typed):
Returns:
name of type
"""
if hasattr(typed, "__args__"):
if hasattr(typed, "__args__") and not isinstance(typed, _SpockVariadicGenericAlias):
name = _get_name_py_version(typed=typed)
bracket_val = f"{name}[{_extract_base_type(typed.__args__[0])}]"
return bracket_val
else:
bracket_value = typed.__name__
bracket_value = _get_name_py_version(typed=typed)
return bracket_value


Expand All @@ -73,7 +73,7 @@ def _recursive_generic_validator(typed):
return_type: recursively built deep_iterable validators
"""
if hasattr(typed, "__args__"):
if hasattr(typed, "__args__") and not isinstance(typed, _SpockVariadicGenericAlias):
# If there are more __args__ then we still need to recurse as it is still a GenericAlias
# Iterate through since there might be multiple types?
if len(typed.__args__) > 1:
Expand Down
3 changes: 3 additions & 0 deletions spock/backend/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

"""Attr utility functions for Spock"""

from spock.utils import _SpockVariadicGenericAlias


def get_attr_fields(input_classes):
"""Gets the attribute fields from all classes
Expand Down Expand Up @@ -147,6 +149,7 @@ def _recursive_list_to_tuple(key, value, typed, class_names):
hasattr(typed, "__args__")
and not isinstance(value, tuple)
and not (isinstance(value, str) and value in class_names)
and not isinstance(typed, _SpockVariadicGenericAlias)
):
# Force those with origin tuple types to be of the defined length
if (typed.__origin__.__name__.lower() == "tuple") and len(value) != len(
Expand Down
16 changes: 14 additions & 2 deletions tests/base/attr_configs_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ class TypeConfig:
high_config: SingleNestedConfig
# Callable
call_me: Callable
# List of Callable
call_us: List[Callable]


@spock
Expand Down Expand Up @@ -203,6 +205,8 @@ class TypeOptConfig:
int_p_2: Optional[int]
# Optional Callable
call_me_maybe: Optional[Callable]
# List optional call me
call_us_maybe: Optional[List[Callable]]


@spock
Expand All @@ -215,6 +219,10 @@ def foo(val: int):
return val * 2


def bar(val: int):
return val * 2


@spock
class TypeDefaultConfig:
"""This creates a test Spock config of all supported variable types as required parameters and falls back
Expand Down Expand Up @@ -266,7 +274,9 @@ class TypeDefaultConfig:
# Double Nested class ref
high_config_def: SingleNestedConfig = SingleNestedConfig
# Optional Callable
call_me_maybe: Callable = foo
call_me_maybe_def: Callable = foo
# List of Callable
call_us_maybe_def: List[Callable] = [foo, foo]


@spock
Expand Down Expand Up @@ -312,7 +322,9 @@ class TypeDefaultOptConfig:
# Class Enum
class_enum_opt_def: Optional[ClassChoice] = NestedStuff
# Optional Callable
call_me_maybe: Optional[Callable] = foo
call_me_maybe_opt_def: Optional[Callable] = foo
# List optional call me
call_us_maybe_opt_def: Optional[List[Callable]] = [foo, foo]


@spock
Expand Down
17 changes: 14 additions & 3 deletions tests/base/base_asserts_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ def test_all_set(self, arg_builder):
assert arg_builder.TypeConfig.high_config.double_nested_config.h_factor == 0.99
assert arg_builder.TypeConfig.high_config.double_nested_config.v_factor == 0.90
assert arg_builder.TypeConfig.call_me == foo
assert arg_builder.TypeConfig.call_us[0] == foo
assert arg_builder.TypeConfig.call_us[1] == foo

# Optional #
assert arg_builder.TypeOptConfig.int_p_opt_no_def is None
Expand All @@ -72,6 +74,7 @@ def test_all_set(self, arg_builder):
assert arg_builder.TypeOptConfig.nested_list_opt_no_def is None
assert arg_builder.TypeOptConfig.class_enum_opt_no_def is None
assert arg_builder.TypeOptConfig.call_me_maybe is None
assert arg_builder.TypeOptConfig.call_us_maybe is None


class AllDefaults:
Expand Down Expand Up @@ -124,7 +127,9 @@ def test_all_defaults(self, arg_builder):
arg_builder.TypeDefaultConfig.high_config_def.double_nested_config.v_factor
== 0.90
)
assert arg_builder.TypeDefaultConfig.call_me_maybe == foo
assert arg_builder.TypeDefaultConfig.call_me_maybe_def == foo
assert arg_builder.TypeDefaultConfig.call_us_maybe_def[0] == foo
assert arg_builder.TypeDefaultConfig.call_us_maybe_def[1] == foo

# Optional w/ Defaults #
assert arg_builder.TypeDefaultOptConfig.int_p_opt_def == 10
Expand Down Expand Up @@ -160,7 +165,9 @@ def test_all_defaults(self, arg_builder):
assert arg_builder.TypeDefaultOptConfig.nested_list_opt_def[1].two == "bye"
assert arg_builder.TypeDefaultOptConfig.class_enum_opt_def.one == 11
assert arg_builder.TypeDefaultOptConfig.class_enum_opt_def.two == "ciao"
assert arg_builder.TypeDefaultOptConfig.call_me_maybe == foo
assert arg_builder.TypeDefaultOptConfig.call_me_maybe_opt_def == foo
assert arg_builder.TypeDefaultOptConfig.call_us_maybe_opt_def[0] == foo
assert arg_builder.TypeDefaultOptConfig.call_us_maybe_opt_def[1] == foo


class AllInherited:
Expand Down Expand Up @@ -212,6 +219,8 @@ def test_all_inherited(self, arg_builder):
arg_builder.TypeInherited.high_config.double_nested_config.v_factor == 0.90
)
assert arg_builder.TypeInherited.call_me == foo
assert arg_builder.TypeInherited.call_us[0] == foo
assert arg_builder.TypeInherited.call_us[1] == foo

# Optional w/ Defaults #
assert arg_builder.TypeInherited.int_p_opt_def == 10
Expand All @@ -225,7 +234,9 @@ def test_all_inherited(self, arg_builder):
assert arg_builder.TypeInherited.tuple_p_opt_def_int == (10, 20)
assert arg_builder.TypeInherited.tuple_p_opt_def_str == ("Spock", "Package")
assert arg_builder.TypeInherited.tuple_p_opt_def_bool == (True, False)
assert arg_builder.TypeInherited.call_me_maybe == foo
assert arg_builder.TypeInherited.call_me_maybe_opt_def == foo
assert arg_builder.TypeInherited.call_us_maybe_opt_def[0] == foo
assert arg_builder.TypeInherited.call_us_maybe_opt_def[1] == foo


class AllDynamic:
Expand Down
16 changes: 12 additions & 4 deletions tests/base/test_cmd_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,14 @@ def arg_builder(monkeypatch):
"--SecondDoubleNestedConfig.morph_tolerance",
"0.2",
"--TypeConfig.call_me",
'tests.base.attr_configs_test.foo'
'tests.base.attr_configs_test.bar',
"--TypeConfig.call_us",
"['tests.base.attr_configs_test.bar', 'tests.base.attr_configs_test.bar']"
],
)
config = ConfigArgBuilder(
TypeConfig, NestedStuff, NestedListStuff, SingleNestedConfig,
FirstDoubleNestedConfig, SecondDoubleNestedConfig,desc="Test Builder"
FirstDoubleNestedConfig, SecondDoubleNestedConfig, desc="Test Builder"
)
return config.generate()

Expand Down Expand Up @@ -121,7 +123,9 @@ def test_class_overrides(self, arg_builder):
assert arg_builder.NestedListStuff[1].two == "Working"
assert isinstance(arg_builder.SingleNestedConfig.double_nested_config, SecondDoubleNestedConfig) is True
assert arg_builder.SecondDoubleNestedConfig.morph_tolerance == 0.2
assert arg_builder.TypeConfig.call_me == foo
assert arg_builder.TypeConfig.call_me == bar
assert arg_builder.TypeConfig.call_us[0] == bar
assert arg_builder.TypeConfig.call_us[1] == bar


class TestClassOnlyCmdLine:
Expand Down Expand Up @@ -192,7 +196,9 @@ def arg_builder(monkeypatch):
"--TypeConfig.high_config",
"SingleNestedConfig",
"--TypeConfig.call_me",
'tests.base.attr_configs_test.foo'
'tests.base.attr_configs_test.foo',
"--TypeConfig.call_us",
"['tests.base.attr_configs_test.foo', 'tests.base.attr_configs_test.foo']"
],
)
config = ConfigArgBuilder(
Expand Down Expand Up @@ -237,6 +243,8 @@ def test_class_overrides(self, arg_builder):
assert arg_builder.NestedListStuff[1].one == 21
assert arg_builder.NestedListStuff[1].two == "Working"
assert arg_builder.TypeConfig.call_me == foo
assert arg_builder.TypeConfig.call_us[0] == foo
assert arg_builder.TypeConfig.call_us[1] == foo


class TestRaiseCmdLineNoKey:
Expand Down
3 changes: 2 additions & 1 deletion tests/conf/json/test.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,6 @@
"TypeConfig": {
"float_p": 12.0
},
"call_me": "tests.base.attr_configs_test.foo"
"call_me": "tests.base.attr_configs_test.foo",
"call_us": ["tests.base.attr_configs_test.foo", "tests.base.attr_configs_test.foo"]
}
Loading

0 comments on commit 0c06821

Please sign in to comment.