diff --git a/CHANGES.rst b/CHANGES.rst index 86990da..3facae8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,6 +11,10 @@ Document the intended API, ``_ext_replacement()``. See `issue 73 `_. +- Make ``AbstractDynamicObjectIO._ext_getattr`` handle a default + value, and add ``_ext_replacement_getattr`` for when it will only + be called once. See `issue 73 + `_. 1.0.0a3 (2018-07-28) ==================== diff --git a/src/nti/externalization/_datastructures.pxd b/src/nti/externalization/_datastructures.pxd index e15e03c..59b8479 100644 --- a/src/nti/externalization/_datastructures.pxd +++ b/src/nti/externalization/_datastructures.pxd @@ -42,7 +42,8 @@ cdef class AbstractDynamicObjectIO(ExternalizableDictionaryMixin): cpdef _ext_all_possible_keys(self) cpdef _ext_setattr(self, ext_self, k, value) - cpdef _ext_getattr(self, ext_self, k) + cpdef _ext_getattr(self, ext_self, k, default=*) + cpdef _ext_replacement_getattr(self, k, default=*) @cython.locals( k=str # cython can optimize k.startswith('constantstring') diff --git a/src/nti/externalization/datastructures.py b/src/nti/externalization/datastructures.py index c729669..55e6f8e 100644 --- a/src/nti/externalization/datastructures.py +++ b/src/nti/externalization/datastructures.py @@ -159,13 +159,28 @@ def _ext_all_possible_keys(self): def _ext_setattr(self, ext_self, k, value): raise NotImplementedError() - def _ext_getattr(self, ext_self, k): + def _ext_getattr(self, ext_self, k, default=NotGiven): """ - Return the attribute of the `ext_self` object with the external name `k`. - If the attribute does not exist, should raise (typically :exc:`AttributeError`) + _ext_getattr(object, name[, default]) -> value + + Return the attribute of the *ext_self* object with the internal name *name*. + If the attribute does not exist, should raise (typically :exc:`AttributeError`), + unless *default* is given, in which case it returns that. + + .. versionchanged:: 1.0a4 + Add the *default* argument. """ raise NotImplementedError() + def _ext_replacement_getattr(self, name, default=NotGiven): + """ + Like `_ext_getattr`, but automatically fills in `_ext_replacement` + for the *ext_self* argument. + + .. versionadded:: 1.0a4 + """ + return self._ext_getattr(self._ext_replacement(), name, default) + def _ext_keys(self): """ Return only the names of attributes that should be externalized. @@ -320,11 +335,13 @@ class ExternalizableInstanceDict(AbstractDynamicObjectIO): def _ext_all_possible_keys(self): return frozenset(self._ext_replacement().__dict__.keys()) - def _ext_getattr(self, ext_self, k): - return getattr(ext_self, k) + def _ext_getattr(self, ext_self, k, default=NotGiven): + if default is NotGiven: + return getattr(ext_self, k) + return getattr(ext_self, k, default) - def _ext_setattr(self, ext_self, k, v): - setattr(ext_self, k, v) + def _ext_setattr(self, ext_self, k, value): + setattr(ext_self, k, value) def _ext_accept_update_key(self, k, ext_self, ext_keys): return ( @@ -450,9 +467,11 @@ def _ext_all_possible_keys(self): ]) return cache.ext_all_possible_keys - def _ext_getattr(self, ext_self, k): + def _ext_getattr(self, ext_self, k, default=NotGiven): # TODO: Should this be directed through IField.get? - return getattr(ext_self, k) + if default is NotGiven: + return getattr(ext_self, k) + return getattr(ext_self, k, default) def _ext_setattr(self, ext_self, k, value): validate_named_field_value(ext_self, self._iface, k, value)() diff --git a/src/nti/externalization/tests/test_datastructures.py b/src/nti/externalization/tests/test_datastructures.py index 2907e66..36b390d 100644 --- a/src/nti/externalization/tests/test_datastructures.py +++ b/src/nti/externalization/tests/test_datastructures.py @@ -28,9 +28,31 @@ # pylint: disable=inherit-non-class,attribute-defined-outside-init,abstract-method # pylint:disable=no-value-for-parameter,too-many-function-args -class TestAbstractDynamicObjectIO(ExternalizationLayerTest): +class CommonTestMixins(object): - def _makeOne(self): + def _makeOne(self, context=None): + raise NotImplementedError() + + def test_ext_getattr_default(self): + io = self._makeOne() + assert_that(io._ext_getattr(self, 'no_such_attribute', None), + is_(none())) + + def test_ext_getattr_no_default(self): + io = self._makeOne() + get = io._ext_getattr + with self.assertRaises(AttributeError): + get(self, 'no_such_attribute') + + def test_ext_replacement_getattr_default(self): + io = self._makeOne() + assert_that(io._ext_replacement_getattr('no_such_attribute', None), + is_(none())) + +class TestAbstractDynamicObjectIO(CommonTestMixins, + ExternalizationLayerTest): + + def _makeOne(self, context=None): from nti.externalization.datastructures import AbstractDynamicObjectIO class IO(AbstractDynamicObjectIO): _ext_setattr = staticmethod(setattr) @@ -139,14 +161,15 @@ def test_update_takes_external_fields(self): assert_that(inst, has_property('id', 'id')) class TestInterfaceObjectIO(CleanUp, + CommonTestMixins, unittest.TestCase): def _getTargetClass(self): from nti.externalization.datastructures import InterfaceObjectIO return InterfaceObjectIO - def _makeOne(self, ext_self, *args, **kwargs): - return self._getTargetClass()(ext_self, *args, **kwargs) + def _makeOne(self, context=None, iface_upper_bound=interface.Interface): + return self._getTargetClass()(context, iface_upper_bound=iface_upper_bound) def test_find_primitive_keys_dne(self): ext_self = self @@ -479,3 +502,15 @@ class Consistent(object): io = IO(Consistent()) assert_that(io, has_property('_iface', IGrandChild)) + + +class TestExternalizableInstanceDict(CommonTestMixins, + unittest.TestCase): + + def _makeOne(self, context=None): + from nti.externalization.datastructures import ExternalizableInstanceDict + class FUT(ExternalizableInstanceDict): + def _ext_replacement(self): + return context + + return FUT()