Skip to content

Commit

Permalink
Merge c6fe7f3 into 95bdfdb
Browse files Browse the repository at this point in the history
  • Loading branch information
ncilfone committed Apr 6, 2021
2 parents 95bdfdb + c6fe7f3 commit 50c824c
Show file tree
Hide file tree
Showing 14 changed files with 261 additions and 92 deletions.
2 changes: 1 addition & 1 deletion spock/backend/attr/payload.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def _update_payload(base_payload, input_classes, payload):
if isinstance(values, list):
# Check for incorrect specific override of global def
if keys not in attr_fields:
raise TypeError(f'Referring to a class space {keys} that is undefined')
raise ValueError(f'Referring to a class space {keys} that is undefined')
# We are in a repeated class def
# Raise if the key set is different from the defined set (i.e. incorrect arguments)
key_set = set(list(chain(*[list(val.keys()) for val in values])))
Expand Down
57 changes: 40 additions & 17 deletions spock/backend/attr/typed.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ def __new__(cls, x):
return super().__new__(cls, x)


def _get_name_py_version(typed):
"""Gets the name of the type depending on the python version
*Args*:
typed: the type of the parameter
*Returns*:
name of the type
"""
return typed._name if hasattr(typed, '_name') else typed.__name__


def _extract_base_type(typed):
"""Extracts the name of the type from a _GenericAlias
Expand All @@ -43,10 +58,7 @@ def _extract_base_type(typed):
name of type
"""
if hasattr(typed, '__args__'):
if minor < 7:
name = typed.__name__
else:
name = typed._name
name = _get_name_py_version(typed=typed)
bracket_val = f"{name}[{_extract_base_type(typed.__args__[0])}]"
return bracket_val
else:
Expand Down Expand Up @@ -229,8 +241,6 @@ def _in_type(instance, attribute, value, options):
*Returns*:
"""
if type(options) not in [list, tuple, EnumMeta]:
raise TypeError(f'options argument must be of type List, Tuple, or Enum -- given {type(options)}')
if type(value) not in options:
raise ValueError(f'{attribute.name} must be in {options}')

Expand Down Expand Up @@ -294,10 +304,7 @@ def _type_katra(typed, default=None, optional=False):
if isinstance(typed, type):
name = typed.__name__
elif isinstance(typed, _GenericAlias):
if minor < 7:
name = typed.__name__
else:
name = typed._name
name = _get_name_py_version(typed=typed)
else:
raise TypeError('Encountered an uxpected type in _type_katra')
special_key = None
Expand Down Expand Up @@ -348,18 +355,32 @@ def _handle_optional_typing(typed):
type_args = typed.__args__
# Optional[X] has type_args = (X, None) and is equal to Union[X, None]
if (len(type_args) == 2) and (typed == Union[type_args[0], None]):
# Since this is true we need to strip out the OG type
# Grab all the types that are not NoneType and collapse to a list
type_list = [val for val in type_args if val is not type(None)]
if len(type_list) > 1:
raise TypeError(f"Passing multiple subscript types to GenericAlias is not supported: {type_list}")
else:
typed = type_list[0]
typed = type_args[0]
# Set the optional flag to true
optional = True
return typed, optional


def _check_generic_recursive_single_type(typed):
"""Checks generics for the single types -- mixed types of generics are not allowed
*Args*:
typed: type
*Returns*:
"""
# Check if it has __args__ to look for optionality as it is a GenericAlias
if hasattr(typed, '__args__'):
if len(set(typed.__args__)) > 1:
type_list = [str(val) for val in typed.__args__]
raise TypeError(f"Passing multiple different subscript types to GenericAlias is not supported: {type_list}")
else:
for val in typed.__args__:
_check_generic_recursive_single_type(typed=val)


def katra(typed, default=None):
"""Public interface to create a katra
Expand All @@ -380,6 +401,8 @@ def katra(typed, default=None):
"""
# Handle optionals
typed, optional = _handle_optional_typing(typed)
# Check generic types for consistent types
_check_generic_recursive_single_type(typed)
# We need to check if the type is a _GenericAlias so that we can handle subscripted general types
# If it is subscript typed it will not be T which python uses as a generic type name
if isinstance(typed, _GenericAlias) and (not isinstance(typed.__args__[0], TypeVar)):
Expand Down
20 changes: 9 additions & 11 deletions spock/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
from spock.backend.attr.builder import AttrBuilder
from spock.backend.attr.payload import AttrPayload
from spock.backend.attr.saver import AttrSaver
from spock.backend.base import Spockspace
from spock.utils import check_payload_overwrite
from spock.utils import deep_payload_update

Expand Down Expand Up @@ -61,22 +60,17 @@ def __call__(self, *args, **kwargs):
"""
return ConfigArgBuilder(*args, **kwargs)

def generate(self, unclass=False):
def generate(self):
"""Generate method that returns the actual argument namespace
*Args*:
unclass: swaps the backend attr class type for dictionaries
*Returns*:
argument namespace consisting of all config classes
"""
if unclass:
self._arg_namespace = Spockspace(**{k: Spockspace(**{
val.name: getattr(v, val.name) for val in v.__attrs_attrs__})
for k, v in self._arg_namespace.__dict__.items()})
return self._arg_namespace

@staticmethod
Expand All @@ -95,11 +89,15 @@ def _set_backend(args):
# Gather if all attr backend
type_attrs = all([attr.has(arg) for arg in args])
if not type_attrs:
raise TypeError("*args must be of all attrs backend")
elif type_attrs:
backend = {'builder': AttrBuilder, 'payload': AttrPayload, 'saver': AttrSaver}
which_idx = [attr.has(arg) for arg in args].index(False)
if hasattr(args[which_idx], '__name__'):
raise TypeError(f"*args must be of all attrs backend -- missing a @spock decorator on class "
f"{args[which_idx].__name__}")
else:
raise TypeError(f"*args must be of all attrs backend -- invalid type "
f"{type(args[which_idx])}")
else:
raise TypeError("*args must be of all attrs backend")
backend = {'builder': AttrBuilder, 'payload': AttrPayload, 'saver': AttrSaver}
return backend

def _get_config_paths(self):
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ class StrChoice(Enum):
option_2 = 'option_2'


class FailedEnum(Enum):
str_type = 'hello'
float_type = 10.0


class IntChoice(Enum):
option_1 = 10
option_2 = 20
Expand Down
119 changes: 117 additions & 2 deletions tests/attr/test_all_attr.py → tests/attr_tests/test_all_attr.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@

from attr.exceptions import FrozenInstanceError
import glob
import os
import pytest
from spock.builder import ConfigArgBuilder
from spock.config import isinstance_spock
from tests.attr.attr_configs_test import *
from tests.attr_tests.attr_configs_test import *
import sys


Expand Down Expand Up @@ -190,6 +191,16 @@ def arg_builder(monkeypatch):
return config.generate()


class TestHelp:
def test_help(self, monkeypatch):
with monkeypatch.context() as m:
m.setattr(sys, 'argv', ['', '--config',
'./tests/conf/yaml/test.yaml', '--help'])
with pytest.raises(SystemExit):
config = ConfigArgBuilder(TypeConfig, NestedStuff, NestedListStuff, TypeOptConfig, desc='Test Builder')
return config.generate()


class TestFrozen:
"""Testing the frozen state of the spock config object"""
@staticmethod
Expand Down Expand Up @@ -389,6 +400,17 @@ def arg_builder(monkeypatch):
return config.generate()


class TestNonAttrs:
def test_non_attrs_fail(self, monkeypatch):
with monkeypatch.context() as m:
with pytest.raises(TypeError):
class AttrFail:
failed_attr: int
config = ConfigArgBuilder(TypeConfig, NestedStuff, NestedListStuff, TypeOptConfig, AttrFail,
configs=['./tests/conf/yaml/test.yaml'])
return config.generate()


class TestChoiceRaises:
"""Check all inherited types work as expected """
def test_choice_raise(self, monkeypatch):
Expand Down Expand Up @@ -438,6 +460,68 @@ def test_type_unknown(self, monkeypatch):
ConfigArgBuilder(TypeConfig, NestedStuff, NestedListStuff, desc='Test Builder')


class TestUnknownClassParameterArg:
def test_class_parameter_unknown(self, monkeypatch):
with monkeypatch.context() as m:
m.setattr(sys, 'argv', ['', '--config',
'./tests/conf/yaml/test_class_incorrect.yaml'])
with pytest.raises(ValueError):
ConfigArgBuilder(TypeConfig, NestedStuff, NestedListStuff, desc='Test Builder')


class TestUnknownClassArg:
def test_class_unknown(self, monkeypatch):
with monkeypatch.context() as m:
m.setattr(sys, 'argv', ['', '--config',
'./tests/conf/yaml/test_missing_class.yaml'])
with pytest.raises(ValueError):
ConfigArgBuilder(TypeConfig, NestedStuff, NestedListStuff, desc='Test Builder')


class TestWrongRepeatedClass:
def test_class_unknown(self, monkeypatch):
with monkeypatch.context() as m:
m.setattr(sys, 'argv', ['', '--config',
'./tests/conf/yaml/test_incorrect_repeated_class.yaml'])
with pytest.raises(ValueError):
ConfigArgBuilder(TypeConfig, NestedStuff, NestedListStuff, desc='Test Builder')


class TestEnumMixedFail:
def test_enum_mixed_fail(self, monkeypatch):
with monkeypatch.context() as m:
with pytest.raises(TypeError):
@spock
class EnumFail:
choice_mixed: FailedEnum


class TestIncorrectType:
def test_incorrect_type(self, monkeypatch):
with monkeypatch.context() as m:
with pytest.raises(TypeError):
@spock
class TypeFail:
weird_type: lambda x: x


class TestEnumClassMissing:
def test_enum_class_missing(self, monkeypatch):
with monkeypatch.context() as m:
m.setattr(sys, 'argv', ['', '--config',
'./tests/conf/yaml/test_wrong_class_enum.yaml'])
with pytest.raises(ValueError):
ConfigArgBuilder(TypeConfig, NestedStuff, NestedListStuff, desc='Test Builder')


class TestMixedGeneric:
def test_mixed_generic(self, monkeypatch):
with monkeypatch.context() as m:
with pytest.raises(TypeError):
@spock
class GenericFail:
generic_fail: Tuple[List[int], List[int], int]

class TestConfigCycles:
"""Checks the raise for cyclical dependencies"""
def test_config_cycles(self, monkeypatch):
Expand Down Expand Up @@ -486,9 +570,40 @@ def test_yaml_file_writer(self, monkeypatch, tmp_path):
assert len(list(tmp_path.iterdir())) == 1


class TestYAMLWriterSavePath:
def test_yaml_file_writer_save_path(self, monkeypatch):
"""Test the YAML writer works correctly"""
with monkeypatch.context() as m:
m.setattr(sys, 'argv', ['', '--config',
'./tests/conf/yaml/test_save_path.yaml'])
config = ConfigArgBuilder(TypeConfig, NestedStuff, NestedListStuff, TypeOptConfig, desc='Test Builder')
# Test the chained version
config_values = config.save(file_extension='.yaml', file_name='pytest').generate()
check_path = str(config_values.TypeConfig.save_path) + '/pytest.spock.cfg.yaml'
fname = glob.glob(check_path)[0]
with open(fname, 'r') as fin:
print(fin.read())
assert os.path.exists(check_path)
# Clean up if assert is good
if os.path.exists(check_path):
os.remove(check_path)


class TestYAMLWriterNoPath:
def test_yaml_file_writer_no_path(self, monkeypatch):
"""Test the YAML writer works correctly"""
with monkeypatch.context() as m:
m.setattr(sys, 'argv', ['', '--config',
'./tests/conf/yaml/test.yaml'])
with pytest.raises(ValueError):
config = ConfigArgBuilder(TypeConfig, NestedStuff, NestedListStuff, TypeOptConfig, desc='Test Builder')
# Test the chained version
config.save(file_extension='.yaml', file_name='pytest').generate()


class TestWritePathRaise:
def test_yaml_file_writer(self, monkeypatch, tmp_path):
"""Test the YAML writer works correctly"""
"""Test the YAML writer fails correctly when create path isn't set"""
with monkeypatch.context() as m:
m.setattr(sys, 'argv', ['', '--config',
'./tests/conf/yaml/test.yaml'])
Expand Down
4 changes: 4 additions & 0 deletions tests/conf/yaml/test_class_incorrect.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# include another file
config: [test.yaml]
TypeConfig:
not_real: 10
7 changes: 7 additions & 0 deletions tests/conf/yaml/test_incorrect_repeated_class.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# include another file
config: [test.yaml]
NestedListStuff:
- foo: 10
two: hello
- foo: 20
two: bye
4 changes: 4 additions & 0 deletions tests/conf/yaml/test_missing_class.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# include another file
config: [test.yaml]
MissingClass:
not_real: 10
3 changes: 3 additions & 0 deletions tests/conf/yaml/test_save_path.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# include another file
config: [test.yaml]
save_path: /tmp
4 changes: 4 additions & 0 deletions tests/conf/yaml/test_wrong_class_enum.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# include another file
config: [test.yaml]
# Class Enum
class_enum: WhoAmI
10 changes: 7 additions & 3 deletions tests/debug/debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from spock.builder import ConfigArgBuilder
from spock.config import isinstance_spock
from params.first import Test
from params.first import NestedListStuff, Stuff, OtherStuff
from params.first import NestedListStuff
from spock.backend.attr.typed import SavePath
import pickle
from argparse import Namespace
Expand Down Expand Up @@ -100,11 +100,15 @@
# ccccombo_breaker: int


class Hello:
new: int


def main():
attrs_class = ConfigArgBuilder(
Test, NestedListStuff,
Test, NestedListStuff, ["names"],
desc='I am a description'
).save(user_specified_path='/tmp').generate()
).save(user_specified_path='/tmp').generate(unclass=True)
# with open('/tmp/debug.pickle', 'wb') as fid:
# pickle.dump(attrs_class, file=fid)

Expand Down

0 comments on commit 50c824c

Please sign in to comment.