[Zope-dev] z3c.form - ComputedErrorViewMessage and crazy adapter behaviour
Martin Aspeli
optilude+lists at gmail.com
Tue Mar 9 09:10:34 EST 2010
Hi,
I posted about this earlier, but the message seems to have gotten lost
in the ether.
I am trying and struggling to use ComputedErrorViewMessage. So far, I've
discovered two problems, one which affects IValue adapters in general.
1. The last discriminator of a ComputedErrorViewMessage (and an
ErrorViewMessage) is listed as 'content':
ErrorViewMessage = value.StaticValueCreator(
discriminators = ('error', 'request', 'widget', 'field', 'form',
'content')
)
ComputedErrorViewMessage = value.ComputedValueCreator(
discriminators = ('error', 'request', 'widget', 'field', 'form',
'content')
)
However, the adapter lookup further down in error.py does:
class ErrorViewSnippet(object):
...
def update(self):
value = zope.component.queryMultiAdapter(
(self.context, self.request, self.widget,
self.field, self.form, self),
interfaces.IValue, name='message')
if value is not None:
self.message = value.get()
else:
self.message = self.createMessage()
Notice how the last parameter is 'self', i.e. the ErrorViewSnippet. To
me, at least, 'content' suggests this refers to the value returned by
form.getContent(), most likely form.context.
There's no test for this discriminator that I can see, so I'm not sure
whether the "bug" is that 'content' is a poor name, or if the last part
of that tuple should be self.form.getContent() or self.form.context?
2. The order in which we use ErrorViewMessage or
ComputedErrorViewMessage seems to make a difference to its ability to
specialise. I really can't figure out what the bug is, though. I've
captured and annotated an interpreter session to illustrate the problem:
First, some imports:
>>> from zope.interface import Interface
>>> from zope.component import provideAdapter
>>> from zope import schema
>>> from zope.schema.interfaces import TooSmall
>>> from z3c.form import form, error, field
>>> from z3c.form.testing import setupFormDefaults, TestRequest
Set up a test environment with two forms.
>>> setupFormDefaults()
>>> class DummyContext(object):
... pass
>>> context = DummyContext()
>>> class ISchema(Interface):
... field1 = schema.Int(min=10)
>>> class Form1(form.Form):
... fields = field.Fields(ISchema)
... ignoreContext = True
>>> class Form2(form.Form):
... fields = field.Fields(ISchema)
... ignoreContext = True
Define two error message value adapters. Note that message2 is a
specialisation of message1, that also takes the form into account. Also
note that message1 (the more general) happens to be created first.
>>> message1 = error.ErrorViewMessage(u"Message 1", error=TooSmall,
field=ISchema['field1'])
>>> message2 = error.ErrorViewMessage(u"Message 2", error=TooSmall,
field=ISchema['field1'], form=Form2)
Now register these adapters (the order doesn't matter)
>>> provideAdapter(message1, name=u"message")
>>> provideAdapter(message2, name=u"message")
Let's now create an instance of Form1 and try to validate it.
>>> form1 = Form1(context, TestRequest(form={'form.widgets.field1': '5'}))
>>> form1.update()
>>> data, errors = form1.extractData()
>>> len(errors)
1
>>> errors[0].message
u'Message 1'
No surprises there. Now let's try Form2. We'd expect to see message2, since
it is registered for the more specific form:
>>> form2 = Form2(context, TestRequest(form={'form.widgets.field1': '5'}))
>>> form2.update()
>>> data, errors = form2.extractData()
>>> len(errors)
1
>>> errors[0].message
u'Message 1'
Ouch. Why is our more specific adapter being ignored?
Recall that the ErrorViewSnippet does the IValue adapter lookup like so:
value = zope.component.queryMultiAdapter(
(self.context, self.request, self.widget,
self.field, self.form, self),
interfaces.IValue, name='message')
The adapted objects are:
>>> (errors[0].context, errors[0].request, errors[0].widget,
errors[0].field, errors[0].form, errors[0])
(TooSmall(5, 10), <z3c.form.testing.TestRequest instance
URL=http://127.0.0.1>, <TextWidget 'form.widgets.field1'>,
<zope.schema._bootstrapfields.Int object at 0x1030a1190>,
<__main__.Form2 object at 0x1035c0b90>, <ErrorViewSnippet for TooSmall>)
Let's look at the interfaces provided by ISchema['field1']:
>>> from zope.interface import directlyProvidedBy
>>> list(directlyProvidedBy(ISchema['field1']).flattened())
[<InterfaceClass z3c.form.util.IGeneratedForObject_4345958800>,
<InterfaceClass z3c.form.util.IGeneratedForObject_4345958800>,
<InterfaceClass zope.interface.Interface>]
>>>
Let's also look at what our adapters are actually adapting:
>>> message1.__component_adapts__
(<class 'zope.schema._bootstrapinterfaces.TooSmall'>, None, None,
<InterfaceClass z3c.form.util.IGeneratedForObject_4345958800>, None, None)
>>> message2.__component_adapts__
(<class 'zope.schema._bootstrapinterfaces.TooSmall'>, None, None,
<InterfaceClass z3c.form.util.IGeneratedForObject_4345958800>, <class
'__main__.Form2'>, None)
Our adapters are both in the registry:
>>> from z3c.form.interfaces import IValue
>>> from pprint import pprint
>>> from zope.component import getSiteManager
>>> pprint([r for r in getSiteManager().registeredAdapters() if
r.provided == IValue])
[AdapterRegistration(<BaseGlobalComponents base>,
[zope.schema._bootstrapinterfaces.TooSmall, Interface, Interface,
IGeneratedForObject_4345958800, __main__.Form2, Interface], IValue,
u'message', <z3c.form.value.ValueFactory object at 0x1030a1690>, u''),
AdapterRegistration(<BaseGlobalComponents base>,
[zope.schema._bootstrapinterfaces.TooSmall, Interface, Interface,
IGeneratedForObject_4345958800, Interface, Interface], IValue,
u'message', <z3c.form.value.ValueFactory object at 0x10268e390>, u'')]
Sigh.
Let's try again, this time creating the value adapters in the opposite
order:
>>> from zope.interface import Interface
>>> from zope.component import provideAdapter
>>> from zope import schema
>>> from zope.schema.interfaces import TooSmall
>>> from z3c.form import form, error, field
>>> from z3c.form.testing import setupFormDefaults, TestRequest
Set up a test environment with two forms.
>>> setupFormDefaults()
>>> class DummyContext(object):
... pass
>>> context = DummyContext()
>>> class ISchema(Interface):
... field1 = schema.Int(min=10)
>>> class Form1(form.Form):
... fields = field.Fields(ISchema)
... ignoreContext = True
>>> class Form2(form.Form):
... fields = field.Fields(ISchema)
... ignoreContext = True
Define two error message value adapters. Note that message2 is a
specialisation of message1, that also takes the form into account. Also
note that message2 (the more specific) happens to be created first.
>>> message2 = error.ErrorViewMessage(u"Message 2", error=TooSmall,
field=ISchema['field1'], form=Form2)
>>> message1 = error.ErrorViewMessage(u"Message 1", error=TooSmall,
field=ISchema['field1'])
Now register these adapters (the order doesn't matter)
>>> provideAdapter(message1, name=u"message")
>>> provideAdapter(message2, name=u"message")
Let's now create an instance of Form1 and try to validate it.
>>> form1 = Form1(context, TestRequest(form={'form.widgets.field1': '5'}))
>>> form1.update()
>>> data, errors = form1.extractData()
>>> len(errors)
1
>>> errors[0].message
u'Message 1'
No surprises there. Now let's try Form2. This time, we get "Message 2"
>>> form2 = Form2(context, TestRequest(form={'form.widgets.field1': '5'}))
>>> form2.update()
>>> data, errors = form2.extractData()
>>> len(errors)
1
>>> errors[0].message
u'Message 2'
Let's also check the registry:
>>> (errors[0].context, errors[0].request, errors[0].widget,
errors[0].field, errors[0].form, errors[0])
(TooSmall(5, 10), <z3c.form.testing.TestRequest instance
URL=http://127.0.0.1>, <TextWidget 'form.widgets.field1'>,
<zope.schema._bootstrapfields.Int object at 0x10309b810>,
<__main__.Form2 object at 0x10392bd50>, <ErrorViewSnippet for TooSmall>)
>>> from zope.interface import directlyProvidedBy
>>> list(directlyProvidedBy(ISchema['field1']).flattened())
[<InterfaceClass z3c.form.util.IGeneratedForObject_4345935888>,
<InterfaceClass z3c.form.util.IGeneratedForObject_4345935888>,
<InterfaceClass zope.interface.Interface>]
>>> message1.__component_adapts__
(<class 'zope.schema._bootstrapinterfaces.TooSmall'>, None, None,
<InterfaceClass z3c.form.util.IGeneratedForObject_4345935888>, None, None)
>>> message2.__component_adapts__
(<class 'zope.schema._bootstrapinterfaces.TooSmall'>, None, None,
<InterfaceClass z3c.form.util.IGeneratedForObject_4345935888>, <class
'__main__.Form2'>, None)
>>> from z3c.form.interfaces import IValue
>>> from pprint import pprint
>>> from zope.component import getSiteManager
>>> pprint([r for r in getSiteManager().registeredAdapters() if
r.provided == IValue])
[AdapterRegistration(<BaseGlobalComponents base>,
[zope.schema._bootstrapinterfaces.TooSmall, Interface, Interface,
IGeneratedForObject_4345935888, Interface, Interface], IValue,
u'message', <z3c.form.value.ValueFactory object at 0x10309bd10>, u''),
AdapterRegistration(<BaseGlobalComponents base>,
[zope.schema._bootstrapinterfaces.TooSmall, Interface, Interface,
IGeneratedForObject_4345935888, __main__.Form2, Interface], IValue,
u'message', <z3c.form.value.ValueFactory object at 0x10309ba10>, u'')]
If there's a difference here, I can't see it.
Martin
More information about the Zope-Dev
mailing list