[Python] Sphinx Compatible Forwarding Patterns in Python
We develop a Python example that showcases the forwarding pattern while handling docstrings in a Sphinx compatible way. We maintain this compatibility in two ways: first, using a metaclass, and second, using decorators. Along the way, we discover a few things about binding instance and class attributes.
A Simple Example¶
We define two classes: a SimpleWrapper class that forwards to a Wrappee class.
SimpleWrapper¶
class SimpleWrapper(object):
def __init__(self, wrappee):
self.wrappee = wrappee
def foo(self, val):
"""
Forward to wrappee.foo()
"""
return self.wrappee(val)
Wrappee¶
class Wrappee(object):
forwarded_methods = ('foo',)
def __init__(self, offset):
self.offset = float(offset)
def foo(self, val):
"""
Args:
val(float): the input
Returns:
float: val, plus offset
"""
return val + self.offset
If it was just a single method, e.g. SimpleWrapper.foo()
, something like the above would probably be sufficient.
- However, suppose we have multiple methods to forward. In this scenario we might prefer
- to streamline the definition of the Wrapper, and
- to have Sphinx pick up the docstrings from the Wrappee
Streamlining Wrapper¶
We propose that the definition of Wrapper should not include repetitive, boilerplate forwarding code. On the other hand, we want Sphinx to pick up the forwarded methods.
To achieve these two goals, we use a metaclass to build the Wrapper type and loop over the forwarded_methods assigning the functions to the new class. When Sphinx examines a Wrapper class, it will extract the boilerplate forwarding code because the metaclass added definitions for each of the forwarding methods.
WrapperMetaclass¶
class WrapperMetaclass(type):
def __init__(cls, name, bases, dct):
super(WrapperMetaclass, cls).__init__(name, bases, dct)
for method_name in Wrappee.forwarded_methods:
setattr(cls, method_name, Wrappee.__dict__[method_name])
Wrapper¶
There is some small additional complexity in Wrapper wherein we must modify the magic function
__getattribute__()
to return foo()
as a bound method to the wrappee instance.
In this case, overriding __getattr__()
will not suffice as the fallback behavior of __getattr__()
will create the wrong binding, a binding to self.
class Wrapper(object):
__metaclass__ = WrapperMetaclass
def __init__(self, wrappee):
self.wrappee = wrappee
def bar(self):
"""
A placeholder to compare against the 'foo' method installed
by the metaclass
"""
pass
def __getattribute__(self, attr):
if attr in Wrappee.forwarded_methods:
f = type(self).__dict__[attr]
return f.__get__(self.wrappee, Wrappee)
else:
return super(Wrapper, self).__getattribute__(attr)
Decorators¶
Alternatively, suppose that we prefer self-documenting boilerplate code. In this case, only the docstrings must be updated, and we can do this without recourse to metaclasses.
Decorated Wrapper¶
We use a parameterized decorator which, itself, returns a decorator.
def forwarded(cls):
def decorator(f):
attr = getattr(cls, f.__name__)
f.__doc__ = attr.__doc__
return f
return decorator
Application of the parameterized decorator is apparent: one simply passes the Wrappee class.
class DecoratedWrapper(object):
def __init__(self, wrappee):
self.wrappee = wrappee
@forwarded(Wrappee)
def foo(self, val):
self.wrappee.foo(val)
Results¶
It is interesting to consider the various ways of accessing a function and the implications on the respective calling contexts.
In [1]: from Forwarding import *
In [2]: wrappee = Wrappee(1)
In [3]: wrapper = Wrapper(wrappee)
In [4]: type(wrapper).__dict__['foo']
Out[4]: <function Forwarding.foo>
In [5]: type(wrapper).__dict__['bar']
Out[5]: <function Forwarding.bar>
In [6]: getattr(wrapper, 'foo')
Out[6]: <bound method Wrappee.foo of <Forwarding.Wrappee object at 0x7f6c0f5ba3d0>>
In [7]: getattr(wrapper, 'bar')
Out[7]: <bound method Wrapper.bar of <Forwarding.Wrapper object at 0x7f6c0f5ba410>>
Note that the class __dict__ defines both foo()
and bar()
as functions. However, getattr()
binds the former to the Wrappee instance and the latter to the Wrapper instance. This is the behavior we wanted.
Moreover, the docstring is preserved from the Wrappee class.
In [8]: print wrapper.foo.__doc__
Args:
val(float): the input
Returns:
float: val, plus offset
Autoclass Behavior¶
For both the metaclass and decorator appraoches, when we use the Sphinx autoclass directive, we get exactly what we expect.
-
class
Forwarding.
Wrapper
(wrappee)¶ -
bar
()¶ A placeholder to compare against the ‘foo’ method installed by the metaclass
-
foo
(val)¶ Parameters: val (float) – the input Returns: float – val, plus offset
-
The decorated method DecoratedWrapper.foo()
has the same docstring and, accordingly, generates the same Sphinx documentation.