Skip to content

Commit

Permalink
Make Interface.getTaggedValue follow the __iro__.
Browse files Browse the repository at this point in the history
Previously it manually walked up __bases__, meaning the answers could be inconsistent.

Fixes #190.

Also fixes several minor issues in the documentation, mostly cross-reference related.
  • Loading branch information
jamadden committed Mar 18, 2020
1 parent d0c6a59 commit f4b777d
Show file tree
Hide file tree
Showing 7 changed files with 262 additions and 35 deletions.
18 changes: 16 additions & 2 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,17 @@
the future). For details, see the documentation for
``zope.interface.ro``.

- Make inherited tagged values in interfaces respect the resolution
order (``__iro__``), as method and attribute lookup does. Previously
tagged values could give inconsistent results. See `issue 190
<https://github.com/zopefoundation/zope.interface/issues/190>`_.

- Add ``getDirectTaggedValue`` (and related methods) to interfaces to
allow accessing tagged values irrespective of inheritance. See
`issue 190
<https://github.com/zopefoundation/zope.interface/issues/190>`_.


4.7.2 (2020-03-10)
==================

Expand All @@ -214,10 +225,13 @@

- Drop support for Python 3.4.

- Fix ``queryTaggedValue``, ``getTaggedValue``, ``getTaggedValueTags``
subclass inheritance. See `PR 144
- Change ``queryTaggedValue``, ``getTaggedValue``,
``getTaggedValueTags`` in interfaces. They now include inherited
values by following ``__bases__``. See `PR 144
<https://github.com/zopefoundation/zope.interface/pull/144>`_.

.. caution:: This may be a breaking change.

- Add support for Python 3.8.


Expand Down
22 changes: 22 additions & 0 deletions docs/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,28 @@ Tagged values can also be defined from within an interface definition:
>>> IWithTaggedValues.getTaggedValue('squish')
'squash'

Tagged values are inherited in the same way that attribute and method
descriptions are. Inheritance can be ignored by using the "direct"
versions of functions.

.. doctest::

>>> class IExtendsIWithTaggedValues(IWithTaggedValues):
... zope.interface.taggedValue('child', True)
>>> IExtendsIWithTaggedValues.getTaggedValue('child')
True
>>> IExtendsIWithTaggedValues.getDirectTaggedValue('child')
True
>>> IExtendsIWithTaggedValues.getTaggedValue('squish')
'squash'
>>> print(IExtendsIWithTaggedValues.queryDirectTaggedValue('squish'))
None
>>> IExtendsIWithTaggedValues.setTaggedValue('squish', 'SQUASH')
>>> IExtendsIWithTaggedValues.getTaggedValue('squish')
'SQUASH'
>>> IExtendsIWithTaggedValues.getDirectTaggedValue('squish')
'SQUASH'

Invariants
==========

Expand Down
10 changes: 5 additions & 5 deletions docs/api/declarations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ carefully at each object it documents, including providing examples.
.. autointerface:: zope.interface.interfaces.IInterfaceDeclaration


.. currentmodule:: zope.interface.declarations
.. currentmodule:: zope.interface

Declaring The Interfaces of Objects
===================================
Expand Down Expand Up @@ -536,7 +536,7 @@ You'll notice that an ``IDeclaration`` is a type of
implementedBy
-------------

.. autofunction:: implementedByFallback
.. autofunction:: implementedBy


Consider the following example:
Expand Down Expand Up @@ -774,7 +774,7 @@ Exmples for :meth:`Declaration.__add__`:
ProvidesClass
-------------

.. autoclass:: ProvidesClass
.. autoclass:: zope.interface.declarations.ProvidesClass


Descriptor semantics (via ``Provides.__get__``):
Expand Down Expand Up @@ -851,7 +851,7 @@ collect function to help with this:
ObjectSpecification
-------------------

.. autofunction:: ObjectSpecification
.. autofunction:: zope.interface.declarations.ObjectSpecification


For example:
Expand Down Expand Up @@ -924,7 +924,7 @@ For example:
ObjectSpecificationDescriptor
-----------------------------

.. autoclass:: ObjectSpecificationDescriptor
.. autoclass:: zope.interface.declarations.ObjectSpecificationDescriptor

For example:

Expand Down
16 changes: 16 additions & 0 deletions docs/api/specifications.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Specification objects implement the API defined by
:member-order: bysource

.. autoclass:: zope.interface.interface.Specification
:no-members:

For example:

Expand Down Expand Up @@ -172,7 +173,22 @@ first is that of an "element", which provides us a simple way to query
for information generically (this is important because we'll see that
``IInterface`` implements this interface):

..
IElement defines __doc__ to be an Attribute, so the docstring
in the class isn't used._
.. autointerface:: IElement

Objects that have basic documentation and tagged values.

Known derivatives include :class:`IAttribute` and its derivative
:class:`IMethod`; these have no notion of inheritance.
:class:`IInterface` is also a derivative, and it does have a
notion of inheritance, expressed through its ``__bases__`` and
ordered in its ``__iro__`` (both defined by
:class:`ISpecification`).


.. autoclass:: zope.interface.interface.Element
:no-members:

Expand Down
38 changes: 26 additions & 12 deletions src/zope/interface/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ def getDoc(self):
""" Returns the documentation for the object. """
return self.__doc__

###
# Tagged values.
#
# Direct tagged values are set only in this instance. Others
# may be inherited (for those subclasses that have that concept).
###

def getTaggedValue(self, tag):
""" Returns the value associated with 'tag'. """
if not self.__tagged_values:
Expand All @@ -98,7 +105,7 @@ def queryTaggedValue(self, tag, default=None):
return self.__tagged_values.get(tag, default) if self.__tagged_values else default

def getTaggedValueTags(self):
""" Returns a list of all tags. """
""" Returns a collection of all tags. """
return self.__tagged_values.keys() if self.__tagged_values else ()

def setTaggedValue(self, tag, value):
Expand All @@ -107,6 +114,10 @@ def setTaggedValue(self, tag, value):
self.__tagged_values = {}
self.__tagged_values[tag] = value

queryDirectTaggedValue = queryTaggedValue
getDirectTaggedValue = getTaggedValue
getDirectTaggedValueTags = getTaggedValueTags


@_use_c_impl
class SpecificationBase(object):
Expand Down Expand Up @@ -512,12 +523,14 @@ def validateInvariants(self, obj, errors=None):
raise Invalid(errors)

def queryTaggedValue(self, tag, default=None):
""" Returns the value associated with 'tag'. """
value = Element.queryTaggedValue(self, tag, default=_marker)
if value is not _marker:
return value
for base in self.__bases__:
value = base.queryTaggedValue(tag, default=_marker)
"""
Queries for the value associated with *tag*, returning it from the nearest
interface in the ``__iro__``.
If not found, returns *default*.
"""
for iface in self.__iro__:
value = iface.queryDirectTaggedValue(tag, _marker)
if value is not _marker:
return value
return default
Expand All @@ -531,11 +544,9 @@ def getTaggedValue(self, tag):

def getTaggedValueTags(self):
""" Returns a list of all tags. """
keys = list(Element.getTaggedValueTags(self))
for base in self.__bases__:
for key in base.getTaggedValueTags():
if key not in keys:
keys.append(key)
keys = set()
for base in self.__iro__:
keys.update(base.getDirectTaggedValueTags())
return keys

def __repr__(self): # pragma: no cover
Expand Down Expand Up @@ -783,6 +794,9 @@ def fromMethod(meth, interface=None, name=None):
def _wire():
from zope.interface.declarations import classImplements

from zope.interface.interfaces import IElement
classImplements(Element, IElement)

from zope.interface.interfaces import IAttribute
classImplements(Attribute, IAttribute)

Expand Down
99 changes: 84 additions & 15 deletions src/zope/interface/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,29 +44,96 @@


class IElement(Interface):
"""Objects that have basic documentation and tagged values.
"""
Objects that have basic documentation and tagged values.
Known derivatives include :class:`IAttribute` and its derivative
:class:`IMethod`; these have no notion of inheritance.
:class:`IInterface` is also a derivative, and it does have a
notion of inheritance, expressed through its ``__bases__`` and
ordered in its ``__iro__`` (both defined by
:class:`ISpecification`).
"""

# Note that defining __doc__ as an Attribute hides the docstring
# from introspection. When changing it, also change it in the Sphinx
# ReST files.

__name__ = Attribute('__name__', 'The object name')
__doc__ = Attribute('__doc__', 'The object doc string')

###
# Tagged values.
#
# Direct values are established in this instance. Others may be
# inherited. Although ``IElement`` itself doesn't have a notion of
# inheritance, ``IInterface`` *does*. It might have been better to
# make ``IInterface`` define new methods
# ``getIndirectTaggedValue``, etc, to include inheritance instead
# of overriding ``getTaggedValue`` to do that, but that ship has sailed.
# So to keep things nice and symmetric, we define the ``Direct`` methods here.
###

def getTaggedValue(tag):
"""Returns the value associated with `tag`.
"""Returns the value associated with *tag*.
Raise a `KeyError` if the tag isn't set.
Raise a `KeyError` of the tag isn't set.
If the object has a notion of inheritance, this searches
through the inheritance hierarchy and returns the nearest result.
If there is no such notion, this looks only at this object.
.. versionchanged:: 4.7.0
This method should respect inheritance if present.
"""

def queryTaggedValue(tag, default=None):
"""Returns the value associated with `tag`.
"""
As for `getTaggedValue`, but instead of raising a `KeyError`, returns *default*.
Return the default value of the tag isn't set.
.. versionchanged:: 4.7.0
This method should respect inheritance if present.
"""

def getTaggedValueTags():
"""Returns a list of all tags."""
"""
Returns a collection of all tags in no particular order.
If the object has a notion of inheritance, this
includes all the inherited tagged values. If there is
no such notion, this looks only at this object.
.. versionchanged:: 4.7.0
This method should respect inheritance if present.
"""

def setTaggedValue(tag, value):
"""Associates `value` with `key`."""
"""
Associates *value* with *key* directly in this object.
"""

def getDirectTaggedValue(tag):
"""
As for `getTaggedValue`, but never includes inheritance.
.. versionadded:: 5.0.0
"""

def queryDirectTaggedValue(tag, default=None):
"""
As for `queryTaggedValue`, but never includes inheritance.
.. versionadded:: 5.0.0
"""

def getDirectTaggedValueTags():
"""
As for `getTaggedValueTags`, but includes only tags directly
set on this object.
.. versionadded:: 5.0.0
"""


class IAttribute(IElement):
Expand Down Expand Up @@ -148,22 +215,23 @@ def weakref(callback=None):

__bases__ = Attribute("""Base specifications
A tuple if specifications from which this specification is
A tuple of specifications from which this specification is
directly derived.
""")

__sro__ = Attribute("""Specification-resolution order
A tuple of the specification and all of it's ancestor
specifications from most specific to least specific.
specifications from most specific to least specific. The specification
itself is the first element.
(This is similar to the method-resolution order for new-style classes.)
""")

__iro__ = Attribute("""Interface-resolution order
A tuple of the of the specification's ancestor interfaces from
A tuple of the specification's ancestor interfaces from
most specific to least specific. The specification itself is
included if it is an interface.
Expand Down Expand Up @@ -240,14 +308,14 @@ def meth2():
- You assert that your object implement the interfaces.
There are several ways that you can assert that an object
implements an interface:
There are several ways that you can declare that an object
provides an interface:
1. Call `zope.interface.implements` in your class definition.
1. Call `zope.interface.implementer` on your class definition.
2. Call `zope.interfaces.directlyProvides` on your object.
2. Call `zope.interface.directlyProvides` on your object.
3. Call `zope.interface.classImplements` to assert that instances
3. Call `zope.interface.classImplements` to declare that instances
of a class implement an interface.
For example::
Expand Down Expand Up @@ -321,6 +389,7 @@ def __iter__():

__module__ = Attribute("""The name of the module defining the interface""")


class IDeclaration(ISpecification):
"""Interface declaration
Expand Down

0 comments on commit f4b777d

Please sign in to comment.