diff -r d7dd088ed557 -r 097b0c025eec src/source/dev_guide/howto-adapter.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/source/dev_guide/howto-adapter.rst Tue Dec 11 17:00:42 2018 +0100 @@ -0,0 +1,180 @@ +.. _adapterhowto: + + +How to define an annotations adapter? +===================================== + +Adapters are important concepts of ZCA and PyAMS framework. If you don't know what are adapters, see :ref:`zca`. + + +What are annotations? ++++++++++++++++++++++ + +When an adapter have to add persistent attributes to a persistent class, it can add these attributes directly into +persistent instances. But this way can lead to conflicts when several adapters want to use the same attribute name for +different kinds of information. + +Annotations are an elegant way to handle this use case: they are based on a BTree which is stored into a +specific instance attribute (*__annotations__*). Any adapter can then use this dictionary to store it's own +informations, using it's own namespace as dictionary key. + +ZODB browser allows you to display existing annotations: + +.. image:: _static/annotations-1.png + +This example displays several annotations, each using it's own namespace: + +.. image:: _static/annotations-2.png + + +Designing interfaces +++++++++++++++++++++ + +The first step with ZCA is to design your interfaces. + +The are going to base our example on PyAMS_content 'paragraphs' component: a content class is marked as a +*paragraphs container target*, a class that can store paragraphs. But the real storage of paragraphs is done by +another *container* class: + +.. code-block:: python + :linenos: + + from zope.annotation.interfaces import IAttributeAnnotatable + from zope.containers.constraints import containers, contains + + + class IBaseParagraph(Interface): + """Base paragraph interface""" + + containers('.IParagraphContainer') + + + class IParagraphContainer(IOrderedContainer): + """Paragraphs container""" + + contains(IBaseParagraph) + + + class IParagraphContainerTarget(IAttributeAnnotatable): + """Paragraphs container marker interface""" + + + PARAGRAPH_CONTAINER_KEY = 'pyams_content.paragraph' + + +- line 5 to 8: :class:`IBaseParagraph` is the base interface for all paragraphs; constraint implies that paragraphs + can only be stored in a container implementing :class:`IParagraphContainer` interface. +- line 11 to 14: :class:`IParagraphContainer` is the base interface for paragraphs containers; constraint implies that + such a container can only contain objects implementing :class:`IBaseParagraph` interface. +- line 17 to 18: :class:`IParagraphContainerTarget` is only a *marker* interface which doesn't provide any method or + attribute; it only inherits from :class:`IAttributeAnnotatable`, which implies that classes implementing this + interface allows other classes to add informations as annotations through a dedicated *__annotations__* attribute. +- line 21: this is the key which will be used to store our annotation. + + +Creating persistent classes ++++++++++++++++++++++++++++ + +The first step is to declare that a given content class can store paragraphs: + +.. code-block:: python + :linenos: + + from pyams_content.component.paragraph.interfaces import IParagraphContainerTarget + from zope.interface import implementer + + @implementer(IParagraphContainerTarget) + class WfNewsEvent(WfSharedContent): + """News event class""" + +Here we just say "Well, I'm a shared content, and I'm OK to store paragraphs!". + +So we can design the paragraphs container class. It's this class which will *really* store the paragraphs: + +.. code-block:: python + :linenos: + + @implementer(IParagraphContainer) + class ParagraphContainer(BTreeOrderedContainer): + """Paragraphs container""" + +The paragraphs container class inherits from a :class:`BTreeOrderedContainer` and implements +:class:`IParagraphContainer`. + +The last operation is to create the adapter, which is the *glue* between the *target* class and the paragraphs +container: + +.. code-block:: python + :linenos: + + from pyams_utils.adapter import adapter_config, get_annotation_adapter + + @adapter_config(context=IParagraphContainerTarget, provides=IParagraphContainer) + def paragraph_container_factory(target): + """Paragraphs container factory""" + return get_annotation_adapter(target, + PARAGRAPH_CONTAINER_KEY, + ParagraphContainer, + name='++paras++') + +PyAMS provides a shortcut to create an annotation adapter in :func:`pyams_utils.adapter.get_annotation_adapter`. +It's mandatory arguments are: + +- **context** (line 6): the context to which the adapter is applied +- **key** (line 7): the string key used to access and store context's annotations +- **factory** (line 8): if the requested annotation is missing, a new one is created using this factory (which can be a class or + a function) + +Optional arguments are: + +- **markers** (None by default): if set, should be a list of marker interfaces which will be assigned to object + created by the factory +- **notify**: if *True* (default), an :class:`ObjectCreatedEvent` event is notified on object creation +- **locate**: if *True* (default), context is set as *parent* of created object +- **parent**: if *locate* is True and if *parent* is set, this is the object to which the new object should be *parented* + instead of initial context +- **name** (None by default): some objects need to be traversed, especially when you have to be able to access them through an URL; this + is the name given to created object. + + +Using your adapter +++++++++++++++++++ + +Starting from your *content* object, it's then very simple to access to the paragraphs container: + +.. code-block:: python + :linenos: + + event = WfNewsEvent() + paragraphs_container = IParagraphContainer(event, None) + +And that's it! From now I can get access to all paragraphs associated with my initial content!! + + +Managing traversal +++++++++++++++++++ + +As said before, sometimes you have to be able to *traverse* from an initial content to a given sub-content +managed by an adapter. + +PyAMS defines a custom :class:`pyams_utils.traversing.NamespaceTraverser`: when a request traversing subpath is +starting with '++' characters, it is looking for a named traverser providing :class:`ITraversable` interface +to the last traversed object. + +.. code-block:: python + :linenos: + + @adapter_config(name='paras', context=IParagraphContainerTarget, provides=ITraversable) + class ParagraphContainerNamespace(ContextAdapter): + """++paras++ namespace adapter""" + + def traverse(self, name, furtherpath=None): + return IParagraphContainer(self.context) + +- line 1: the adapter is named "paras"; this is matching the *++paras++* name which was given to our annotation adapter +- line 2: the adapter is just a simple context adapter, so inheriting from :class:`pyams_utils.adapter.ContextAdapter` +- lines 5 to 6: the *traverse* method is used to access the adapted content; if a name like "++ns++value" is given + to an adapted object, the "value" part is given as *name" argument. + +From now, as soon as an URL like "/mycontent/++paras++/" will be used, you will get access to the paragraphs container. +This is a standard BTree container, so will get access to it's sub-objects by key.