Skip to content

Commit a761359

Browse files
authored
Support for List[Callable] (#230)
* support for List[Callable] * linted
1 parent 0d4b82a commit a761359

14 files changed

+170
-21
lines changed

spock/args.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ def _attribute_name_to_config_name_mapping(
116116
attribute_name_to_config_name_mapping, attr.name, n.__name__
117117
):
118118
raise _SpockDuplicateArgumentError(
119-
f"`{attr.name}` key is located in more than one config and cannot be resolved automatically."
119+
f"`{attr.name}` key is located in more than one config and cannot be resolved automatically. "
120120
f"Either specify the config name (`<config>.{attr.name}`) or change the key name in the config."
121121
)
122122
attribute_name_to_config_name_mapping[attr.name] = n.__name__

spock/backend/builder.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,9 @@ def _extract_other_types(self, typed, module_name):
184184
185185
"""
186186
return_list = []
187-
if hasattr(typed, "__args__"):
187+
if hasattr(typed, "__args__") and not isinstance(
188+
typed, _SpockVariadicGenericAlias
189+
):
188190
for val in typed.__args__:
189191
recurse_return = self._extract_other_types(val, module_name)
190192
if isinstance(recurse_return, list):

spock/backend/field_handlers.py

+95-1
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,84 @@ def handle_optional_attribute_type(
383383
_SpockNotOptionalError
384384
385385
"""
386-
print("hi")
386+
raise _SpockNotOptionalError(
387+
f"Parameter `{attr_space.attribute.name}` within `{attr_space.config_space.name}` is of "
388+
f"type `{type(attr_space.attribute.type)}` which seems to be unsupported -- "
389+
f"are you missing an @spock decorator on a base python class?"
390+
)
391+
392+
393+
class RegisterListCallableField(RegisterFieldTemplate):
394+
"""Class that registers callable types
395+
396+
Attributes:
397+
special_keys: dictionary to check special keys
398+
399+
"""
400+
401+
def __init__(self):
402+
"""Init call to RegisterSimpleField
403+
404+
Args:
405+
"""
406+
super(RegisterListCallableField, self).__init__()
407+
408+
def _convert(self, val):
409+
str_field = str(val)
410+
module, fn = str_field.rsplit(".", 1)
411+
try:
412+
call_ref = getattr(importlib.import_module(module), fn)
413+
except Exception as e:
414+
raise _SpockValueError(
415+
f"Attempted to import module {module} and callable {fn} however it could not be found on the current "
416+
f"python path: {e}"
417+
)
418+
return call_ref
419+
420+
def _recurse_callables(self, val: List):
421+
attr_list = []
422+
for sub in val:
423+
if isinstance(sub, list) or isinstance(sub, List):
424+
attr_list.append(self._recurse_callables(sub))
425+
else:
426+
attr_list.append(self._convert(sub))
427+
return attr_list
428+
429+
def handle_attribute_from_config(
430+
self, attr_space: AttributeSpace, builder_space: BuilderSpace
431+
):
432+
"""Handles setting a simple attribute when it is a spock class type
433+
434+
Args:
435+
attr_space: holds information about a single attribute that is mapped to a ConfigSpace
436+
builder_space: named_tuple containing the arguments and spock_space
437+
438+
Returns:
439+
"""
440+
# These are always going to be strings... cast just in case
441+
attr_list = []
442+
for val in builder_space.arguments[attr_space.config_space.name][
443+
attr_space.attribute.name
444+
]:
445+
if isinstance(val, list) or isinstance(val, List):
446+
attr_list.append(self._recurse_callables(val))
447+
else:
448+
attr_list.append(self._convert(val))
449+
attr_space.field = attr_list
450+
451+
def handle_optional_attribute_type(
452+
self, attr_space: AttributeSpace, builder_space: BuilderSpace
453+
):
454+
"""Not implemented for this type
455+
456+
Args:
457+
attr_space: holds information about a single attribute that is mapped to a ConfigSpace
458+
builder_space: named_tuple containing the arguments and spock_space
459+
460+
Raises:
461+
_SpockNotOptionalError
462+
463+
"""
387464
raise _SpockNotOptionalError(
388465
f"Parameter `{attr_space.attribute.name}` within `{attr_space.config_space.name}` is of "
389466
f"type `{type(attr_space.attribute.type)}` which seems to be unsupported -- "
@@ -640,6 +717,19 @@ def handle_optional_attribute_type(
640717
self._attr_type(attr_space).__name__
641718
] = attr_space.field
642719

720+
@classmethod
721+
def _find_list_callables(cls, typed):
722+
out = False
723+
if hasattr(typed, "__args__") and not isinstance(
724+
typed.__args__[0], _SpockVariadicGenericAlias
725+
):
726+
out = cls._find_list_callables(typed.__args__[0])
727+
elif hasattr(typed, "__args__") and isinstance(
728+
typed.__args__[0], _SpockVariadicGenericAlias
729+
):
730+
out = True
731+
return out
732+
643733
@classmethod
644734
def recurse_generate(cls, spock_cls, builder_space: BuilderSpace):
645735
"""Call on a spock classes to iterate through the attrs attributes and handle each based on type and optionality
@@ -668,6 +758,10 @@ def recurse_generate(cls, spock_cls, builder_space: BuilderSpace):
668758
(attribute.type is list) or (attribute.type is List)
669759
) and _is_spock_instance(attribute.metadata["type"].__args__[0]):
670760
handler = RegisterList()
761+
elif (
762+
(attribute.type is list) or (attribute.type is List)
763+
) and cls._find_list_callables(attribute.metadata["type"]):
764+
handler = RegisterListCallableField()
671765
# Enums
672766
elif isinstance(attribute.type, EnumMeta) and _check_iterable(
673767
attribute.type

spock/backend/saver.py

+16-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# SPDX-License-Identifier: Apache-2.0
55

66
"""Handles prepping and saving the Spock config"""
7-
7+
import typing
88
from abc import abstractmethod
99
from uuid import uuid4
1010

@@ -180,6 +180,18 @@ def _convert_tuples_2_lists(self, clean_inner_dict, inner_val, inner_key):
180180
clean_inner_dict.update({inner_key: inner_val})
181181
return clean_inner_dict
182182

183+
def _callable_2_str(self, val):
184+
"""Converts a callable to a str based on the module and name
185+
186+
Args:
187+
val: callable object
188+
189+
Returns:
190+
string of module.name
191+
192+
"""
193+
return f"{val.__module__}.{val.__name__}"
194+
183195
def _recursive_tuple_to_list(self, value):
184196
"""Recursively turn tuples into lists
185197
@@ -277,6 +289,8 @@ def _recursively_handle_clean(
277289
# For those that are a spock class and are repeated (cls_name == key) simply convert to dict
278290
if (cls_name in all_cls) and (cls_name == key):
279291
clean_val.append(attr.asdict(l_val))
292+
elif callable(l_val):
293+
clean_val.append(self._callable_2_str(l_val))
280294
# For those whose cls is different than the key just append the cls name
281295
elif cls_name in all_cls:
282296
# Change the flag as this is a repeated class -- which needs to be compressed into a single
@@ -292,8 +306,7 @@ def _recursively_handle_clean(
292306
out_dict.update({key: clean_val})
293307
# Catch any callables -- convert back to the str representation
294308
elif callable(val):
295-
call_2_str = f"{val.__module__}.{val.__name__}"
296-
out_dict.update({key: call_2_str})
309+
out_dict.update({key: self._callable_2_str(val)})
297310
# If it's a spock class but has a parent then just use the class name to reference the values
298311
elif (val_name in all_cls) and parent_name is not None:
299312
out_dict.update({key: val_name})

spock/backend/typed.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,12 @@ def _extract_base_type(typed):
5151
Returns:
5252
name of type
5353
"""
54-
if hasattr(typed, "__args__"):
54+
if hasattr(typed, "__args__") and not isinstance(typed, _SpockVariadicGenericAlias):
5555
name = _get_name_py_version(typed=typed)
5656
bracket_val = f"{name}[{_extract_base_type(typed.__args__[0])}]"
5757
return bracket_val
5858
else:
59-
bracket_value = typed.__name__
59+
bracket_value = _get_name_py_version(typed=typed)
6060
return bracket_value
6161

6262

@@ -73,7 +73,7 @@ def _recursive_generic_validator(typed):
7373
return_type: recursively built deep_iterable validators
7474
7575
"""
76-
if hasattr(typed, "__args__"):
76+
if hasattr(typed, "__args__") and not isinstance(typed, _SpockVariadicGenericAlias):
7777
# If there are more __args__ then we still need to recurse as it is still a GenericAlias
7878
# Iterate through since there might be multiple types?
7979
if len(typed.__args__) > 1:

spock/backend/utils.py

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
"""Attr utility functions for Spock"""
77

8+
from spock.utils import _SpockVariadicGenericAlias
9+
810

911
def get_attr_fields(input_classes):
1012
"""Gets the attribute fields from all classes
@@ -147,6 +149,7 @@ def _recursive_list_to_tuple(key, value, typed, class_names):
147149
hasattr(typed, "__args__")
148150
and not isinstance(value, tuple)
149151
and not (isinstance(value, str) and value in class_names)
152+
and not isinstance(typed, _SpockVariadicGenericAlias)
150153
):
151154
# Force those with origin tuple types to be of the defined length
152155
if (typed.__origin__.__name__.lower() == "tuple") and len(value) != len(

tests/base/attr_configs_test.py

+14-2
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ class TypeConfig:
158158
high_config: SingleNestedConfig
159159
# Callable
160160
call_me: Callable
161+
# List of Callable
162+
call_us: List[Callable]
161163

162164

163165
@spock
@@ -203,6 +205,8 @@ class TypeOptConfig:
203205
int_p_2: Optional[int]
204206
# Optional Callable
205207
call_me_maybe: Optional[Callable]
208+
# List optional call me
209+
call_us_maybe: Optional[List[Callable]]
206210

207211

208212
@spock
@@ -215,6 +219,10 @@ def foo(val: int):
215219
return val * 2
216220

217221

222+
def bar(val: int):
223+
return val * 2
224+
225+
218226
@spock
219227
class TypeDefaultConfig:
220228
"""This creates a test Spock config of all supported variable types as required parameters and falls back
@@ -266,7 +274,9 @@ class TypeDefaultConfig:
266274
# Double Nested class ref
267275
high_config_def: SingleNestedConfig = SingleNestedConfig
268276
# Optional Callable
269-
call_me_maybe: Callable = foo
277+
call_me_maybe_def: Callable = foo
278+
# List of Callable
279+
call_us_maybe_def: List[Callable] = [foo, foo]
270280

271281

272282
@spock
@@ -312,7 +322,9 @@ class TypeDefaultOptConfig:
312322
# Class Enum
313323
class_enum_opt_def: Optional[ClassChoice] = NestedStuff
314324
# Optional Callable
315-
call_me_maybe: Optional[Callable] = foo
325+
call_me_maybe_opt_def: Optional[Callable] = foo
326+
# List optional call me
327+
call_us_maybe_opt_def: Optional[List[Callable]] = [foo, foo]
316328

317329

318330
@spock

tests/base/base_asserts_test.py

+14-3
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ def test_all_set(self, arg_builder):
5252
assert arg_builder.TypeConfig.high_config.double_nested_config.h_factor == 0.99
5353
assert arg_builder.TypeConfig.high_config.double_nested_config.v_factor == 0.90
5454
assert arg_builder.TypeConfig.call_me == foo
55+
assert arg_builder.TypeConfig.call_us[0] == foo
56+
assert arg_builder.TypeConfig.call_us[1] == foo
5557

5658
# Optional #
5759
assert arg_builder.TypeOptConfig.int_p_opt_no_def is None
@@ -72,6 +74,7 @@ def test_all_set(self, arg_builder):
7274
assert arg_builder.TypeOptConfig.nested_list_opt_no_def is None
7375
assert arg_builder.TypeOptConfig.class_enum_opt_no_def is None
7476
assert arg_builder.TypeOptConfig.call_me_maybe is None
77+
assert arg_builder.TypeOptConfig.call_us_maybe is None
7578

7679

7780
class AllDefaults:
@@ -124,7 +127,9 @@ def test_all_defaults(self, arg_builder):
124127
arg_builder.TypeDefaultConfig.high_config_def.double_nested_config.v_factor
125128
== 0.90
126129
)
127-
assert arg_builder.TypeDefaultConfig.call_me_maybe == foo
130+
assert arg_builder.TypeDefaultConfig.call_me_maybe_def == foo
131+
assert arg_builder.TypeDefaultConfig.call_us_maybe_def[0] == foo
132+
assert arg_builder.TypeDefaultConfig.call_us_maybe_def[1] == foo
128133

129134
# Optional w/ Defaults #
130135
assert arg_builder.TypeDefaultOptConfig.int_p_opt_def == 10
@@ -160,7 +165,9 @@ def test_all_defaults(self, arg_builder):
160165
assert arg_builder.TypeDefaultOptConfig.nested_list_opt_def[1].two == "bye"
161166
assert arg_builder.TypeDefaultOptConfig.class_enum_opt_def.one == 11
162167
assert arg_builder.TypeDefaultOptConfig.class_enum_opt_def.two == "ciao"
163-
assert arg_builder.TypeDefaultOptConfig.call_me_maybe == foo
168+
assert arg_builder.TypeDefaultOptConfig.call_me_maybe_opt_def == foo
169+
assert arg_builder.TypeDefaultOptConfig.call_us_maybe_opt_def[0] == foo
170+
assert arg_builder.TypeDefaultOptConfig.call_us_maybe_opt_def[1] == foo
164171

165172

166173
class AllInherited:
@@ -212,6 +219,8 @@ def test_all_inherited(self, arg_builder):
212219
arg_builder.TypeInherited.high_config.double_nested_config.v_factor == 0.90
213220
)
214221
assert arg_builder.TypeInherited.call_me == foo
222+
assert arg_builder.TypeInherited.call_us[0] == foo
223+
assert arg_builder.TypeInherited.call_us[1] == foo
215224

216225
# Optional w/ Defaults #
217226
assert arg_builder.TypeInherited.int_p_opt_def == 10
@@ -225,7 +234,9 @@ def test_all_inherited(self, arg_builder):
225234
assert arg_builder.TypeInherited.tuple_p_opt_def_int == (10, 20)
226235
assert arg_builder.TypeInherited.tuple_p_opt_def_str == ("Spock", "Package")
227236
assert arg_builder.TypeInherited.tuple_p_opt_def_bool == (True, False)
228-
assert arg_builder.TypeInherited.call_me_maybe == foo
237+
assert arg_builder.TypeInherited.call_me_maybe_opt_def == foo
238+
assert arg_builder.TypeInherited.call_us_maybe_opt_def[0] == foo
239+
assert arg_builder.TypeInherited.call_us_maybe_opt_def[1] == foo
229240

230241

231242
class AllDynamic:

tests/base/test_cmd_line.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,14 @@ def arg_builder(monkeypatch):
7979
"--SecondDoubleNestedConfig.morph_tolerance",
8080
"0.2",
8181
"--TypeConfig.call_me",
82-
'tests.base.attr_configs_test.foo'
82+
'tests.base.attr_configs_test.bar',
83+
"--TypeConfig.call_us",
84+
"['tests.base.attr_configs_test.bar', 'tests.base.attr_configs_test.bar']"
8385
],
8486
)
8587
config = ConfigArgBuilder(
8688
TypeConfig, NestedStuff, NestedListStuff, SingleNestedConfig,
87-
FirstDoubleNestedConfig, SecondDoubleNestedConfig,desc="Test Builder"
89+
FirstDoubleNestedConfig, SecondDoubleNestedConfig, desc="Test Builder"
8890
)
8991
return config.generate()
9092

@@ -121,7 +123,9 @@ def test_class_overrides(self, arg_builder):
121123
assert arg_builder.NestedListStuff[1].two == "Working"
122124
assert isinstance(arg_builder.SingleNestedConfig.double_nested_config, SecondDoubleNestedConfig) is True
123125
assert arg_builder.SecondDoubleNestedConfig.morph_tolerance == 0.2
124-
assert arg_builder.TypeConfig.call_me == foo
126+
assert arg_builder.TypeConfig.call_me == bar
127+
assert arg_builder.TypeConfig.call_us[0] == bar
128+
assert arg_builder.TypeConfig.call_us[1] == bar
125129

126130

127131
class TestClassOnlyCmdLine:
@@ -192,7 +196,9 @@ def arg_builder(monkeypatch):
192196
"--TypeConfig.high_config",
193197
"SingleNestedConfig",
194198
"--TypeConfig.call_me",
195-
'tests.base.attr_configs_test.foo'
199+
'tests.base.attr_configs_test.foo',
200+
"--TypeConfig.call_us",
201+
"['tests.base.attr_configs_test.foo', 'tests.base.attr_configs_test.foo']"
196202
],
197203
)
198204
config = ConfigArgBuilder(
@@ -237,6 +243,8 @@ def test_class_overrides(self, arg_builder):
237243
assert arg_builder.NestedListStuff[1].one == 21
238244
assert arg_builder.NestedListStuff[1].two == "Working"
239245
assert arg_builder.TypeConfig.call_me == foo
246+
assert arg_builder.TypeConfig.call_us[0] == foo
247+
assert arg_builder.TypeConfig.call_us[1] == foo
240248

241249

242250
class TestRaiseCmdLineNoKey:

tests/conf/json/test.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,6 @@
4747
"TypeConfig": {
4848
"float_p": 12.0
4949
},
50-
"call_me": "tests.base.attr_configs_test.foo"
50+
"call_me": "tests.base.attr_configs_test.foo",
51+
"call_us": ["tests.base.attr_configs_test.foo", "tests.base.attr_configs_test.foo"]
5152
}

0 commit comments

Comments
 (0)