Repository: zopefoundation/zope.interface (language: python)

Issue:
Update __repr__ and __str__ for (at least) Provides and InterfaceClass?
(Moved from https://github.com/zopefoundation/zope.interface/pull/234#discussion_r596750717) 

Given `spec = Provides(AClass, IFoo, IBar)`, the repr will be something like 
```
<zope.interface.Provides for instances of <class 'some.module.AClass'> providing (<InterfaceClass some.module.interfaces.IFoo>, <InterfaceClass some.module.interfaces.IBar>)>
```

Informative, but not terse.

Basically none of these objects have a `__str__` method.

`__repr__` is meant to "look like a valid Python expression that could be used to recreate an object with the same value (given an appropriate environment)."

In contrast `__str__`  computes "the 'informal' string representation of an object. This differs from `__repr__()` in that it does not have to be a valid Python expression: a more convenient or concise representation may be used instead. The return value must be a string object."

For `Provides`, it would be pretty easy to give a `repr` that actually can be evaluated (in many cases) outputting something like
```
Provides(AClass, IFoo, IBar)
```

Similarly, interfaces could have a `__str__` that just outputs `__module__.__name__`. 

That would transform reports from 
```
Object <InterfaceClass BTrees.Interfaces.ISet> has different legacy and C3 MROs:
  Legacy RO (len=7)                                  C3 RO (len=7; inconsistent=no)
  ====================================================================================================
    <InterfaceClass BTrees.Interfaces.ISet>            <InterfaceClass BTrees.Interfaces.ISet>
    <InterfaceClass BTrees.Interfaces.IKeySequence>    <InterfaceClass BTrees.Interfaces.IKeySequence>
  - <InterfaceClass BTrees.Interfaces.ISized>
    <InterfaceClass BTrees.Interfaces.ISetMutable>     <InterfaceClass BTrees.Interfaces.ISetMutable>
    <InterfaceClass BTrees.Interfaces.IKeyed>          <InterfaceClass BTrees.Interfaces.IKeyed>
    <InterfaceClass BTrees.Interfaces.ICollection>     <InterfaceClass BTrees.Interfaces.ICollection>
                                                     + <InterfaceClass BTrees.Interfaces.ISized>
    <InterfaceClass zope.interface.Interface>          <InterfaceClass zope.interface.Interface>
```

to
```
Object <InterfaceClass BTrees.Interfaces.ISet> has different legacy and C3 MROs:
  Legacy RO (len=7)                                  C3 RO (len=7; inconsistent=no)
  ==================================================================================
    BTrees.Interfaces.ISet            BTrees.Interfaces.ISet
    BTrees.Interfaces.IKeySequence    BTrees.Interfaces.IKeySequence
  - BTrees.Interfaces.ISized
    BTrees.Interfaces.ISetMutable     BTrees.Interfaces.ISetMutable
    BTrees.Interfaces.IKeyed          BTrees.Interfaces.IKeyed
    BTrees.Interfaces.ICollection     BTrees.Interfaces.ICollection
                                    + BTrees.Interfaces.ISized
    zope.interface.Interface          zope.interface.Interface
```

(With similar benefits when `Provides` is present.)

Since the changes in `Provides` in #234 mean that there *might* have to be doctest updates, and since adding a `__str__` mostly shouldn't (but might!) cause doctest updates, this might be a good time (before 5.3) to make such a change.

What do you think?


Discussion / hints:
Pending discussion of this, I'd like to go ahead and publish the trunk to PyPI as zope.interface 5.3.0a1; that will unblock zope.component to move forward (https://github.com/zopefoundation/zope.component/pull/53 and https://github.com/zopefoundation/zope.component/pull/55).

If we decide to go ahead with these changes, I can make the PR, and then we can release that as 5.3.0. 

If we decide not to make these changes, I can release 5.3.0 with no changes from 5.3.0a1.

I'll wait at least an hour before taking any action.
I like it.

Fix patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 1cbff80b..600fbb8d 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -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)
 ==================
 
diff --git a/docs/README.rst b/docs/README.rst
index 3daf8652..b8eef135 100644
--- a/docs/README.rst
+++ b/docs/README.rst
@@ -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
@@ -694,11 +694,11 @@ 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
@@ -706,13 +706,13 @@ that lists the specification and all of it's ancestors:
   ... 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
diff --git a/docs/api/declarations.rst b/docs/api/declarations.rst
index 05d39eb4..cdbf815f 100644
--- a/docs/api/declarations.rst
+++ b/docs/api/declarations.rst
@@ -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.
diff --git a/docs/verify.rst b/docs/verify.rst
index bf602d0f..6a313cd4 100644
--- a/docs/verify.rst
+++ b/docs/verify.rst
@@ -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
@@ -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.
@@ -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.
@@ -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
 
@@ -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
@@ -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.
 
@@ -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.)
@@ -222,7 +222,7 @@ 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.
 
@@ -230,7 +230,7 @@ Requiring too many arguments is an error.
 
    >>> 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.
@@ -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):
@@ -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
@@ -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
 
@@ -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.
diff --git a/src/zope/interface/declarations.py b/src/zope/interface/declarations.py
index 45d29980..935b026a 100644
--- a/src/zope/interface/declarations.py
+++ b/src/zope/interface/declarations.py
@@ -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
@@ -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, )
@@ -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'
@@ -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
@@ -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
@@ -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::
 
@@ -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.")
@@ -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__'],)
 
 
 ##############################################################################
diff --git a/src/zope/interface/interface.py b/src/zope/interface/interface.py
index 53049e2d..74476418 100644
--- a/src/zope/interface/interface.py
+++ b/src/zope/interface/interface.py
@@ -890,10 +890,10 @@ def validateInvariants(self, obj, errors=None):
                 try:
                     invariant(obj)
                 except Invalid as error:
-                     if errors is not None:
-                         errors.append(error)
-                     else:
-                         raise
+                    if errors is not None:
+                        errors.append(error)
+                    else:
+                        raise
 
         if errors:
             raise Invalid(errors)
@@ -925,18 +925,22 @@ def getTaggedValueTags(self):
             keys.update(base.getDirectTaggedValueTags())
         return keys
 
-    def __repr__(self):  # pragma: no cover
+    def __repr__(self):
         try:
             return self._v_repr
         except AttributeError:
-            name = self.__name__
-            m = self.__ibmodule__
-            if m:
-                name = '%s.%s' % (m, name)
+            name = str(self)
             r = "<%s %s>" % (self.__class__.__name__, name)
             self._v_repr = r # pylint:disable=attribute-defined-outside-init
             return r
 
+    def __str__(self):
+        name = self.__name__
+        m = self.__ibmodule__
+        if m:
+            name = '%s.%s' % (m, name)
+        return name
+
     def _call_conform(self, conform):
         try:
             return conform(self)


Test patch:
diff --git a/src/zope/interface/tests/test_declarations.py b/src/zope/interface/tests/test_declarations.py
index 0c21b8ce..8efe2d80 100644
--- a/src/zope/interface/tests/test_declarations.py
+++ b/src/zope/interface/tests/test_declarations.py
@@ -384,7 +384,7 @@ def test_ctor_no_bases(self):
     def test___repr__(self):
         impl = self._makeOne()
         impl.__name__ = 'Testing'
-        self.assertEqual(repr(impl), '<implementedBy Testing>')
+        self.assertEqual(repr(impl), 'classImplements(Testing)')
 
     def test___reduce__(self):
         from zope.interface.declarations import implementedBy
@@ -544,7 +544,6 @@ class Foo(object):
     def test_builtins_added_to_cache(self):
         from zope.interface import declarations
         from zope.interface.declarations import Implements
-        from zope.interface._compat import _BUILTINS
         with _MonkeyDict(declarations,
                          'BuiltinImplementationSpecifications') as specs:
             self.assertEqual(list(self._callFUT(tuple)), [])
@@ -554,8 +553,8 @@ def test_builtins_added_to_cache(self):
                 spec = specs[typ]
                 self.assertIsInstance(spec, Implements)
                 self.assertEqual(repr(spec),
-                                 '<implementedBy %s.%s>'
-                                 % (_BUILTINS, typ.__name__))
+                                 'classImplements(%s)'
+                                 % (typ.__name__,))
 
     def test_builtins_w_existing_cache(self):
         from zope.interface import declarations
@@ -1304,25 +1303,6 @@ def _test():
             return foo.__provides__
         self.assertRaises(AttributeError, _test)
 
-    def test__repr__(self):
-        from zope.interface.interface import InterfaceClass
-        IFoo = InterfaceClass("IFoo")
-        assert IFoo.__name__ == 'IFoo'
-        assert IFoo.__module__ == __name__
-        assert repr(IFoo) == '<InterfaceClass %s.IFoo>' % (__name__,)
-
-        IBar = InterfaceClass("IBar")
-
-        inst = self._makeOne(type(self), IFoo, IBar)
-        self.assertEqual(
-            repr(inst),
-            "<zope.interface.Provides "
-            "for instances of <class '%(mod)s.ProvidesClassTests'> "
-            "providing (<InterfaceClass %(mod)s.IFoo>, <InterfaceClass %(mod)s.IBar>)>"  % {
-                'mod': __name__,
-            }
-        )
-
 
 class ProvidesClassStrictTests(ProvidesClassTests):
     # Tests that require the strict C3 resolution order.
@@ -1334,9 +1314,6 @@ def _do_calculate_ro(self, base_mros):
                 return ProvidesClass._do_calculate_ro(self, base_mros=base_mros, strict=True)
         return StrictProvides
 
-    def test__repr__(self):
-        self.skipTest("Not useful for the subclass.")
-
     def test_overlapping_interfaces_corrected(self):
         # Giving Provides(cls, IFace), where IFace is already
         # provided by cls, doesn't produce invalid resolution orders.
@@ -1361,6 +1338,195 @@ class Base(object):
         ))
 
 
+class TestProvidesClassRepr(unittest.TestCase):
+
+    def _getTargetClass(self):
+        from zope.interface.declarations import ProvidesClass
+        return ProvidesClass
+
+    def _makeOne(self, *args, **kw):
+        return self._getTargetClass()(*args, **kw)
+
+    def test__repr__(self):
+        from zope.interface.interface import InterfaceClass
+        IFoo = InterfaceClass("IFoo")
+        assert IFoo.__name__ == 'IFoo'
+        assert IFoo.__module__ == __name__
+        assert repr(IFoo) == '<InterfaceClass %s.IFoo>' % (__name__,)
+
+        IBar = InterfaceClass("IBar")
+
+        inst = self._makeOne(type(self), IFoo, IBar)
+        self.assertEqual(
+            repr(inst),
+            "directlyProvides(TestProvidesClassRepr, IFoo, IBar)"
+        )
+
+    def test__repr__module_provides_typical_use(self):
+        # as created through a ``moduleProvides()`` statement
+        # in a module body
+        from zope.interface.tests import dummy
+        provides = dummy.__provides__ # pylint:disable=no-member
+        self.assertEqual(
+            repr(provides),
+            "directlyProvides(sys.modules['zope.interface.tests.dummy'], IDummyModule)"
+        )
+
+    def test__repr__module_after_pickle(self):
+        # It doesn't matter, these objects can't be pickled.
+        import pickle
+        from zope.interface.tests import dummy
+        provides = dummy.__provides__ # pylint:disable=no-member
+        for proto in range(pickle.HIGHEST_PROTOCOL + 1):
+            with self.assertRaises(pickle.PicklingError):
+                pickle.dumps(provides, proto)
+
+    def test__repr__directlyProvides_module(self):
+        import sys
+        from zope.interface.tests import dummy
+        from zope.interface.declarations import directlyProvides
+        from zope.interface.declarations import alsoProvides
+        from zope.interface.interface import InterfaceClass
+
+        IFoo = InterfaceClass('IFoo')
+        IBar = InterfaceClass('IBar')
+
+        orig_provides = dummy.__provides__ # pylint:disable=no-member
+        del dummy.__provides__ # pylint:disable=no-member
+        self.addCleanup(setattr, dummy, '__provides__', orig_provides)
+
+        directlyProvides(dummy, IFoo)
+        provides = dummy.__provides__ # pylint:disable=no-member
+
+        self.assertEqual(
+            repr(provides),
+            "directlyProvides(sys.modules['zope.interface.tests.dummy'], IFoo)"
+        )
+
+        alsoProvides(dummy, IBar)
+        provides = dummy.__provides__ # pylint:disable=no-member
+
+        self.assertEqual(
+            repr(provides),
+            "directlyProvides(sys.modules['zope.interface.tests.dummy'], IFoo, IBar)"
+        )
+
+        # If we make this module also provide IFoo and IBar, then the repr
+        # lists both names.
+        my_module = sys.modules[__name__]
+        assert not hasattr(my_module, '__provides__')
+
+        directlyProvides(my_module, IFoo, IBar)
+        self.addCleanup(delattr, my_module, '__provides__')
+        self.assertIs(my_module.__provides__, provides)
+        self.assertEqual(
+            repr(provides),
+            "directlyProvides(('zope.interface.tests.dummy', "
+            "'zope.interface.tests.test_declarations'), "
+            "IFoo, IBar)"
+        )
+
+    def test__repr__module_provides_cached_shared(self):
+        from zope.interface.interface import InterfaceClass
+        from zope.interface.declarations import ModuleType
+        IFoo = InterfaceClass("IFoo")
+
+        inst = self._makeOne(ModuleType, IFoo)
+        inst._v_module_names += ('some.module',)
+        inst._v_module_names += ('another.module',)
+        self.assertEqual(
+            repr(inst),
+            "directlyProvides(('some.module', 'another.module'), IFoo)"
+        )
+
+    def test__repr__duplicate_names(self):
+        from zope.interface.interface import InterfaceClass
+        IFoo = InterfaceClass("IFoo", __module__='mod1')
+        IFoo2 = InterfaceClass("IFoo", __module__='mod2')
+        IBaz = InterfaceClass("IBaz")
+
+        inst = self._makeOne(type(self), IFoo, IBaz, IFoo2)
+        self.assertEqual(
+            repr(inst),
+            "directlyProvides(TestProvidesClassRepr, IFoo, IBaz, mod2.IFoo)"
+        )
+
+    def test__repr__implementedBy_in_interfaces(self):
+        from zope.interface import Interface
+        from zope.interface import implementedBy
+        class IFoo(Interface):
+            "Does nothing"
+
+        class Bar(object):
+            "Does nothing"
+
+        impl = implementedBy(type(self))
+
+        inst = self._makeOne(Bar, IFoo, impl)
+        self.assertEqual(
+            repr(inst),
+            'directlyProvides(Bar, IFoo, classImplements(TestProvidesClassRepr))'
+        )
+
+    def test__repr__empty_interfaces(self):
+        inst = self._makeOne(type(self))
+        self.assertEqual(
+            repr(inst),
+            'directlyProvides(TestProvidesClassRepr)',
+        )
+
+    def test__repr__non_class(self):
+        class Object(object):
+            __bases__ = ()
+            __str__ = lambda _: self.fail("Should not call str")
+
+            def __repr__(self):
+                return '<Object>'
+        inst = self._makeOne(Object())
+        self.assertEqual(
+            repr(inst),
+            'directlyProvides(<Object>)',
+        )
+
+    def test__repr__providedBy_from_class(self):
+        from zope.interface.declarations import implementer
+        from zope.interface.declarations import providedBy
+        from zope.interface.interface import InterfaceClass
+        IFoo = InterfaceClass("IFoo")
+
+        @implementer(IFoo)
+        class Foo(object):
+            pass
+
+        inst = providedBy(Foo())
+        self.assertEqual(
+            repr(inst),
+            'classImplements(Foo, IFoo)'
+        )
+
+    def test__repr__providedBy_alsoProvides(self):
+        from zope.interface.declarations import implementer
+        from zope.interface.declarations import providedBy
+        from zope.interface.declarations import alsoProvides
+        from zope.interface.interface import InterfaceClass
+        IFoo = InterfaceClass("IFoo")
+        IBar = InterfaceClass("IBar")
+
+        @implementer(IFoo)
+        class Foo(object):
+            pass
+
+        foo = Foo()
+        alsoProvides(foo, IBar)
+
+        inst = providedBy(foo)
+        self.assertEqual(
+            repr(inst),
+            "directlyProvides(Foo, IBar, classImplements(Foo, IFoo))"
+        )
+
+
+
 class Test_Provides(unittest.TestCase):
 
     def _callFUT(self, *args, **kw):
@@ -1631,13 +1797,6 @@ class Foo(object):
         self.assertEqual(cp.__reduce__(),
                          (type(cp), (Foo, type(Foo), IBar)))
 
-    def test__repr__(self):
-        inst = self._makeOne(type(self), type)
-        self.assertEqual(
-            repr(inst),
-            "<zope.interface.declarations.ClassProvides for %r>"  % type(self)
-        )
-
 
 class ClassProvidesStrictTests(ClassProvidesTests):
     # Tests that require the strict C3 resolution order.
@@ -1649,9 +1808,6 @@ def _do_calculate_ro(self, base_mros):
                 return ClassProvides._do_calculate_ro(self, base_mros=base_mros, strict=True)
         return StrictClassProvides
 
-    def test__repr__(self):
-        self.skipTest("Not useful for the subclass.")
-
     def test_overlapping_interfaces_corrected(self):
         # Giving ClassProvides(cls, metaclass, IFace), where IFace is already
         # provided by metacls, doesn't produce invalid resolution orders.
@@ -1682,6 +1838,85 @@ class metaclass(type):
             Interface
         ))
 
+
+class TestClassProvidesRepr(unittest.TestCase):
+
+    def _getTargetClass(self):
+        from zope.interface.declarations import ClassProvides
+        return ClassProvides
+
+    def _makeOne(self, *args, **kw):
+        return self._getTargetClass()(*args, **kw)
+
+    def test__repr__empty(self):
+        inst = self._makeOne(type(self), type)
+        self.assertEqual(
+            repr(inst),
+            "directlyProvides(TestClassProvidesRepr)"
+        )
+
+    def test__repr__providing_one(self):
+        from zope.interface import Interface
+        class IFoo(Interface):
+            "Does nothing"
+
+        inst = self._makeOne(type(self), type, IFoo)
+        self.assertEqual(
+            repr(inst),
+            "directlyProvides(TestClassProvidesRepr, IFoo)"
+        )
+
+    def test__repr__duplicate_names(self):
+        from zope.interface.interface import InterfaceClass
+        IFoo = InterfaceClass("IFoo", __module__='mod1')
+        IFoo2 = InterfaceClass("IFoo", __module__='mod2')
+        IBaz = InterfaceClass("IBaz")
+
+        inst = self._makeOne(type(self), type, IFoo, IBaz, IFoo2)
+        self.assertEqual(
+            repr(inst),
+            "directlyProvides(TestClassProvidesRepr, IFoo, IBaz, mod2.IFoo)"
+        )
+
+    def test__repr__implementedBy(self):
+        from zope.interface.declarations import implementer
+        from zope.interface.declarations import implementedBy
+        from zope.interface.interface import InterfaceClass
+        IFoo = InterfaceClass("IFoo")
+
+        @implementer(IFoo)
+        class Foo(object):
+            pass
+
+        inst = implementedBy(Foo)
+        self.assertEqual(
+            repr(inst),
+            'classImplements(Foo, IFoo)'
+        )
+
+    def test__repr__implementedBy_generic_callable(self):
+        from zope.interface.declarations import implementedBy
+        # We can't get a __name__ by default, so we get a
+        # module name and a question mark
+        class Callable(object):
+            def __call__(self):
+                return self
+
+        inst = implementedBy(Callable())
+        self.assertEqual(
+            repr(inst),
+            'classImplements(%s.?)' % (__name__,)
+        )
+
+        c = Callable()
+        c.__name__ = 'Callable'
+        inst = implementedBy(c)
+        self.assertEqual(
+            repr(inst),
+            'classImplements(Callable)'
+        )
+
+
 class Test_directlyProvidedBy(unittest.TestCase):
 
     def _callFUT(self, *args, **kw):
diff --git a/src/zope/interface/tests/test_exceptions.py b/src/zope/interface/tests/test_exceptions.py
index a55f522d..ecebf91a 100644
--- a/src/zope/interface/tests/test_exceptions.py
+++ b/src/zope/interface/tests/test_exceptions.py
@@ -36,7 +36,7 @@ def test___str__(self):
         self.assertEqual(
             str(dni),
             "An object has failed to implement interface "
-            "<InterfaceClass zope.interface.tests.test_exceptions.IDummy>: "
+            "zope.interface.tests.test_exceptions.IDummy: "
             "Does not declaratively implement the interface."
         )
 
@@ -45,7 +45,7 @@ def test___str__w_candidate(self):
         self.assertEqual(
             str(dni),
             "The object 'candidate' has failed to implement interface "
-            "<InterfaceClass zope.interface.tests.test_exceptions.IDummy>: "
+            "zope.interface.tests.test_exceptions.IDummy: "
             "Does not declaratively implement the interface."
         )
 
@@ -65,7 +65,7 @@ def test___str__(self):
         self.assertEqual(
             str(dni),
             'An object has failed to implement interface '
-            '<InterfaceClass zope.interface.tests.test_exceptions.IDummy>: '
+            'zope.interface.tests.test_exceptions.IDummy: '
             "The 'missing' attribute was not provided.")
 
     def test___str__w_candidate(self):
@@ -73,7 +73,7 @@ def test___str__w_candidate(self):
         self.assertEqual(
             str(dni),
             'The object \'candidate\' has failed to implement interface '
-            '<InterfaceClass zope.interface.tests.test_exceptions.IDummy>: '
+            'zope.interface.tests.test_exceptions.IDummy: '
             "The 'missing' attribute was not provided.")
 
 
@@ -161,7 +161,7 @@ def test__str__(self):
         self.assertEqual(
             str(dni),
             "The object 'target' has failed to implement interface "
-            "<InterfaceClass zope.interface.tests.test_exceptions.IDummy>:\n"
+            "zope.interface.tests.test_exceptions.IDummy:\n"
             "    The contract of 'aMethod' is violated because I said so\n"
             "    Regular exception"
         )
diff --git a/src/zope/interface/tests/test_ro.py b/src/zope/interface/tests/test_ro.py
index 61f92b68..5542d28f 100644
--- a/src/zope/interface/tests/test_ro.py
+++ b/src/zope/interface/tests/test_ro.py
@@ -259,16 +259,16 @@ def _check_handler_complex_diamond(self):
 
         self.assertEqual('\n'.join(l.rstrip() for l in record.getMessage().splitlines()), """\
 Object <InterfaceClass zope.interface.tests.test_ro.A> has different legacy and C3 MROs:
-  Legacy RO (len=7)                                  C3 RO (len=7; inconsistent=no)
-  ====================================================================================================
-    <InterfaceClass zope.interface.tests.test_ro.A>    <InterfaceClass zope.interface.tests.test_ro.A>
-    <InterfaceClass zope.interface.tests.test_ro.B>    <InterfaceClass zope.interface.tests.test_ro.B>
-  - <InterfaceClass zope.interface.tests.test_ro.E>
-    <InterfaceClass zope.interface.tests.test_ro.C>    <InterfaceClass zope.interface.tests.test_ro.C>
-    <InterfaceClass zope.interface.tests.test_ro.D>    <InterfaceClass zope.interface.tests.test_ro.D>
-                                                     + <InterfaceClass zope.interface.tests.test_ro.E>
-    <InterfaceClass zope.interface.tests.test_ro.F>    <InterfaceClass zope.interface.tests.test_ro.F>
-    <InterfaceClass zope.interface.Interface>          <InterfaceClass zope.interface.Interface>""")
+  Legacy RO (len=7)                 C3 RO (len=7; inconsistent=no)
+  ==================================================================
+    zope.interface.tests.test_ro.A    zope.interface.tests.test_ro.A
+    zope.interface.tests.test_ro.B    zope.interface.tests.test_ro.B
+  - zope.interface.tests.test_ro.E
+    zope.interface.tests.test_ro.C    zope.interface.tests.test_ro.C
+    zope.interface.tests.test_ro.D    zope.interface.tests.test_ro.D
+                                    + zope.interface.tests.test_ro.E
+    zope.interface.tests.test_ro.F    zope.interface.tests.test_ro.F
+    zope.interface.Interface          zope.interface.Interface""")
 
     def test_ExtendedPathIndex_implement_thing_implementedby_super(self):
         # See https://github.com/zopefoundation/zope.interface/pull/182#issuecomment-598754056