Beispiel #1
0
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'
Beispiel #2
0
    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
Beispiel #3
0
    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
Beispiel #4
0
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)
Beispiel #5
0
class SampleC(DataObject):
    _restrictions = {
        'v': [1, 2, 3],
        'w': R.NULL_FLOAT,
        'x': R(SampleB, type(None)),
        'y': SampleA,
        'z': MgdRest(),
        }
Beispiel #6
0
 class FailsIntDefault(DataObject):
     _restrictions = {'int_default': R(default=int)}
Beispiel #7
0
 def test_r_creation(self, args, kwargs):
     assert R(*args, **kwargs)
Beispiel #8
0
 class FailsMixed(DataObject):
     _restrictions = {'mixed': R(int, 1, 2)}
Beispiel #9
0
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
Beispiel #10
0
 class B(DataObject):
     _restrictions = {
         'datetime': R.DATETIME,
         'date': R.DATE,
         'default': R()
     }
Beispiel #11
0
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)
Beispiel #12
0
 class B(DataObject):
     _restrictions = {
         'x': R(1, 2),
         'y': R.INT.with_default(100),
     }
Beispiel #13
0
 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)
Beispiel #14
0
class A(DataObject):
    _restrictions = {'id': R.INT, 'name': R.STR, 'status': R(0, 1, 2)}

    @classmethod
    def create(cls, **kwargs):
        return cls(data=kwargs)
Beispiel #15
0
class MgdRest(ManagedRestrictions):
    _restriction = R()

    def manage(self):
        if self.data == 'bad':
            raise RestrictionError.bad_data(self.data, '')
Beispiel #16
0
 class Singletons(DataObject):
     _restrictions = {
         'v': R(1, 2, 3),
         'x': R(SampleB, type(None)),
         'y': SampleA
         }
Beispiel #17
0
 def test_restriction_error(self, allowed, default):
     assert R(*allowed, default=default)