class A(Validator): _restrictions = { 'name': Name(), 'age': Age(), 'city': R(*[e for e in it.chain.from_iterable(city_state.values())]), 'state': R(*city_state.keys()) } def _validate(self): assert self.city in city_state[self.state], 'Mismatched city and state'
def test_restrictions_runtime(self, d, strict, key): restric = {'id': R(0, 1, 2), 'x': R.INT.with_default(1), 'y': R()} class B(DataObject): _restrictions = restric data_ = {'id': 0, 'x': 2, 'y': 'hi'} if key == 'extra': data_['z'] = None elif key == 'missing': del data_['x'] if not d: data_ = None b = B(data=data_, strict=strict) assert b
def test_deep_restriction(self, deep): restric = {'id': [0, 1, 2], 'x': R.INT.with_default(1), 'y': []} if deep: restric['deep'] = {'this': [], 'fails': R(1, 2, 3, default=1)} class B(DataObject): _restrictions = restric
class ManagedList(ManagedRestrictions): """ Use this when you need a restriction for a list of DataObject's. """ _restriction = R(list, type(None)) def __init__(self, obj_cls, nullable=False): """ :param obj_cls: The DO to check each value in the list against. :type obj_cls: DataObject :param nullable: Valid values are a list of Do's or a NoneType. :type nullable: bool """ super(ManagedList, self).__init__() self.obj_cls = obj_cls self.nullable = nullable def manage(self): if self.data is not None: items = [] for item in self.data: items.append(item if type(item) == self.obj_cls else self.obj_cls(item)) self.data = items else: if not self.nullable: raise RestrictionError.bad_data(self.data, self._restriction.allowed)
class SampleC(DataObject): _restrictions = { 'v': [1, 2, 3], 'w': R.NULL_FLOAT, 'x': R(SampleB, type(None)), 'y': SampleA, 'z': MgdRest(), }
class FailsIntDefault(DataObject): _restrictions = {'int_default': R(default=int)}
def test_r_creation(self, args, kwargs): assert R(*args, **kwargs)
class FailsMixed(DataObject): _restrictions = {'mixed': R(int, 1, 2)}
class TestDataObject(object): @pytest.mark.parametrize('id, name, status', data) def test_init(self, id, name, status): a = A.create(id=id, name=name, status=status) assert a assert a.id == id assert a.name == name assert a.status == status assert a['id'] == id assert a['name'] == name assert a['status'] == status assert a(data=a) def test_class_namespace(self): try: class B(DataObject): _restrictions = {'x': R.INT.with_default(1)} x = None B(data={'x': 1}) raise Exception( 'Failed to protect namespace clash between _restrictions and cls.x!' ) except AttributeError: assert True except Exception as e: assert False, str(e) @pytest.mark.parametrize('deep', [ pytest.param( True, marks=pytest.mark.xfail(raises=DataObjectError), id='deep'), pytest.param(False, id='!deep') ]) def test_deep_restriction(self, deep): restric = {'id': [0, 1, 2], 'x': R.INT.with_default(1), 'y': []} if deep: restric['deep'] = {'this': [], 'fails': R(1, 2, 3, default=1)} class B(DataObject): _restrictions = restric @pytest.mark.xfail(raises=DataObjectError) def test_malformed_restrictions(self): class FailsMalformed(DataObject): _restrictions = {'malformed': None} @pytest.mark.xfail(raises=RestrictionError) def test_mixed_restrictions(self): class FailsMixed(DataObject): _restrictions = {'mixed': R(int, 1, 2)} @pytest.mark.parametrize('restriction', [[bool], ([bool], None)]) @pytest.mark.xfail(raises=DataObjectError) def test_legacy_restrictions(self, restriction): class FailsLegacy(DataObject): _restrictions = {'legacy': restriction} @pytest.mark.xfail(raises=RestrictionError) def test_int_default(self): class FailsIntDefault(DataObject): _restrictions = {'int_default': R(default=int)} @pytest.mark.parametrize('d, strict, key', [ pytest.param(True, True, 'extra', marks=pytest.mark.xfail(raises=DataObjectError), id='d-strict-extra'), pytest.param(True, True, 'missing', marks=pytest.mark.xfail(raises=DataObjectError), id='d-strict-missing'), pytest.param(True, True, None, id='d-strict-None'), pytest.param(True, False, 'extra', marks=pytest.mark.xfail(raises=DataObjectError), id='d-!strict-extra'), pytest.param(True, False, 'missing', id='d-!strict-missing'), pytest.param(True, False, None, id='d-!strict-None'), pytest.param(False, True, None, marks=pytest.mark.xfail(raises=DataObjectError), id='!d-strict-None'), pytest.param(False, False, None, id='!d-!strict-None') ]) def test_restrictions_runtime(self, d, strict, key): restric = {'id': R(0, 1, 2), 'x': R.INT.with_default(1), 'y': R()} class B(DataObject): _restrictions = restric data_ = {'id': 0, 'x': 2, 'y': 'hi'} if key == 'extra': data_['z'] = None elif key == 'missing': del data_['x'] if not d: data_ = None b = B(data=data_, strict=strict) assert b def test_nested_restrictions(self): class B(DataObject): _restrictions = { 'x': R(1, 2), 'y': R.INT.with_default(100), } class C(DataObject): _restrictions = {'a': A, 'b': B} data_ = { 'a': { 'id': 1, 'name': 'evil-jenkins', 'status': 0 }, 'b': { 'x': 1, 'y': 23 } } c = C(data=data_) assert c assert c.get('a') == c['a'] == c.a assert type(c.a) is A assert c.a.id assert type(c.b) is B assert c.b.x # Test nested validation try: c.b.x = 'invalid' raise MyTestException('Invalid value assigned to c.b.x!') except MyTestException as e: assert False, str(e) except Exception: assert True try: c.b = {'invalid': 'values'} raise MyTestException('Invalid data dict assigned to c.b!') except MyTestException as e: assert False, str(e) except Exception: assert True # Test default value behavior c_default = C(strict=False) assert c_default assert c_default.a assert type(c_default.a) is A assert type(c_default.b) is B for k, v in c_default.a.items(): assert v is None, [(k, v) for k, v in c_default.a.items()] @pytest.mark.parametrize( 'restrictions', [pytest.param(R(A, type(None)), id='([A, type(None)], None)'), A]) def test_supported_nested_restrictions_format(self, restrictions): class B(DataObject): _restrictions = {'a': restrictions} class C(DataObject): _restrictions = {'b': B} c = C(data={ 'b': { 'a': A(data={ 'id': 1, 'name': 'evil-jenkins', 'status': 0 }) } }) assert c assert c.b assert c.b.a assert type(c.b.a) is A @pytest.mark.parametrize('restrictions', [ pytest.param( (A, None), marks=pytest.mark.xfail(reason="'None' data not allowed for DO"), id='(A, None)'), pytest.param(A, marks=pytest.mark.xfail) ]) def test_null_nested_object(self, restrictions): class B(DataObject): _restrictions = {'a': restrictions} b = B(data={'a': None}) assert b def test_missing_restrictions(self): try: class B(DataObject): pass B() raise MyTestException('Error should have thrown.') except MyTestException as e: assert False, str(e) except Exception: assert True def test_nesting_dict_restrictions(self): try: class B(DataObject): _restrictions = {'a': {'x': [], 'y': []}} B(data={'a': {'x': 1, 'y': 2}}) raise MyTestException('Error should have thrown.') except MyTestException as e: assert False, str(e) except Exception: assert True @pytest.mark.parametrize('id, name, status', short_data) def test_setitem(self, id, name, status): a = A.create(id=id, name=name, status=status) new_id = 10 a.id = new_id assert not our_hasattr( a, 'id'), 'Restricted key should not be in attribute space' assert a['id'] == new_id assert a.id == new_id newer_id = 11 a['id'] = newer_id assert a['id'] == newer_id assert a.id == newer_id try: a['invalid'] = 'something' raise MyTestException( 'Able to assign a value to an unrestricted key!') except MyTestException as e: assert False, str(e) except Exception: assert True # Attribute space can be freeform, but will not become part of restricted data schema a.invalid = 'something' assert our_hasattr(a, 'invalid'), 'Attribute not found' assert 'invalid' not in a assert a.invalid == 'something' try: _ = a['invalid'] raise MyTestException( 'Able to pull out value set in attribute namespace from keyspace!' ) except MyTestException as e: assert False, str(e) except Exception: assert True @pytest.mark.parametrize('id, name, status', short_data) def test_get(self, id, name, status): a = A.create(id=id, name=name, status=status) assert a.get('id') == a.id == id assert a.get('name') == a.name == name assert a.get('status') == a.status == status try: _ = a['nope'] assert False except KeyError: assert True try: _ = a.nope assert False except AttributeError: assert True @pytest.mark.parametrize('id, name, status', short_data) @pytest.mark.parametrize('key', keys) def test_get_2(self, id, name, status, key): a = A.create(id=id, name=name, status=status) assert a.get(key) is not None @pytest.mark.parametrize('id, name, status', short_data) def test_clear_pop(self, id, name, status): a = A.create(id=id, name=name, status=status) try: a.clear() assert False except TypeError: assert True try: a.pop('id') assert False except TypeError: assert True try: a.popitem() assert False except TypeError: assert True try: del a['id'] assert False except TypeError: assert True try: a.update({'id': 1}) assert False except TypeError: assert True @pytest.mark.parametrize( 'complex', [pytest.param(True, marks=pytest.mark.xfail), False]) def test_str_repr(self, complex): from datetime import date, datetime class B(DataObject): _restrictions = { 'datetime': R.DATETIME, 'date': R.DATE, 'default': R() } class MyObj(dict): pass a = B( data={ 'datetime': datetime.now(), 'date': date.today(), 'default': MyObj if complex else 'hello world' }) # __repr__ returns JSON assert json.loads('%r' % a) # __str__ returns string assert '%s' % a @pytest.mark.parametrize('d, strict', [ ('valid', True), pytest.param( 'invalid', True, marks=pytest.mark.xfail(reason='Data does not meet restrictions')), pytest.param( None, True, marks=pytest.mark.xfail(reason='Data does not meet restrictions')), pytest.param('partial', True, marks=pytest.mark.xfail( reason='Partial data not allowed when strict.')), ('valid', False), pytest.param( 'invalid', False, marks=pytest.mark.xfail(reason='Data does not meet restrictions')), (None, False), ('partial', False) ]) def test_strict(self, d, strict): if d == 'valid': d = { 'id': short_data[0][0], 'name': short_data[0][1], 'status': short_data[0][2] } elif d == 'invalid': d = {'id': None, 'name': None, 'status': None} elif d == 'partial': d = {'id': 1} a = A(data=d, strict=strict) assert a, '__init__ failed!' assert a(data=d, strict=strict), '__call__ failed!' @pytest.mark.parametrize('id, name, status', short_data) def test_attr_restr_mutually_exclusive(self, id, name, status): """ Restriction keys should not be present in attr space. Not key attributes should live in attribute space. :return: :rtype: """ a = A.create(id=id, name=name, status=status) assert not any([our_hasattr(a, e) for e in A._restrictions.keys()]) assert all([e in a for e in A._restrictions.keys()]) a.x = 'x' a.y = 'y' attributes = ['x', 'y'] assert all([our_hasattr(a, e) for e in attributes]) assert not any([e in a for e in attributes]) def test_multiple_dataobjs_not_allowed(self): class First(DataObject): _restrictions = {'id': R.INT} class Second(DataObject): _restrictions = {'id': R.INT} try: type('Mixed', (DataObject, ), { '_restrictions': { 'id': [First, Second] }, '__module__': 'pytest' }) raise MyTestException( 'Mixed Data Objects should not be allowed in restrictions') except DataObjectError: assert True @pytest.mark.parametrize('id, name, status', short_data) def test_dir(self, id, name, status): inst = A.create(id=id, name=name, status=status) for k in A._restrictions: assert k in dir(inst) def test_schema(self): schema = A.schema for k in A._restrictions: assert k in schema
class B(DataObject): _restrictions = { 'datetime': R.DATETIME, 'date': R.DATE, 'default': R() }
class MgdDatetime(ManagedRestrictions): """ Managed from and to date/datetime restrictions. It follows the following logic: From To None Epoch date(time) Epoch date(time) Valid Datetime/Date instance Do nothing Do nothing Everything else Parse as ISO date(time) Parse as ISO date(time) Note: The default value will not be set properly in `strict=False` initializations that are missing the relevant key. This is due to strictness impacting ManagedRestrictions execution; the `manage` method does not execute on `strict=False`. Since this implementation sets the default in this method, the abstraction fails to Example: class A(DataObject): _restrictions = { 'from_date': MgdDatetime.from_from_date() } A(strict=False) # {"from_date": null} A(data={'from_date': None}, strict=False) # {"from_date": "1969-12-31"} A({'from_date': None}) # {"from_date": "1969-12-31"} """ dt_obj = None _restriction = R() _parse_dt_fmt = {datetime: '%Y-%m-%dT%H:%M:%S', date: '%Y-%m-%d'} defaults = { 'from': lambda dt: dt.fromtimestamp(0), 'to': lambda dt: dt.now() if dt is datetime else dt.today() } def __init__(self, dt_obj=None, default_key=None, nullable=False, *args, **kwargs): """ :param dt_obj: Initialize the restriction as date or datetime. :type dt_obj: Type[Union[datetime, date]] :param default_key: Manages "from" or "to" :type default_key: str :type nullable: bool """ assert dt_obj in self._parse_dt_fmt, 'Invalid "dt_obj"(=%s)' % dt_obj assert default_key is None or default_key in self.defaults, 'Invalid "default_key"(=%s)' % default_key self.dt_obj = dt_obj self.default_key = default_key self.nullable = nullable if self.dt_obj is datetime: self._restriction = R.NULL_DATETIME if self.nullable else R.DATETIME else: self._restriction = R.NULL_DATE if self.nullable else R.DATE super(MgdDatetime, self).__init__(*args, **kwargs) def manage(self): """ Implements the logic outlined in class docstring. Uses datetime.strptime by design to be more strict on the string parsing for ISO format. """ if self.data is None: if self.default_key: self.data = self.defaults[self.default_key](self.dt_obj) elif type(self.data) not in [datetime, date]: self.data = datetime.strptime(self.data, self._parse_dt_fmt[self.dt_obj]) if self.dt_obj is date: self.data = self.data.date() self._restriction(self.data) if self.data is not None and self.dt_obj is datetime: self.data = self.data.replace(microsecond=0) @classmethod def from_from_date(cls): """ Create a MgdDatetime instance for validating a `from_date` restriction. This will validate a DATE format, not DATETIME. :rtype: MgdDatetime """ return cls(dt_obj=date, default_key='from') @classmethod def from_to_date(cls): """ Create a MgdDatetime instance for validating a `to_date` restriction. This will validate a DATE format, not DATETIME. :rtype: MgdDatetime """ return cls(dt_obj=date, default_key='to') @classmethod def from_from_datetime(cls): """ Create a MgdDatetime instance for validating a `from_datetime` restriction. This will validate a DATETIME format, not DATE. :rtype: MgdDatetime """ return cls(dt_obj=datetime, default_key='from') @classmethod def from_to_datetime(cls): """ Create a MgdDatetime instance for validating a `to_datetime` This will validate a DATETIME format, not DATE. :rtype: MgdDatetime """ return cls(dt_obj=datetime, default_key='to') @classmethod def datetime(cls): """ Create MgdDatetime instance for validating and standardizing a DATETIME format. :rtype: MgdDatetime """ return cls(dt_obj=datetime) @classmethod def null_datetime(cls): """ Create MgdDatetime instance for validating and standardizing a DATETIME format that is nullable. :rtype: MgdDatetime """ return cls(dt_obj=datetime, nullable=True) @classmethod def date(cls): """ Create MgdDatetime instance for validating and standardizing a DATE format. :rtype: MgdDate """ return cls(dt_obj=date) @classmethod def null_date(cls): """ Create MgdDatetime instance for validating and standardizing a DATE format that is nullable. :rtype: MgdDate """ return cls(dt_obj=date, nullable=True)
class B(DataObject): _restrictions = { 'x': R(1, 2), 'y': R.INT.with_default(100), }
def test_init_and_caching(self, allowed, default): instance_1 = R(allowed, default=default) assert instance_1 instance_2 = R(allowed, default=default) assert id(instance_1) == id(instance_2)
class A(DataObject): _restrictions = {'id': R.INT, 'name': R.STR, 'status': R(0, 1, 2)} @classmethod def create(cls, **kwargs): return cls(data=kwargs)
class MgdRest(ManagedRestrictions): _restriction = R() def manage(self): if self.data == 'bad': raise RestrictionError.bad_data(self.data, '')
class Singletons(DataObject): _restrictions = { 'v': R(1, 2, 3), 'x': R(SampleB, type(None)), 'y': SampleA }
def test_restriction_error(self, allowed, default): assert R(*allowed, default=default)