Skip to content

Commit

Permalink
add field event, enable subscriber to be triggered after a zope schem…
Browse files Browse the repository at this point in the history
…a field has been updated
  • Loading branch information
jfroche committed Jan 17, 2014
1 parent 5a32427 commit d1e4d1b
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 9 deletions.
7 changes: 7 additions & 0 deletions src/zope/schema/_bootstrapinterfaces.py
Expand Up @@ -111,3 +111,10 @@ class IContextAwareDefaultFactory(zope.interface.Interface):

def __call__(context):
"""Returns a default value for the field."""


class NO_VALUE(object):
def __repr__(self):
return '<NO_VALUE>'

NO_VALUE = NO_VALUE()
25 changes: 25 additions & 0 deletions src/zope/schema/fieldproperty.py
Expand Up @@ -17,10 +17,24 @@
from copy import copy
import sys
import zope.schema
from zope import interface
from zope import event
from zope.schema import interfaces
from zope.schema._bootstrapinterfaces import NO_VALUE

_marker = object()


@interface.implementer(interfaces.IFieldUpdatedEvent)
class FieldUpdatedEvent(object):

def __init__(self, inst, field, old_value, new_value):
self.inst = inst
self.field = field
self.old_value = old_value
self.new_value = new_value


class FieldProperty(object):
"""Computed attributes based on schema fields
Expand Down Expand Up @@ -51,12 +65,21 @@ def __get__(self, inst, klass):

return value

def queryValue(self, inst, default):
value = inst.__dict__.get(self.__name, default)
if value is default:
field = self.__field.bind(inst)
value = getattr(field, 'default', default)
return value

def __set__(self, inst, value):
field = self.__field.bind(inst)
field.validate(value)
if field.readonly and self.__name in inst.__dict__:
raise ValueError(self.__name, 'field is readonly')
oldvalue = self.queryValue(inst, NO_VALUE)
inst.__dict__[self.__name] = value
event.notify(FieldUpdatedEvent(inst, field, oldvalue, value))

def __getattr__(self, name):
return getattr(self.__field, name)
Expand Down Expand Up @@ -122,4 +145,6 @@ def __set__(self, inst, value):
return
else:
raise ValueError(self.__name, 'field is readonly')
oldvalue = self.queryValue(inst, field, NO_VALUE)
self.setValue(inst, field, value)
event.notify(FieldUpdatedEvent(inst, self.field, oldvalue, value))
19 changes: 19 additions & 0 deletions src/zope/schema/interfaces.py
Expand Up @@ -743,3 +743,22 @@ class IVocabularyFactory(Interface):
def __call__(context):
"""The context provides a location that the vocabulary can make use of.
"""


class IFieldEvent(Interface):

field = Attribute("The field that has been changed")

object = Attribute("The object containing the field")


class IFieldUpdatedEvent(IFieldEvent):
"""
A field has been modified
Subscribers will get the old and the new value together with the field
"""

old_value = Attribute("The value of the field before modification")

new_value = Attribute("The value of the field after modification")
19 changes: 10 additions & 9 deletions src/zope/schema/tests/test__field.py
Expand Up @@ -1900,11 +1900,11 @@ class OK(object):
value = OK()
objf.set(inst, value)
self.assertEqual(inst.field is value, True)
self.assertEqual(len(log), 1)
self.assertEqual(IBeforeObjectAssignedEvent.providedBy(log[0]), True)
self.assertEqual(log[0].object, value)
self.assertEqual(log[0].name, 'field')
self.assertEqual(log[0].context, inst)
self.assertEqual(len(log), 5)
self.assertEqual(IBeforeObjectAssignedEvent.providedBy(log[-1]), True)
self.assertEqual(log[-1].object, value)
self.assertEqual(log[-1].name, 'field')
self.assertEqual(log[-1].context, inst)

def test_set_allows_IBOAE_subscr_to_replace_value(self):
from zope.event import subscribers
Expand Down Expand Up @@ -1934,12 +1934,13 @@ def _replace(event):
subscribers.append(_replace)
objf = self._makeOne(schema, __name__='field')
inst = DummyInstance()
self.assertEqual(len(log), 4)
objf.set(inst, ok1)
self.assertEqual(inst.field is ok2, True)
self.assertEqual(len(log), 1)
self.assertEqual(log[0].object, ok2)
self.assertEqual(log[0].name, 'field')
self.assertEqual(log[0].context, inst)
self.assertEqual(len(log), 5)
self.assertEqual(log[-1].object, ok2)
self.assertEqual(log[-1].name, 'field')
self.assertEqual(log[-1].context, inst)


class DictTests(unittest.TestCase):
Expand Down
149 changes: 149 additions & 0 deletions src/zope/schema/tests/test_fieldproperty.py
Expand Up @@ -111,6 +111,44 @@ def test_ctor_explicit(self):
self.assertEqual(prop.readonly, field.readonly)
self.assertEqual(prop.required, field.required)

def test_query_value_with_default(self):
from zope.schema import Text
from zope.schema._compat import u
field = Text(
__name__='testing',
description=u('DESCRIPTION'),
default=u('DEFAULT'),
readonly=True,
required=True,
)

prop = self._makeOne(field=field)

class Foo(object):
testing = prop
foo = Foo()
self.assertEqual(prop.queryValue(foo, 'test'), u('DEFAULT'))
foo.testing = u('NO')
self.assertEqual(prop.queryValue(foo, 'test'), u('NO'))

def test_query_value_without_default(self):
from zope.schema import Text
from zope.schema._compat import u
field = Text(
__name__='testing',
description=u('DESCRIPTION'),
readonly=True,
required=True,
)

prop = self._makeOne(field=field)

class Foo(object):
testing = prop
foo = Foo()
self.assertEqual(prop.queryValue(foo, 'test'), None)
# it should be NO_VALUE ...

def test___get___from_class(self):
prop = self._makeOne()

Expand Down Expand Up @@ -227,6 +265,60 @@ class Foo(object):
self.assertRaises(ValueError, setattr, foo, 'testing', '123')
self.assertEqual(_validated, ['123'])

def test_field_event(self):
from zope.schema import Text
from zope.schema._compat import u
from zope.event import subscribers
from zope.schema.fieldproperty import FieldUpdatedEvent
log = []
subscribers.append(log.append)
self.assertEqual(log, [])
field = Text(
__name__='testing',
description=u('DESCRIPTION'),
default=u('DEFAULT'),
readonly=True,
required=True,
)
self.assertEqual(len(log), 6)
event = log[0]
self.assertTrue(isinstance(event, FieldUpdatedEvent))
self.assertEqual(event.inst, field)
self.assertEqual(event.old_value, 0)
self.assertEqual(event.new_value, 0)
self.assertEqual(
[ev.field.__name__ for ev in log],
['min_length', 'max_length', 'title', 'description', 'required', 'readonly'])

def test_field_event_update(self):
from zope.schema import Text
from zope.schema._compat import u
from zope.event import subscribers
from zope.schema.fieldproperty import FieldUpdatedEvent
field = Text(
__name__='testing',
description=u('DESCRIPTION'),
default=u('DEFAULT'),
required=True,
)
prop = self._makeOne(field=field)

class Foo(object):
testing = prop
foo = Foo()

log = []
subscribers.append(log.append)
foo.testing = u'Bar'
foo.testing = u'Foo'
self.assertEqual(len(log), 2)
event = log[1]
self.assertTrue(isinstance(event, FieldUpdatedEvent))
self.assertEqual(event.inst, foo)
self.assertEqual(event.field, field)
self.assertEqual(event.old_value, u'Bar')
self.assertEqual(event.new_value, u'Foo')


class FieldPropertyStoredThroughFieldTests(_Base, _Integration):

Expand Down Expand Up @@ -390,6 +482,9 @@ class _Faux(object):
readonly = False
default = '456'

def query(self, inst, default):
return default

def bind(self, other):
return self

Expand Down Expand Up @@ -463,6 +558,60 @@ class Foo(object):
foo.__dict__['testing'] = '789'
self.assertRaises(ValueError, setattr, foo, 'testing', '123')

def test_field_event(self):
from zope.schema import Text
from zope.schema._compat import u
from zope.event import subscribers
from zope.schema.fieldproperty import FieldUpdatedEvent
log = []
subscribers.append(log.append)
self.assertEqual(log, [])
field = Text(
__name__='testing',
description=u('DESCRIPTION'),
default=u('DEFAULT'),
readonly=True,
required=True,
)
self.assertEqual(len(log), 6)
event = log[0]
self.assertTrue(isinstance(event, FieldUpdatedEvent))
self.assertEqual(event.inst, field)
self.assertEqual(event.old_value, 0)
self.assertEqual(event.new_value, 0)
self.assertEqual(
[ev.field.__name__ for ev in log],
['min_length', 'max_length', 'title', 'description', 'required', 'readonly'])

def test_field_event_update(self):
from zope.schema import Text
from zope.schema._compat import u
from zope.event import subscribers
from zope.schema.fieldproperty import FieldUpdatedEvent
field = Text(
__name__='testing',
description=u('DESCRIPTION'),
default=u('DEFAULT'),
required=True,
)
prop = self._makeOne(field=field)

class Foo(object):
testing = prop
foo = Foo()

log = []
subscribers.append(log.append)
foo.testing = u'Bar'
foo.testing = u'Foo'
self.assertEqual(len(log), 2)
event = log[1]
self.assertTrue(isinstance(event, FieldUpdatedEvent))
self.assertEqual(event.inst, foo)
self.assertEqual(event.field, field)
self.assertEqual(event.old_value, u'Bar')
self.assertEqual(event.new_value, u'Foo')


def _getSchema():
from zope.interface import Interface
Expand Down

0 comments on commit d1e4d1b

Please sign in to comment.