DX behaviors with adapter factory and acquisition

I have a DX behavior with an adapter factory (see FYI: Dexterity behaviors and (direct) attribute access explained).

IMyBehavior(self.context).aq_inner
*** AttributeError: 'MyBehavior' object has no attribute 'aq_inner'

Schema-only behaviors do not have this problem.
How can I get acquisition to work when the behavior has an adapter factory?

I'd guess:

  1. You must split the adapter factory into a factory function (this gets registered as the factory) and an adapter class (instantiated and returned by the factory function).

  2. Adapter class must be inherited from Implicit acquisition base class.

  3. Factoey function first instantiates the adapter with context, but the returns acquisition wrapper with adapter.__of__(context)

1 Like

Thanks, @datakurre, you're the best! Your guess is 100% correct.
For future reference, here's what I did, and it works:

configure.zcml:

  <plone:behavior
      title=...
      description=...                                               
      provides=".interfaces.IMyBehavior"                              
      factory=".behavior.MyBehaviorFactory"                           
      for="plone.dexterity.interfaces.IDexterityContent"                     
      marker=".interfaces.IMyBehaviorLayer"         
      />                                                                     
interfaces.py
from plone.autoform.interfaces import IFormFieldProvider
from plone.supermodel import model
from zope.interface import provider
from zope.publisher.interfaces.browser import IDefaultBrowserLayer

class IMyBehaviorLayer(IDefaultBrowserLayer):
    """Marker interface that defines a browser layer."""

@provider(IFormFieldProvider)
class IMyBehaviorLayer(model.Schema):
    """ Behavior schema definition goes here """
behavior.py
from Acquisition import Explicit
from ....interfaces import IMyBehavior
from plone.dexterity.interfaces import IDexterityContent
from zope.component import adapter
from zope.interface import implementer

@implementer(IMyBehavior)
@adapter(IDexterityContent)
class MyBehavior(Explicit):

    def __init__(self, context):
        self.context = context

    # rest of adapter class comes here


def MyBehaviorFactory(context):
    adapter = MyBehavior(context)
    return adapter.__of__(context)

In my original post I used an example of aq_inner, but actually, I'm more interested in aq_explicit. That's why I used Acquisition.Explicit as a base class for the adapter.

I'm still not entirely clear why it has to be done with this split of the adapter class and the factory function, instead of somehow having the adapter class constructor do the work. But I'm not complaining.

I am sorry, this is not correct: even if it might work by accident, it mixes some concepts up.

Better first read https://pypi.python.org/pypi/plone.behavior#zcml-reference (or better the whole README) to get an overall idea how behaviors are working.

  • IMyBehaviorLayer is defined twice, I guess for the second you meant IMyBehavior
  • the Dexterity marker interface is completely different from browser-layers and must not be confused (browser layers are just markers on the request!). A Dexterity marker interface is a simple class deriving from Interface. It is, what is adapted by calling IMyBehavior with context. If a behavior is assigned in a DX-FTI, the marker interfaces are provided automatically by the DexterityItem or DexterityContainer via FTIAwareSpecification.
  • I don't understand why MyBehavior has the @adapter. In your case, this is superfluous, and semantically not 100% correct. Once a behavior is assigned to a type (in FTI) the marker is provided by context. If not assigned it is not. So it is also semantically wrong by claiming it's @adapter(IDexterityContent), It is not valid for any IDexterityContent.
  • in ZCML name is missing (you should get a warning at the moment).

So corrected it should look like this:

configure.zcml:

  <plone:behavior
      title=...
      description=...    
      name='short.namespace.name'                                           
      provides=".interfaces.IMyBehavior"                              
      factory=".behavior.my_behavior_factory"                           
      marker=".interfaces.IMyBehaviorMarker"  
      />                                                                     
interfaces.py
from plone.autoform.interfaces import IFormFieldProvider
from plone.supermodel import model
from zope.interface import Interface
from zope.interface import provider

class IMyBehaviorMarker(Interface):
    """Marker interface for type accepting MyBehavior."""

@provider(IFormFieldProvider)
class IMyBehavior(model.Schema):
    """ Behavior schema definition goes here """
behavior.py
from Acquisition import Explicit
from ....interfaces import IMyBehavior
from zope.component import adapter
from zope.interface import implementer

@implementer(IMyBehavior)
class MyBehavior(Explicit):

    def __init__(self, context):
        self.context = context

    # rest of adapter class comes here


def my_behavior_factory(context):
    adapter = MyBehavior(context)
    return adapter.__of__(context)

Thanks, @jensens for looking it over. To your points:

  • Yes, I did make a mistake in the double definition of IMyBehaviorLayer, I did mean IMyBehavior for the second one. (Obviously, a transcription error in my example code - my real code would not even have started up.)
  • Regarding the marker interface, thanks for clarifying that I should not use a browser layer. Though I'm not clear on what you mean by the marker interfaces are provided automatically by the DexterityItem or DexterityContainer via FTIAwareSpecification. I initially had some trouble getting this working, until I found this sentence in the docs: If a factory is given, a marker interface different from the behavior interface must be declared. (my emphasis)
  • I use the @adapter because I went from the docs as my starting point. Is the example in the docs incorrect? In my case, I really don't care to adapt my behavior to any other interface, so I can see that it's superfluous. Also, the docs say: In many cases, you will omit the adapter() line, provided your behavior is generic enough to work on any context.
  • I do not get a warning from the missing name. The same page in the docs also does not use name in the ZCML and says that the name is constructed from the full dotted name to the behavior interface. The previous page, also mentions that the plone.behavior.interfaces.IBehavior named utility registered by the ZCML has a name that is the full dotted name of the behavior interface.

I would still be curious to find out whether this splitting of the factory function from the behavior class is necessary, or if the same can be achieved by some clever code in the __init__ or another method of the class.

Also, the ILeadImage behavior in plone.app.contenttypes declares @adapter in the same way:

@implementer(ILeadImage)
@adapter(IDexterityContent)
class LeadImage(object):

    def __init__(self, context):
        self.context = context

Sure. Is it a documentation bug, a plone.app.contenttypes bug, or a
behavior bug?