[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