z3c.form - ComputedErrorViewMessage and crazy adapter behaviour
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
participants (1)
-
Martin Aspeli