diff --git a/README.md b/README.md index 56bd976..af3d7cb 100644 --- a/README.md +++ b/README.md @@ -343,6 +343,24 @@ A DSVError is raised with 3 errors in args. """ ``` +--- +### Feature: Self-defined Spec name in error message +```python +from data_spec_validator.spec import Checker, dsv_feature, validate_data_spec, INT + +@dsv_feature(spec_name='CustomSpecName') +class _MySpec: + a = Checker([INT]) + +nok_data = dict( + a='abc', +) + +validate_data_spec(nok_data, _MySpec) +""" +TypeError: field: CustomSpecName.a, reason: 'abc' is not an integer +""" +``` --- ## Test diff --git a/data_spec_validator/spec/defines.py b/data_spec_validator/spec/defines.py index 9bf8ee6..ffd0dc4 100644 --- a/data_spec_validator/spec/defines.py +++ b/data_spec_validator/spec/defines.py @@ -76,8 +76,10 @@ def __str__(self, *args, **kwargs): class ValidateResult: def __init__(self, spec: Type = None, field: str = None, value: Any = None, check: str = None, error=None): + from .features import get_spec_name # FIXME: refine the structure to avoid circular import + # TODO: Output spec & check information when there's a debug message level for development. - self.__spec = spec.__name__ if spec else None + self.__spec = get_spec_name(spec) if spec else None self.__field = field self.__value = value self.__check = check diff --git a/data_spec_validator/spec/features.py b/data_spec_validator/spec/features.py index 4f90402..80524a7 100644 --- a/data_spec_validator/spec/features.py +++ b/data_spec_validator/spec/features.py @@ -5,12 +5,13 @@ class _DSVFeatureParams: - __slots__ = ('_strict', '_any_keys_set', '_err_mode') + __slots__ = ('_strict', '_any_keys_set', '_err_mode', '_spec_name') - def __init__(self, strict, any_keys_set: Union[Set[Tuple[str, ...]], None], err_mode): + def __init__(self, strict, any_keys_set: Union[Set[Tuple[str, ...]], None], err_mode, spec_name): self._strict = strict self._any_keys_set = any_keys_set or set() self._err_mode = err_mode + self._spec_name = spec_name @property def err_mode(self) -> ErrorMode: @@ -24,30 +25,45 @@ def strict(self) -> bool: def any_keys_set(self) -> set: return self._any_keys_set + @property + def spec_name(self) -> str: + return self._spec_name + def __repr__(self): - return f'_DSVFeatureParams(strict={self._strict}, any_keys_set={self._any_keys_set}, err_mode={self._err_mode})' + return f'_DSVFeatureParams(strict={self._strict}, any_keys_set={self._any_keys_set}, err_mode={self._err_mode}), spec_name={self._spec_name}' _FEAT_PARAMS = '__feat_params__' def _process_class( - cls: Type, strict: bool, any_keys_set: Union[Set[Tuple[str, ...]], None], err_mode: ErrorMode + cls: Type, + strict: bool, + any_keys_set: Union[Set[Tuple[str, ...]], None], + err_mode: ErrorMode, + spec_name: Optional[str], ) -> Type: - setattr(cls, _FEAT_PARAMS, _DSVFeatureParams(strict, any_keys_set, err_mode)) + setattr(cls, _FEAT_PARAMS, _DSVFeatureParams(strict, any_keys_set, err_mode, spec_name)) return cls def dsv_feature( - strict: bool = False, any_keys_set: Optional[Set[Tuple[str, ...]]] = None, err_mode=ErrorMode.MSE + strict: bool = False, + any_keys_set: Optional[Set[Tuple[str, ...]]] = None, + err_mode=ErrorMode.MSE, + spec_name: Optional[str] = None, ) -> Callable: def wrap(cls: Type) -> Type: - return _process_class(cls, strict, any_keys_set, err_mode) + return _process_class(cls, strict, any_keys_set, err_mode, spec_name) return wrap +def get_spec_name(spec) -> str: + return getattr(spec, _FEAT_PARAMS).spec_name if hasattr(spec, _FEAT_PARAMS) else spec.__name__ + + def get_err_mode(spec) -> ErrorMode: feat_params: Union[_DSVFeatureParams, None] = getattr(spec, _FEAT_PARAMS, None) return feat_params.err_mode if feat_params else ErrorMode.MSE diff --git a/test/test_nested_spec.py b/test/test_nested_spec.py index d43d340..5138a31 100644 --- a/test/test_nested_spec.py +++ b/test/test_nested_spec.py @@ -1,6 +1,7 @@ import unittest -from data_spec_validator.spec import BOOL, DICT, DIGIT_STR, FLOAT, INT, NONE, SPEC, STR, Checker, validate_data_spec +from data_spec_validator.spec import BOOL, DICT, DIGIT_STR, FLOAT, INT, NONE, SPEC, STR, Checker, validate_data_spec, \ + dsv_feature from .utils import is_something_error @@ -61,3 +62,28 @@ class ChildSpec3: c3_f=[], ) assert is_something_error(TypeError, validate_data_spec, nok_data, NestedSpec) + + def test_nested_error_field_name(self): + class NestedSpec: + class ChildSpec1: + @dsv_feature(spec_name='NestedSpec.c1_f.s_1') + class ChildSpec11: + f_11 = Checker([FLOAT]) + + s_1 = Checker([SPEC], SPEC=ChildSpec11) + + c1_f = Checker([SPEC], SPEC=ChildSpec1) + + nok_data = dict( + c1_f=dict( + s_1=dict( + f_11=None, + ), + ), + ) + + with self.assertRaises(TypeError) as exc_info: + validate_data_spec(nok_data, NestedSpec) + + exc_msg = str(exc_info.exception) + self.assertEqual(exc_msg, 'field: NestedSpec.c1_f.s_1.f_11, reason: None is not a float')