Skip to content

Commit

Permalink
Merge pull request #78 from NextThought/eid-persistent-allowed
Browse files Browse the repository at this point in the history
Make ExternalizableInstanceDict subclassable along with Persistent.
  • Loading branch information
jamadden committed Jul 30, 2018
2 parents 5c6e755 + 441711d commit 3df10e3
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 15 deletions.
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

- Nothing changed yet.

- ``ExternalizableInstanceDict`` no longer inherits from
``AbstractDynamicIO``, it just implements the same interface (with
the exception of many of the ``_ext`` methods). This class is deprecated.

1.0.0a4 (2018-07-30)
====================
Expand Down
2 changes: 2 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,12 +192,14 @@
'http://zopecontainer.readthedocs.io/en/latest': None,
'http://zopedatetime.readthedocs.io/en/latest': None,
'http://zopedublincore.readthedocs.io/en/latest': None,
'http://zopeevent.readthedocs.io/en/latest': None,
'http://zopehookable.readthedocs.io/en/latest': None,
'http://zopeinterface.readthedocs.io/en/latest': None,
'http://zopeintid.readthedocs.io/en/latest/': None,
'http://zopemimetype.readthedocs.io/en/latest/': None,
'http://zopeproxy.readthedocs.io/en/latest': None,
'http://zopeschema.readthedocs.io/en/latest/': None,
'http://zopelifecycleevent.readthedocs.io/en/latest/': None,
}

extlinks = {
Expand Down
10 changes: 8 additions & 2 deletions src/nti/externalization/_datastructures.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,15 @@ cdef class AbstractDynamicObjectIO(ExternalizableDictionaryMixin):
cpdef find_factory_for_named_value(self, key, value, registry)
cdef _updateFromExternalObject(self, parsed)

# cdef class _ExternalizableInstanceDict(AbstractDynamicObjectIO):
# cdef dict __dict__

cdef class ExternalizableInstanceDict(AbstractDynamicObjectIO):
pass

# This class is sometimes subclassed while also subclassing persistent.Persistent,
# which doesn't work if it's an extension class, so we can't do that. It's rarely used,
# so performance doesn't matter as much.
#cdef class ExternalizableInstanceDict(AbstractDynamicObjectIO):
# pass

cdef class InterfaceObjectIO(AbstractDynamicObjectIO):
cdef readonly _ext_self
Expand Down
84 changes: 74 additions & 10 deletions src/nti/externalization/datastructures.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# There are a *lot* of fixme (XXX and the like) in this file.
# Turn those off in general so we can see through the noise.
# pylint:disable=fixme

# pylint:disable=keyword-arg-before-vararg

# stdlib imports
import numbers
Expand All @@ -26,6 +26,7 @@

from nti.schema.interfaces import find_most_derived_interface

from .interfaces import IInternalObjectIO
from .interfaces import IInternalObjectIOFinder
from .interfaces import IAnonymousObjectFactory
from .interfaces import StandardInternalFields
Expand Down Expand Up @@ -90,6 +91,11 @@ def _ext_standard_external_dictionary(self, replacement, mergeFrom=None, **kwarg
decorate_callback=kwargs.get('decorate_callback', NotGiven))

def toExternalDictionary(self, mergeFrom=None, *unused_args, **kwargs):
"""
Produce the standard external dictionary for this object.
Uses `_ext_replacement`.
"""
return self._ext_standard_external_dictionary(self._ext_replacement(),
mergeFrom=mergeFrom,
**kwargs)
Expand Down Expand Up @@ -317,21 +323,34 @@ def _updateFromExternalObject(self, parsed):
interface.classImplements(AbstractDynamicObjectIO, IInternalObjectIOFinder)


class ExternalizableInstanceDict(AbstractDynamicObjectIO):
"""
Externalizes to a dictionary containing the members of ``__dict__``
that do not start with an underscore.
Meant to be used as a super class; also can be used as an external object superclass.
"""
class _ExternalizableInstanceDict(AbstractDynamicObjectIO):

# TODO: there should be some better way to customize this if desired (an explicit list)
# TODO: Play well with __slots__? ZODB supports slots, but doesn't recommend them
# TODO: This won't evolve well. Need something more sophisticated,
# probably a meta class.

_update_accepts_type_attrs = False

def __init__(self, context):
self.context = context
for name in (
'_update_accepts_type_attrs',
'__external_use_minimal_base__',
'_excluded_in_ivars_',
'_excluded_out_ivars_',
'_ext_primitive_out_ivars_',
'_prefer_oid_'
):
try:
v = getattr(context, name)
except AttributeError:
continue
else:
setattr(self, name, v)

def _ext_replacement(self):
return self.context

def _ext_all_possible_keys(self):
return frozenset(self._ext_replacement().__dict__.keys())

Expand All @@ -345,13 +364,58 @@ def _ext_setattr(self, ext_self, k, value):

def _ext_accept_update_key(self, k, ext_self, ext_keys):
return (
super(ExternalizableInstanceDict, self)._ext_accept_update_key(k, ext_self, ext_keys)
super(_ExternalizableInstanceDict, self)._ext_accept_update_key(k, ext_self, ext_keys)
or (self._update_accepts_type_attrs and hasattr(ext_self, k))
)

__repr__ = make_repr()


class ExternalizableInstanceDict(object):
"""
Externalizes to a dictionary containing the members of
``__dict__`` that do not start with an underscore.
Meant to be used as a super class; also can be used as an external
object superclass.
.. versionchanged:: 1.0a5
No longer extends `AbstractDynamicObjectIO`, just delegates to it.
.. deprecated:: 1.0a5
Prefer interfaces.
"""
# pylint:disable=protected-access
_update_accepts_type_attrs = _ExternalizableInstanceDict._update_accepts_type_attrs
__external_use_minimal_base__ = _ExternalizableInstanceDict.__external_use_minimal_base__
_excluded_out_ivars_ = AbstractDynamicObjectIO._excluded_out_ivars_
_excluded_in_ivars_ = AbstractDynamicObjectIO._excluded_in_ivars_
_ext_primitive_out_ivars_ = AbstractDynamicObjectIO._ext_primitive_out_ivars_
_prefer_oid_ = AbstractDynamicObjectIO._prefer_oid_

def _ext_replacement(self):
"See `ExternalizableDictionaryMixin._ext_replacement`."
return self

def __make_io(self):
return _ExternalizableInstanceDict(self._ext_replacement())

def __getattr__(self, name):
# here if we didn't have the attribute. Does our IO?
return getattr(self.__make_io(), name)

def updateFromExternalObject(self, parsed, *unused_args, **unused_kwargs):
"See `~.IInternalObjectIO.updateFromExternalObject`"
self.__make_io().updateFromExternalObject(parsed)

def toExternalObject(self, mergeFrom=None, *args, **kwargs):
"See `~.IInternalObjectIO.toExternalObject`. Calls `toExternalDictionary`."
return self.toExternalDictionary(mergeFrom, *args, **kwargs)

def toExternalDictionary(self, mergeFrom=None, *unused_args, **kwargs):
"See `ExternalizableDictionaryMixin.toExternalDictionary`"
return self.__make_io().toExternalDictionary(mergeFrom)

interface.classImplements(ExternalizableInstanceDict, IInternalObjectIO)

_primitives = six.string_types + (numbers.Number, bool)

Expand Down
5 changes: 4 additions & 1 deletion src/nti/externalization/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,12 +398,15 @@ class IObjectModifiedFromExternalEvent(IObjectModifiedEvent):
"""
An object has been updated from an external value.
"""
kwargs = interface.Attribute("The key word arguments")
kwargs = interface.Attribute("The keyword arguments")
external_value = interface.Attribute("The external value")


@interface.implementer(IObjectModifiedFromExternalEvent)
class ObjectModifiedFromExternalEvent(ObjectModifiedEvent):
"""
Default implementation of `IObjectModifiedFromExternalEvent`.
"""

kwargs = None
external_value = None
Expand Down
16 changes: 14 additions & 2 deletions src/nti/externalization/internalization/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from __future__ import division
from __future__ import print_function

import warnings

from zope.interface.interfaces import ComponentLookupError

__all__ = [
Expand All @@ -20,7 +22,7 @@
'default_externalized_object_factory_finder_factory',
'find_factory_for_class_name',
'find_factory_for',
'notifyModified',
'notify_modified',
'validate_field_value',
'validate_named_field_value',
]
Expand All @@ -37,7 +39,7 @@
from .factories import find_factory_for_class_name
from .factories import find_factory_for

from .events import notifyModified
from .events import notifyModified as notify_modified

from .updater import update_from_external_object

Expand All @@ -64,3 +66,13 @@ def new_from_external_object(external_object, *args, **kwargs):
if factory is None:
raise ComponentLookupError("No factory for object", external_object)
return update_from_external_object(factory(), external_object, *args, **kwargs)


def notifyModified(*args, **kwargs):
"""
A deprecated alias of `notify_modified`.
.. deprecated:: 1.0a5
"""
warnings.warn("Use notify_modified instead", FutureWarning, stacklevel=2)
return notify_modified(*args, **kwargs)
25 changes: 25 additions & 0 deletions src/nti/externalization/internalization/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,31 @@ def _notifyModified(containedObject, externalObject, updater, external_keys,

def notifyModified(containedObject, externalObject, updater=None, external_keys=None,
**kwargs):
"""
Create and send an `~.ObjectModifiedFromExternalEvent` for
*containedObject* using `zope.event.notify`.
The *containedObject* is the subject of the event. The
*externalObject* is the dictionary of data that was used to update
the *containedObject*.
*external_keys* is list of keys from *externalObject* that
actually changed *containedObject*. If this is not given, we
assume that all keys in *externalObject* were changed. Note that
these should to correspond to the interface fields of interfaces
that the *containedObject* implements in order to properly be able
to create and populate the `zope.lifecycleevent` `~zope.lifecycleevent.IAttributes`.
*updater*, if given, is the `~nti.externalization.interfaces.IInternalObjectUpdater`
instance that was used to handle the updates. If this object implements
an ``_ext_adjust_modified_event`` method, it will be called to adjust (and return)
the event object that will be notified.
*kwargs* are the keyword arguments passed to the event constructor.
:return: The event object that was notified.
"""

return _notifyModified(containedObject, externalObject, updater, external_keys,
kwargs)

Expand Down
13 changes: 13 additions & 0 deletions src/nti/externalization/tests/test_datastructures.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,3 +514,16 @@ def _ext_replacement(self):
return context

return FUT()


def test_can_also_subclass_persistent(self):
from nti.externalization.datastructures import ExternalizableInstanceDict
from persistent import Persistent

class Base(ExternalizableInstanceDict):
pass

class P(Base, Persistent):
pass

self.assertIsNotNone(P)

0 comments on commit 3df10e3

Please sign in to comment.