Source code for anodi
# anodi
from backports import inspect
empty = inspect.Signature.empty
[docs]def returns (annotation):
"""
Decorator to add ``annotation`` to ``func``'s ``return``
annotation, as though it were a Python 3 ``-> ...`` annotation.
>>> from anodi import returns
>>> @returns(int)
... def example ():
... pass
...
>>> example.__annotations__
{'return': <type 'int'>}
"""
def annotate (func):
func.__annotations__ = getattr(func, '__annotations__', {})
if not annotation is empty:
func.__annotations__['return'] = annotation
return func
return annotate
[docs]def annotated (func=None, returns=empty):
"""
Decorator to treat ``func``'s default args as a combination of
annotations and default values, migrating the annotations to
``func.__annotations__``, leaving only the defaults in
``__defaults__``).
The optional ``returns`` keyword parameter is placed in the
resulting ``__annotations__`` dict.
Each default value must be a tuple, ``(annotation, default)``. To
supply an unannotated parameter with a default value, use the
``empty`` marker object. To supply an annotation without a
default value, use a 1-tuple: ``(annotation,)``.
Note that the Python 2.x rules prohibiting non-default parameters
from coming after defaults still apply, but we don't enforce those
rules. The effect of using the ``(annotation,)`` form *after*
using the ``(annotation, default)`` form is likely to be
surprising, at best.
You may specify an unannotated parameter by using an empty tuple
as its default value. This is to allow placing unannotated
parameters after annotated parameters. Ordinarily, this would not
be allowed, since the annotated parameter would mark the start of
default values, requiring defaults on all subsequent parameters.
We do *not* support nested tuple parameters.
We also don't yet have a way to add annotations to the ``*args``
or ``**kwargs`` catch-all parameters, since they don't take
defaults.
Example:
>>> from anodi import annotated, empty
>>> @annotated
... def example (a, b, c=(int,), d=(), e=(empty, "hi")):
... pass
...
>>> example.__annotations__
{'c': <type 'int'>}
>>> example.__defaults__
('hi',)
>>> @annotated(returns=int)
... def example (a, b, c=(int,), d=(), e=(empty, "hi")):
... pass
...
>>> example.__annotations__
{'c': <type 'int'>, 'return': <type 'int'>}
>>> example.__defaults__
('hi',)
"""
def annotate (func):
func.__annotations__ = getattr(func, '__annotations__', {})
if not returns == empty:
func.__annotations__['return'] = returns
defaults = func.__defaults__
if defaults:
spec = inspect.getfullargspec(func)
# ___TODO:___ support *args, **kwargs annotation?
# extract annotations
nanno = len(defaults)
for (i, name) in enumerate(spec.args[-nanno:]):
if len(defaults[i]) < 1 or defaults[i][0] is empty:
continue
func.__annotations__[name] = defaults[i][0]
# prune annotations, leaving only defaults
defaults = tuple((d[1]
for d in func.__defaults__
if len(d) > 1))
# use ``None`` if there are no defaults left, since that's
# how a function without any defaults would come out.
func.__defaults__ = defaults or None
return func
# if we were called without a ``results`` argument, then we're
# directly decorating ``func``:
if returns == empty:
return annotate(func)
# otherwise, we're indirectly decorating, via ``annotate``:
return annotate