diff options
-rw-r--r-- | docs/README.rst | 226 | ||||
-rw-r--r-- | docs/api/specifications.rst | 2 | ||||
-rw-r--r-- | src/zope/interface/adapter.py | 9 | ||||
-rw-r--r-- | src/zope/interface/declarations.py | 3 | ||||
-rw-r--r-- | src/zope/interface/interface.py | 2 | ||||
-rw-r--r-- | src/zope/interface/tests/test_adapter.py | 7 |
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 |