summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJason Madden <jamadden@gmail.com>2021-03-17 07:35:12 -0500
committerJason Madden <jamadden@gmail.com>2021-03-17 07:39:49 -0500
commit488a317abdfa2a1fa04efd2f2d8d22a433beaaf8 (patch)
treeee112ad969b0ca5987d56b471dd34818e67f8a22
parentdd69666aae99afe0d47b3af81149fbd7e97f59fe (diff)
downloadzope-interface-docs-update.tar.gz
Update the Adaptation docs to be more concrete.docs-update
This should help provide better motivating use cases. Examples inspired by https://glyph.twistedmatrix.com/2021/03/interfaces-and-protocols.html Also some minor typo fixes and updates to comments.
-rw-r--r--docs/README.rst226
-rw-r--r--docs/api/specifications.rst2
-rw-r--r--src/zope/interface/adapter.py9
-rw-r--r--src/zope/interface/declarations.py3
-rw-r--r--src/zope/interface/interface.py2
-rw-r--r--src/zope/interface/tests/test_adapter.py7
6 files changed, 163 insertions, 86 deletions
diff --git a/docs/README.rst b/docs/README.rst
index bfb7712..08e4f86 100644
--- a/docs/README.rst
+++ b/docs/README.rst
@@ -861,7 +861,17 @@ And the list will be filled with the individual exceptions:
Adaptation
==========
-Interfaces can be called to perform adaptation.
+Interfaces can be called to perform *adaptation*. Adaptation is the
+process of converting an object to an object implementing the
+interface. For example, in mathematics, to represent a point in space
+or on a graph there's the familiar Cartesian coordinate system using
+``CartesianPoint(x, y)``, and there's also the Polar coordinate system
+using ``PolarPoint(r, theta)``, plus several others (homogeneous,
+log-polar, etc). Polar points are most convenient for some types of
+operations, but cartesian points may make more intuitive sense to most
+people. Before printing an arbitrary point, we might want to *adapt* it
+to ``ICartesianPoint``, or before performing some mathematical
+operation you might want to adapt the arbitrary point to ``IPolarPoint``.
The semantics are based on those of the :pep:`246` ``adapt``
function.
@@ -870,34 +880,43 @@ If an object cannot be adapted, then a ``TypeError`` is raised:
.. doctest::
- >>> class I(zope.interface.Interface):
- ... pass
+ >>> class ICartesianPoint(zope.interface.Interface):
+ ... x = zope.interface.Attribute("Distance from origin along x axis")
+ ... y = zope.interface.Attribute("Distance from origin along y axis")
- >>> I(0)
+ >>> ICartesianPoint(0)
Traceback (most recent call last):
...
- TypeError: ('Could not adapt', 0, <InterfaceClass builtins.I>)
+ TypeError: ('Could not adapt', 0, <InterfaceClass builtins.ICartesianPoint>)
-unless an alternate value is provided as a second positional argument:
+unless a default value is provided as a second positional argument;
+this value is not checked to see if it implements the interface:
.. doctest::
- >>> I(0, 'bob')
+ >>> ICartesianPoint(0, 'bob')
'bob'
If an object already implements the interface, then it will be returned:
.. doctest::
- >>> @zope.interface.implementer(I)
- ... class C(object):
- ... pass
+ >>> @zope.interface.implementer(ICartesianPoint)
+ ... class CartesianPoint(object):
+ ... """The default cartesian point is the origin."""
+ ... def __init__(self, x=0, y=0):
+ ... self.x = x; self.y = y
+ ... def __repr__(self):
+ ... return "CartesianPoint(%s, %s)" % (self.x, self.y)
- >>> obj = C()
- >>> I(obj) is obj
+ >>> obj = CartesianPoint()
+ >>> ICartesianPoint(obj) is obj
True
+``__conform__``
+---------------
+
:pep:`246` outlines a requirement:
When the object knows about the [interface], and either considers
@@ -905,17 +924,18 @@ If an object already implements the interface, then it will be returned:
This is handled with ``__conform__``. If an object implements
``__conform__``, then it will be used to give the object the chance to
-decide if it knows about the interface.
+decide if it knows about the interface. This is true even if the class
+declares that it implements the interface.
.. doctest::
- >>> @zope.interface.implementer(I)
+ >>> @zope.interface.implementer(ICartesianPoint)
... class C(object):
... def __conform__(self, proto):
- ... return 0
+ ... return "This could be anything."
- >>> I(C())
- 0
+ >>> ICartesianPoint(C())
+ 'This could be anything.'
If ``__conform__`` returns ``None`` (because the object is unaware of
the interface), then the rest of the adaptation process will continue.
@@ -924,43 +944,40 @@ interface, it is returned.
.. doctest::
- >>> @zope.interface.implementer(I)
+ >>> @zope.interface.implementer(ICartesianPoint)
... class C(object):
... def __conform__(self, proto):
... return None
>>> c = C()
- >>> I(c) is c
+ >>> ICartesianPoint(c) is c
True
-Adapter hooks (see ``__adapt__``) will also be used, if present (after
+Adapter hooks (see :ref:`adapt_adapter_hooks`) will also be used, if present (after
a ``__conform__`` method, if any, has been tried):
.. doctest::
>>> from zope.interface.interface import adapter_hooks
- >>> def adapt_0_to_42(iface, obj):
- ... if obj == 0:
- ... return 42
+ >>> def adapt_tuple_to_point(iface, obj):
+ ... if isinstance(obj, tuple) and len(obj) == 2:
+ ... return CartesianPoint(*obj)
- >>> adapter_hooks.append(adapt_0_to_42)
- >>> I(0)
- 42
+ >>> adapter_hooks.append(adapt_tuple_to_point)
+ >>> ICartesianPoint((1, 1))
+ CartesianPoint(1, 1)
- >>> adapter_hooks.remove(adapt_0_to_42)
- >>> I(0)
+ >>> adapter_hooks.remove(adapt_tuple_to_point)
+ >>> ICartesianPoint((1, 1))
Traceback (most recent call last):
...
- TypeError: ('Could not adapt', 0, <InterfaceClass builtins.I>)
-
-``__adapt__``
--------------
+ TypeError: ('Could not adapt', (1, 1), <InterfaceClass builtins.ICartesianPoint>)
-.. doctest::
+.. _adapt_adapter_hooks:
- >>> class I(zope.interface.Interface):
- ... pass
+``__adapt__`` and adapter hooks
+-------------------------------
Interfaces implement the :pep:`246` ``__adapt__`` method to satisfy
the requirement:
@@ -973,7 +990,7 @@ This method is normally not called directly. It is called by the
:pep:`246` adapt framework and by the interface ``__call__`` operator
once ``__conform__`` (if any) has failed.
-The ``adapt`` method is responsible for adapting an object to the
+The ``__adapt__`` method is responsible for adapting an object to the
receiver.
The default version returns ``None`` (because by default no interface
@@ -981,70 +998,117 @@ The default version returns ``None`` (because by default no interface
.. doctest::
- >>> I.__adapt__(0)
+ >>> ICartesianPoint.__adapt__(0)
unless the object given provides the interface ("the object already complies"):
.. doctest::
- >>> @zope.interface.implementer(I)
+ >>> @zope.interface.implementer(ICartesianPoint)
... class C(object):
... pass
>>> obj = C()
- >>> I.__adapt__(obj) is obj
+ >>> ICartesianPoint.__adapt__(obj) is obj
True
-Adapter hooks can be provided (or removed) to provide custom
-adaptation. We'll install a silly hook that adapts 0 to 42.
-We install a hook by simply adding it to the ``adapter_hooks``
-list:
+.. rubric:: Customizing ``__adapt__`` in an interface
+
+It is possible to replace or customize the ``__adapt___``
+functionality for particular interfaces, if that interface "knows how
+to suitably wrap [an] object". This method should return the adapted
+object if it knows how, or call the super class to continue with the
+default adaptation process.
.. doctest::
- >>> from zope.interface.interface import adapter_hooks
- >>> def adapt_0_to_42(iface, obj):
- ... if obj == 0:
- ... return 42
+ >>> import math
+ >>> class IPolarPoint(zope.interface.Interface):
+ ... r = zope.interface.Attribute("Distance from center.")
+ ... theta = zope.interface.Attribute("Angle from horizontal.")
+ ... @zope.interface.interfacemethod
+ ... def __adapt__(self, obj):
+ ... if ICartesianPoint.providedBy(obj):
+ ... # Convert to polar coordinates.
+ ... r = math.sqrt(obj.x ** 2 + obj.y ** 2)
+ ... theta = math.acos(obj.x / r)
+ ... theta = math.degrees(theta)
+ ... return PolarPoint(r, theta)
+ ... return super(type(IPolarPoint), self).__adapt__(obj)
+
+ >>> @zope.interface.implementer(IPolarPoint)
+ ... class PolarPoint(object):
+ ... def __init__(self, r=0, theta=0):
+ ... self.r = r; self.theta = theta
+ ... def __repr__(self):
+ ... return "PolarPoint(%s, %s)" % (self.r, self.theta)
+ >>> IPolarPoint(CartesianPoint(0, 1))
+ PolarPoint(1.0, 90.0)
+ >>> IPolarPoint(PolarPoint())
+ PolarPoint(0, 0)
+
+.. seealso:: :func:`zope.interface.interfacemethod`, which explains
+ how to override functions in interface definitions and why, prior
+ to Python 3.6, the zero-argument version of `super` cannot be used.
- >>> adapter_hooks.append(adapt_0_to_42)
- >>> I.__adapt__(0)
- 42
+.. rubric:: Using adapter hooks for loose coupling
-Hooks must either return an adapter, or ``None`` if no adapter can
-be found.
+Commonly, the author of the interface doesn't know how to wrap all
+possible objects, and neither does the author of an object know how to
+``__conform__`` to all possible interfaces. To support decoupling
+interfaces and objects, interfaces support the concept of "adapter
+hooks." Adapter hooks are a global sequence of callables
+``hook(interface, object)`` that are called, in order, from the
+default ``__adapt__`` method until one returns a non-``None`` result.
-Hooks can be uninstalled by removing them from the list:
+.. note::
+ In many applications, a :doc:`adapter` is installed as
+ the first or only adapter hook.
+
+We'll install a hook that adapts from a 2D ``(x, y)`` Cartesian point
+on a plane to a three-dimensional point ``(x, y, z)`` by assuming the
+``z`` coordinate is 0. First, we'll define this new interface and an
+implementation:
.. doctest::
- >>> adapter_hooks.remove(adapt_0_to_42)
- >>> I.__adapt__(0)
+ >>> class ICartesianPoint3D(ICartesianPoint):
+ ... z = zope.interface.Attribute("Depth.")
+ >>> @zope.interface.implementer(ICartesianPoint3D)
+ ... class CartesianPoint3D(CartesianPoint):
+ ... def __init__(self, x=0, y=0, z=0):
+ ... CartesianPoint.__init__(self, x, y)
+ ... self.z = 0
+ ... def __repr__(self):
+ ... return "CartesianPoint3D(%s, %s, %s)" % (self.x, self.y, self.z)
-It is possible to replace or customize the ``__adapt___``
-functionality for particular interfaces.
+We install a hook by simply adding it to the ``adapter_hooks`` list:
.. doctest::
- >>> class ICustomAdapt(zope.interface.Interface):
- ... @zope.interface.interfacemethod
- ... def __adapt__(self, obj):
- ... if isinstance(obj, str):
- ... return obj
- ... return super(type(ICustomAdapt), self).__adapt__(obj)
+ >>> from zope.interface.interface import adapter_hooks
+ >>> def returns_none(iface, obj):
+ ... print("(First adapter hook returning None.)")
+ >>> def adapt_2d_to_3d(iface, obj):
+ ... if iface == ICartesianPoint3D and ICartesianPoint.providedBy(obj):
+ ... return CartesianPoint3D(obj.x, obj.y, 0)
+ >>> adapter_hooks.append(returns_none)
+ >>> adapter_hooks.append(adapt_2d_to_3d)
+ >>> ICartesianPoint3D.__adapt__(CartesianPoint())
+ (First adapter hook returning None.)
+ CartesianPoint3D(0, 0, 0)
+ >>> ICartesianPoint3D(CartesianPoint())
+ (First adapter hook returning None.)
+ CartesianPoint3D(0, 0, 0)
- >>> @zope.interface.implementer(ICustomAdapt)
- ... class CustomAdapt(object):
- ... pass
- >>> ICustomAdapt('a string')
- 'a string'
- >>> ICustomAdapt(CustomAdapt())
- <CustomAdapt object at ...>
+Hooks can be uninstalled by removing them from the list:
-.. seealso:: :func:`zope.interface.interfacemethod`, which explains
- how to override functions in interface definitions and why, prior
- to Python 3.6, the zero-argument version of `super` cannot be used.
+.. doctest::
+
+ >>> adapter_hooks.remove(returns_none)
+ >>> adapter_hooks.remove(adapt_2d_to_3d)
+ >>> ICartesianPoint3D.__adapt__(CartesianPoint())
.. _global_persistence:
@@ -1094,6 +1158,8 @@ process, the identical object is found and returned:
>>> imported == IFoo
True
+.. rubric:: References to Global Objects
+
The eagle-eyed reader will have noticed the two funny lines like
``sys.modules[__name__].Foo = Foo``. What's that for? To understand,
we must know a bit about how Python "pickles" (``pickle.dump`` or
@@ -1105,8 +1171,8 @@ exist (contrast this with pickling a string or an object instance,
which creates a new object in the receiving process) with all their
necessary state information (for classes and interfaces, the state
information would be things like the list of methods and defined
-attributes) in the receiving process; the pickled byte string needs
-only contain enough data to look up that existing object; this is a
+attributes) in the receiving process, so the pickled byte string needs
+only contain enough data to look up that existing object; this data is a
*reference*. Not only does this minimize the amount of data required
to persist such an object, it also facilitates changing the definition
of the object over time: if a class or interface gains or loses
@@ -1145,7 +1211,7 @@ line is automatic), we still cannot pickle the old one:
>>> orig_Foo = Foo
>>> class Foo(object):
... pass
- >>> sys.modules[__name__].Foo = Foo # XXX, see below
+ >>> sys.modules[__name__].Foo = Foo # XXX, usually automatic
>>> pickle.dumps(orig_Foo)
Traceback (most recent call last):
...
@@ -1210,12 +1276,12 @@ other, and consistent with pickling:
>>> class IFoo(zope.interface.Interface):
... pass
- >>> sys.modules[__name__].IFoo = IFoo
+ >>> sys.modules[__name__].IFoo = IFoo # XXX, usually automatic
>>> f1 = IFoo
>>> pickled_f1 = pickle.dumps(f1)
>>> class IFoo(zope.interface.Interface):
... pass
- >>> sys.modules[__name__].IFoo = IFoo
+ >>> sys.modules[__name__].IFoo = IFoo # XXX, usually automatic
>>> IFoo == f1
True
>>> unpickled_f1 = pickle.loads(pickled_f1)
@@ -1229,12 +1295,12 @@ This isn't quite the case for classes; note how ``f1`` wasn't equal to
>>> class Foo(object):
... pass
- >>> sys.modules[__name__].Foo = Foo
+ >>> sys.modules[__name__].Foo = Foo # XXX, usually automatic
>>> f1 = Foo
>>> pickled_f1 = pickle.dumps(Foo)
>>> class Foo(object):
... pass
- >>> sys.modules[__name__].Foo = Foo
+ >>> sys.modules[__name__].Foo = Foo # XXX, usually automatic
>>> f1 == Foo
False
>>> unpickled_f1 = pickle.loads(pickled_f1)
diff --git a/docs/api/specifications.rst b/docs/api/specifications.rst
index 8a1f21f..8688939 100644
--- a/docs/api/specifications.rst
+++ b/docs/api/specifications.rst
@@ -196,7 +196,7 @@ map to the same value in a dictionary.
Caveats
~~~~~~~
-While this behaviour works will with :ref:`pickling (persistence)
+While this behaviour works well with :ref:`pickling (persistence)
<global_persistence>`, it has some potential downsides to be aware of.
.. rubric:: Weak References
diff --git a/src/zope/interface/adapter.py b/src/zope/interface/adapter.py
index 8242b6b..b9836d9 100644
--- a/src/zope/interface/adapter.py
+++ b/src/zope/interface/adapter.py
@@ -234,12 +234,15 @@ class BaseAdapterRegistry(object):
Remove the item *to_remove* from the (non-``None``, non-empty)
*existing_leaf_sequence* and return the mutated sequence.
- Subclasses that redefine `_leafSequenceType` should override
- this method.
-
If there is more than one item that is equal to *to_remove*
they must all be removed.
+ Subclasses that redefine `_leafSequenceType` should override
+ this method. Note that they can call this method to help
+ in their implementation; this implementation will always
+ return a new tuple constructed by iterating across
+ the *existing_leaf_sequence* and omitting items equal to *to_remove*.
+
:param existing_leaf_sequence:
As for `_addValueToLeaf`, probably an instance of
`_leafSequenceType` but possibly an older type; never `None`.
diff --git a/src/zope/interface/declarations.py b/src/zope/interface/declarations.py
index 9a0146c..1b2328e 100644
--- a/src/zope/interface/declarations.py
+++ b/src/zope/interface/declarations.py
@@ -1040,7 +1040,8 @@ def moduleProvides(*interfaces):
# XXX: is this a fossil? Nobody calls it, no unit tests exercise it, no
# doctests import it, and the package __init__ doesn't import it.
-# (Answer: Versions of zope.container prior to 4.4.0 called this.)
+# (Answer: Versions of zope.container prior to 4.4.0 called this,
+# and zope.proxy.decorator up through at least 4.3.5 called this.)
def ObjectSpecification(direct, cls):
"""Provide object specifications
diff --git a/src/zope/interface/interface.py b/src/zope/interface/interface.py
index d100aae..e3d67ae 100644
--- a/src/zope/interface/interface.py
+++ b/src/zope/interface/interface.py
@@ -425,7 +425,7 @@ class Specification(SpecificationBase):
# some bases that DO implement an interface, and some that DO
# NOT. In such a mixed scenario, you wind up with a set of
# bases to consider that look like this: [[..., Interface],
- # [..., object], ...]. Depending on the order if inheritance,
+ # [..., object], ...]. Depending on the order of inheritance,
# Interface can wind up before or after object, and that can
# happen at any point in the tree, meaning Interface can wind
# up somewhere in the middle of the order. Since Interface is
diff --git a/src/zope/interface/tests/test_adapter.py b/src/zope/interface/tests/test_adapter.py
index a80e4f1..2412f41 100644
--- a/src/zope/interface/tests/test_adapter.py
+++ b/src/zope/interface/tests/test_adapter.py
@@ -799,6 +799,13 @@ class BaseAdapterRegistryTests(unittest.TestCase):
class CustomTypesBaseAdapterRegistryTests(BaseAdapterRegistryTests):
+ """
+ This class may be extended by other packages to test their own
+ adapter registries that use custom types. (So be cautious about
+ breaking changes.)
+
+ One known user is ``zope.component.persistentregistry``.
+ """
def _getMappingType(self):
return CustomMapping