Skip to content

Commit

Permalink
Merge pull request #237 from zopefoundation/issue236
Browse files Browse the repository at this point in the history
Update repr() and str() of some common objects.
  • Loading branch information
jamadden committed Mar 29, 2021
2 parents e6d3805 + ce8f66f commit 4a686fc
Show file tree
Hide file tree
Showing 9 changed files with 444 additions and 97 deletions.
10 changes: 10 additions & 0 deletions CHANGES.rst
Expand Up @@ -2,6 +2,16 @@
Changes
=========

5.4.0 (unreleased)
==================

- Update the ``repr()`` and ``str()`` of various objects to be shorter
and more informative. In many cases, the ``repr()`` is now something
that can be evaluated to produce an equal object. For example, what
was previously printed as ``<implementedBy builtins.list>`` is now
shown as ``classImplements(list, IMutableSequence, IIterable)``. See
`issue 236 <https://github.com/zopefoundation/zope.interface/issues/236>`_.

5.3.0 (2020-03-21)
==================

Expand Down
12 changes: 6 additions & 6 deletions docs/README.rst
Expand Up @@ -677,7 +677,7 @@ interfaces that they declare:

>>> baz_implements = zope.interface.implementedBy(Baz)
>>> baz_implements.__bases__
(<InterfaceClass builtins.IBaz>, <implementedBy ...object>)
(<InterfaceClass builtins.IBaz>, classImplements(object))

>>> baz_implements.extends(IFoo)
True
Expand All @@ -694,25 +694,25 @@ that lists the specification and all of it's ancestors:

>>> from pprint import pprint
>>> pprint(baz_implements.__sro__)
(<implementedBy builtins.Baz>,
(classImplements(Baz, IBaz),
<InterfaceClass builtins.IBaz>,
<InterfaceClass builtins.IFoo>,
<InterfaceClass builtins.IBlat>,
<implementedBy ...object>,
classImplements(object),
<InterfaceClass zope.interface.Interface>)
>>> class IBiz(zope.interface.Interface):
... pass
>>> @zope.interface.implementer(IBiz)
... class Biz(Baz):
... pass
>>> pprint(zope.interface.implementedBy(Biz).__sro__)
(<implementedBy builtins.Biz>,
(classImplements(Biz, IBiz),
<InterfaceClass builtins.IBiz>,
<implementedBy builtins.Baz>,
classImplements(Baz, IBaz),
<InterfaceClass builtins.IBaz>,
<InterfaceClass builtins.IFoo>,
<InterfaceClass builtins.IBlat>,
<implementedBy ...object>,
classImplements(object),
<InterfaceClass zope.interface.Interface>)

Tagged Values
Expand Down
2 changes: 1 addition & 1 deletion docs/api/declarations.rst
Expand Up @@ -624,7 +624,7 @@ an instance:
... def __call__(self):
... return self
>>> implementedBy(Callable())
<implementedBy builtins.?>
classImplements(builtins.?)

Note that the name of the spec ends with a '?', because the ``Callable``
instance does not have a ``__name__`` attribute.
Expand Down
28 changes: 14 additions & 14 deletions docs/verify.rst
Expand Up @@ -49,7 +49,7 @@ defined.
.. doctest::

>>> verify_foo()
The object <Foo...> has failed to implement interface <...IFoo>:
The object <Foo...> has failed to implement interface ...IFoo:
Does not declaratively implement the interface
The base.IBase.x attribute was not provided
The module.IFoo.y attribute was not provided
Expand All @@ -61,7 +61,7 @@ declaring the correct interface.

>>> Foo.x = Foo.y = 42
>>> verify_foo()
The object <Foo...> has failed to implement interface <...IFoo>: Does not declaratively implement the interface.
The object <Foo...> has failed to implement interface ...IFoo: Does not declaratively implement the interface.

If we want to only check the structure of the object, without examining
its declarations, we can use the ``tentative`` argument.
Expand Down Expand Up @@ -119,13 +119,13 @@ exception.
... class Foo(object):
... x = 1
>>> verify_foo()
The object <Foo...> has failed to implement interface <...IFoo>: The module.IFoo.y attribute was not provided.
The object <Foo...> has failed to implement interface ...IFoo: The module.IFoo.y attribute was not provided.
>>> @implementer(IFoo)
... class Foo(object):
... def __init__(self):
... self.y = 2
>>> verify_foo()
The object <Foo...> has failed to implement interface <...IFoo>: The base.IBase.x attribute was not provided.
The object <Foo...> has failed to implement interface ...IFoo: The base.IBase.x attribute was not provided.

If both attributes are missing, an exception is raised reporting
both errors.
Expand All @@ -136,7 +136,7 @@ both errors.
... class Foo(object):
... pass
>>> verify_foo()
The object <Foo ...> has failed to implement interface <...IFoo>:
The object <Foo ...> has failed to implement interface ...IFoo:
The base.IBase.x attribute was not provided
The module.IFoo.y attribute was not provided

Expand All @@ -155,7 +155,7 @@ when trying to get its value, the attribute is considered missing:
... def x(self):
... raise AttributeError
>>> verify_foo()
The object <Foo...> has failed to implement interface <...IFoo>: The module.IFoo.x attribute was not provided.
The object <Foo...> has failed to implement interface ...IFoo: The module.IFoo.x attribute was not provided.


Any other exception raised by a property will propagate to the caller of
Expand Down Expand Up @@ -203,7 +203,7 @@ that takes one argument. If we don't provide it, we get an error.
... class Foo(object):
... pass
>>> verify_foo()
The object <Foo...> has failed to implement interface <...IFoo>: The module.IFoo.simple(arg1) attribute was not provided.
The object <Foo...> has failed to implement interface ...IFoo: The module.IFoo.simple(arg1) attribute was not provided.

Once they exist, they are checked to be callable, and for compatible signatures.

Expand All @@ -213,7 +213,7 @@ Not being callable is an error.

>>> Foo.simple = 42
>>> verify_foo()
The object <Foo...> has failed to implement interface <...IFoo>: The contract of module.IFoo.simple(arg1) is violated because '42' is not a method.
The object <Foo...> has failed to implement interface ...IFoo: The contract of module.IFoo.simple(arg1) is violated because '42' is not a method.

Taking too few arguments is an error. (Recall that the ``self``
argument is implicit.)
Expand All @@ -222,15 +222,15 @@ argument is implicit.)

>>> Foo.simple = lambda self: "I take no arguments"
>>> verify_foo()
The object <Foo...> has failed to implement interface <...IFoo>: The contract of module.IFoo.simple(arg1) is violated because '<lambda>()' doesn't allow enough arguments.
The object <Foo...> has failed to implement interface ...IFoo: The contract of module.IFoo.simple(arg1) is violated because '<lambda>()' doesn't allow enough arguments.

Requiring too many arguments is an error.

.. doctest::

>>> Foo.simple = lambda self, a, b: "I require two arguments"
>>> verify_foo()
The object <Foo...> has failed to implement interface <...IFoo>: The contract of module.IFoo.simple(arg1) is violated because '<lambda>(a, b)' requires too many arguments.
The object <Foo...> has failed to implement interface ...IFoo: The contract of module.IFoo.simple(arg1) is violated because '<lambda>(a, b)' requires too many arguments.

Variable arguments can be used to implement the required number, as
can arguments with defaults.
Expand All @@ -257,7 +257,7 @@ variable keyword arguments, the implementation must also accept them.
... class Foo(object):
... def needs_kwargs(self, a=1, b=2): pass
>>> verify_foo()
The object <Foo...> has failed to implement interface <...IFoo>: The contract of module.IFoo.needs_kwargs(**kwargs) is violated because 'Foo.needs_kwargs(a=1, b=2)' doesn't support keyword arguments.
The object <Foo...> has failed to implement interface ...IFoo: The contract of module.IFoo.needs_kwargs(**kwargs) is violated because 'Foo.needs_kwargs(a=1, b=2)' doesn't support keyword arguments.

>>> oname, __name__ = __name__, 'module'
>>> class IFoo(Interface):
Expand All @@ -267,7 +267,7 @@ variable keyword arguments, the implementation must also accept them.
... class Foo(object):
... def needs_varargs(self, **kwargs): pass
>>> verify_foo()
The object <Foo...> has failed to implement interface <...IFoo>: The contract of module.IFoo.needs_varargs(*args) is violated because 'Foo.needs_varargs(**kwargs)' doesn't support variable arguments.
The object <Foo...> has failed to implement interface ...IFoo: The contract of module.IFoo.needs_varargs(*args) is violated because 'Foo.needs_varargs(**kwargs)' doesn't support variable arguments.

Of course, missing attributes are also found and reported, and the
source interface of the missing attribute is included. Similarly, when
Expand All @@ -288,7 +288,7 @@ the failing method is from a parent class, that is also reported.
... class Foo(Base):
... pass
>>> verify_foo()
The object <Foo...> has failed to implement interface <...IFoo>:
The object <Foo...> has failed to implement interface ...IFoo:
The contract of base.IBase.method(arg1) is violated because 'Base.method()' doesn't allow enough arguments
The module.IFoo.x attribute was not provided

Expand All @@ -313,4 +313,4 @@ attributes, cannot be verified.
... print(e)

>>> verify_foo_class()
The object <class 'Foo'> has failed to implement interface <...IFoo>: The contract of base.IBase.method(arg1) is violated because 'Base.method(self)' doesn't allow enough arguments.
The object <class 'Foo'> has failed to implement interface ...IFoo: The contract of base.IBase.method(arg1) is violated because 'Base.method(self)' doesn't allow enough arguments.
130 changes: 114 additions & 16 deletions src/zope/interface/declarations.py
Expand Up @@ -144,6 +144,43 @@ def _add_interfaces_to_cls(interfaces, cls):
])
return interfaces + (implemented_by_cls,)

@staticmethod
def _argument_names_for_repr(interfaces):
# These don't actually have to be interfaces, they could be other
# Specification objects like Implements. Also, the first
# one is typically/nominally the cls.
ordered_names = []
names = set()
for iface in interfaces:
duplicate_transform = repr
if isinstance(iface, InterfaceClass):
# Special case to get 'foo.bar.IFace'
# instead of '<InterfaceClass foo.bar.IFace>'
this_name = iface.__name__
duplicate_transform = str
elif isinstance(iface, type):
# Likewise for types. (Ignoring legacy old-style
# classes.)
this_name = iface.__name__
duplicate_transform = _implements_name
elif (isinstance(iface, Implements)
and not iface.declared
and iface.inherit in interfaces):
# If nothing is declared, there's no need to even print this;
# it would just show as ``classImplements(Class)``, and the
# ``Class`` has typically already.
continue
else:
this_name = repr(iface)

already_seen = this_name in names
names.add(this_name)
if already_seen:
this_name = duplicate_transform(iface)

ordered_names.append(this_name)
return ', '.join(ordered_names)


class _ImmutableDeclaration(Declaration):
# A Declaration that is immutable. Used as a singleton to
Expand Down Expand Up @@ -286,7 +323,14 @@ def changed(self, originally_changed):
return super(Implements, self).changed(originally_changed)

def __repr__(self):
return '<implementedBy %s>' % (self.__name__)
if self.inherit:
name = getattr(self.inherit, '__name__', None) or _implements_name(self.inherit)
else:
name = self.__name__
declared_names = self._argument_names_for_repr(self.declared)
if declared_names:
declared_names = ', ' + declared_names
return 'classImplements(%s%s)' % (name, declared_names)

def __reduce__(self):
return implementedBy, (self.inherit, )
Expand Down Expand Up @@ -762,15 +806,44 @@ def __init__(self, cls, *interfaces):
self._cls = cls
Declaration.__init__(self, *self._add_interfaces_to_cls(interfaces, cls))

# Added to by ``moduleProvides``, et al
_v_module_names = ()

def __repr__(self):
return "<%s.%s for instances of %s providing %s>" % (
self.__class__.__module__,
self.__class__.__name__,
self._cls,
self.__args[1:],
# The typical way to create instances of this
# object is via calling ``directlyProvides(...)`` or ``alsoProvides()``,
# but that's not the only way. Proxies, for example,
# directly use the ``Provides(...)`` function (which is the
# more generic method, and what we pickle as). We're after the most
# readable, useful repr in the common case, so we use the most
# common name.
#
# We also cooperate with ``moduleProvides`` to attempt to do the
# right thing for that API. See it for details.
function_name = 'directlyProvides'
if self._cls is ModuleType and self._v_module_names:
# See notes in ``moduleProvides``/``directlyProvides``
providing_on_module = True
interfaces = self.__args[1:]
else:
providing_on_module = False
interfaces = (self._cls,) + self.__bases__
ordered_names = self._argument_names_for_repr(interfaces)
if providing_on_module:
mod_names = self._v_module_names
if len(mod_names) == 1:
mod_names = "sys.modules[%r]" % mod_names[0]
ordered_names = (
'%s, ' % (mod_names,)
) + ordered_names
return "%s(%s)" % (
function_name,
ordered_names,
)

def __reduce__(self):
# This reduces to the Provides *function*, not
# this class.
return Provides, self.__args

__module__ = 'zope.interface'
Expand Down Expand Up @@ -841,7 +914,11 @@ def directlyProvides(object, *interfaces): # pylint:disable=redefined-builtin
# that provides some extra caching
object.__provides__ = ClassProvides(object, cls, *interfaces)
else:
object.__provides__ = Provides(cls, *interfaces)
provides = object.__provides__ = Provides(cls, *interfaces)
# See notes in ``moduleProvides``.
if issubclass(cls, ModuleType) and hasattr(object, '__name__'):
provides._v_module_names += (object.__name__,)



def alsoProvides(object, *interfaces): # pylint:disable=redefined-builtin
Expand Down Expand Up @@ -907,11 +984,19 @@ def __init__(self, cls, metacls, *interfaces):
Declaration.__init__(self, *self._add_interfaces_to_cls(interfaces, metacls))

def __repr__(self):
return "<%s.%s for %s>" % (
self.__class__.__module__,
self.__class__.__name__,
self._cls,
)
# There are two common ways to get instances of this object:
# The most interesting way is calling ``@provider(..)`` as a decorator
# of a class; this is the same as calling ``directlyProvides(cls, ...)``.
#
# The other way is by default: anything that invokes ``implementedBy(x)``
# will wind up putting an instance in ``type(x).__provides__``; this includes
# the ``@implementer(...)`` decorator. Those instances won't have any
# interfaces.
#
# Thus, as our repr, we go with the ``directlyProvides()`` syntax.
interfaces = (self._cls, ) + self.__args[2:]
ordered_names = self._argument_names_for_repr(interfaces)
return "directlyProvides(%s)" % (ordered_names,)

def __reduce__(self):
return self.__class__, self.__args
Expand Down Expand Up @@ -1026,7 +1111,7 @@ def moduleProvides(*interfaces):
This function is provided for convenience. It provides a more convenient
way to call directlyProvides. For example::
moduleImplements(I1)
moduleProvides(I1)
is equivalent to::
Expand All @@ -1035,7 +1120,7 @@ def moduleProvides(*interfaces):
frame = sys._getframe(1) # pylint:disable=protected-access
locals = frame.f_locals # pylint:disable=redefined-builtin

# Try to make sure we were called from a class def
# Try to make sure we were called from a module body
if (locals is not frame.f_globals) or ('__name__' not in locals):
raise TypeError(
"moduleProvides can only be used from a module definition.")
Expand All @@ -1044,8 +1129,21 @@ def moduleProvides(*interfaces):
raise TypeError(
"moduleProvides can only be used once in a module definition.")

locals["__provides__"] = Provides(ModuleType,
*_normalizeargs(interfaces))
# Note: This is cached based on the key ``(ModuleType, *interfaces)``;
# One consequence is that any module that provides the same interfaces
# gets the same ``__repr__``, meaning that you can't tell what module
# such a declaration came from. Adding the module name to ``_v_module_names``
# attempts to correct for this; it works in some common situations, but fails
# (1) after pickling (the data is lost) and (2) if declarations are
# actually shared and (3) if the alternate spelling of ``directlyProvides()``
# is used. Problem (3) is fixed by cooperating with ``directlyProvides``
# to maintain this information, and problem (2) is worked around by
# printing all the names, but (1) is unsolvable without introducing
# new classes or changing the stored data...but it doesn't actually matter,
# because ``ModuleType`` can't be pickled!
p = locals["__provides__"] = Provides(ModuleType,
*_normalizeargs(interfaces))
p._v_module_names += (locals['__name__'],)


##############################################################################
Expand Down

0 comments on commit 4a686fc

Please sign in to comment.