diff options
Diffstat (limited to 'docs/README.rst')
-rw-r--r-- | docs/README.rst | 226 |
1 files changed, 146 insertions, 80 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) |