395 lines
14 KiB
Plaintext
395 lines
14 KiB
Plaintext
PEP: 362
|
||
Title: Function Signature Object
|
||
Version: $Revision$
|
||
Last-Modified: $Date$
|
||
Author: Brett Cannon <brett@python.org>, Jiwon Seo <seojiwon@gmail.com>,
|
||
Yury Selivanov <yselivanov@sprymix.com>, Larry Hastings <larry@hastings.org>
|
||
Status: Draft
|
||
Type: Standards Track
|
||
Content-Type: text/x-rst
|
||
Created: 21-Aug-2006
|
||
Python-Version: 3.3
|
||
Post-History: 04-Jun-2012
|
||
|
||
|
||
Abstract
|
||
========
|
||
|
||
Python has always supported powerful introspection capabilities,
|
||
including introspecting functions and methods (for the rest of
|
||
this PEP, "function" refers to both functions and methods). By
|
||
examining a function object you can fully reconstruct the function's
|
||
signature. Unfortunately this information is stored in an inconvenient
|
||
manner, and is spread across a half-dozen deeply nested attributes.
|
||
|
||
This PEP proposes a new representation for function signatures.
|
||
The new representation contains all necessary information about a function
|
||
and its parameters, and makes introspection easy and straightforward.
|
||
|
||
However, this object does not replace the existing function
|
||
metadata, which is used by Python itself to execute those
|
||
functions. The new metadata object is intended solely to make
|
||
function introspection easier for Python programmers.
|
||
|
||
|
||
Signature Object
|
||
================
|
||
|
||
A Signature object represents the call signature of a function and
|
||
its return annotation. For each parameter accepted by the function
|
||
it stores a `Parameter object`_ in its ``parameters`` collection.
|
||
|
||
A Signature object has the following public attributes and methods:
|
||
|
||
* return_annotation : object
|
||
The annotation for the return type of the function if specified.
|
||
If the function has no annotation for its return type, this
|
||
attribute is not set.
|
||
* parameters : OrderedDict
|
||
An ordered mapping of parameters' names to the corresponding
|
||
Parameter objects (keyword-only arguments are in the same order
|
||
as listed in ``code.co_varnames``).
|
||
* bind(\*args, \*\*kwargs) -> BoundArguments
|
||
Creates a mapping from positional and keyword arguments to
|
||
parameters. Raises a ``BindError`` (subclass of ``TypeError``)
|
||
if the passed arguments do not match the signature.
|
||
* bind_partial(\*args, \*\*kwargs) -> BoundArguments
|
||
Works the same way as ``bind()``, but allows the omission
|
||
of some required arguments (mimics ``functools.partial``
|
||
behavior.)
|
||
* format(...) -> str
|
||
Formats the Signature object to a string. Optional arguments allow
|
||
for custom render functions for parameter names,
|
||
annotations and default values, along with custom separators.
|
||
|
||
Signature implements the ``__str__`` method, which fallbacks to the
|
||
``Signature.format()`` call.
|
||
|
||
It's possible to test Signatures for equality. Two signatures
|
||
are equal when they have equal parameters and return annotations.
|
||
|
||
Changes to the Signature object, or to any of its data members,
|
||
do not affect the function itself.
|
||
|
||
|
||
Parameter Object
|
||
================
|
||
|
||
Python's expressive syntax means functions can accept many different
|
||
kinds of parameters with many subtle semantic differences. We
|
||
propose a rich Parameter object designed to represent any possible
|
||
function parameter.
|
||
|
||
The structure of the Parameter object is:
|
||
|
||
* name : str
|
||
The name of the parameter as a string.
|
||
* default : object
|
||
The default value for the parameter, if specified. If the
|
||
parameter has no default value, this attribute is not set.
|
||
* annotation : object
|
||
The annotation for the parameter if specified. If the
|
||
parameter has no annotation, this attribute is not set.
|
||
* is_keyword_only : bool
|
||
True if the parameter is keyword-only, else False.
|
||
* is_args : bool
|
||
True if the parameter accepts variable number of arguments
|
||
(``*args``-like), else False.
|
||
* is_kwargs : bool
|
||
True if the parameter accepts variable number of keyword
|
||
arguments (``**kwargs``-like), else False.
|
||
* is_implemented : bool
|
||
True if the parameter is implemented for use. Some platforms
|
||
implement functions but can't support specific parameters
|
||
(e.g. "mode" for ``os.mkdir``). Passing in an unimplemented
|
||
parameter may result in the parameter being ignored,
|
||
or in NotImplementedError being raised. It is intended that
|
||
all conditions where ``is_implemented`` may be False be
|
||
thoroughly documented.
|
||
|
||
Two parameters are equal when all their attributes are equal.
|
||
|
||
|
||
BoundArguments Object
|
||
=====================
|
||
|
||
Result of a ``Signature.bind`` call. Holds the mapping of arguments
|
||
to the function's parameters.
|
||
|
||
Has the following public attributes:
|
||
|
||
* arguments : OrderedDict
|
||
An ordered, mutable mapping of parameters' names to arguments' values.
|
||
Does not contain arguments' default values.
|
||
* args : tuple
|
||
Tuple of positional arguments values. Dynamically computed from
|
||
the 'arguments' attribute.
|
||
* kwargs : dict
|
||
Dict of keyword arguments values. Dynamically computed from
|
||
the 'arguments' attribute.
|
||
|
||
The ``arguments`` attribute should be used in conjunction with
|
||
``Signature.parameters`` for any arguments processing purposes.
|
||
|
||
``args`` and ``kwargs`` properties can be used to invoke functions:
|
||
::
|
||
|
||
def test(a, *, b):
|
||
...
|
||
|
||
sig = signature(test)
|
||
ba = sig.bind(10, b=20)
|
||
test(*ba.args, **ba.kwargs)
|
||
|
||
|
||
Implementation
|
||
==============
|
||
|
||
The implementation adds a new function ``signature()`` to the ``inspect``
|
||
module. The function is the preferred way of getting a ``Signature`` for
|
||
a callable object.
|
||
|
||
The function implements the following algorithm:
|
||
|
||
- If the object is not callable - raise a TypeError
|
||
|
||
- If the object has a ``__signature__`` attribute and if it
|
||
is not ``None`` - return a deepcopy of it
|
||
|
||
- If it is ``None`` and the object is an instance of
|
||
``BuiltinFunction``, raise a ``ValueError``
|
||
|
||
- If the object is a an instance of ``FunctionType``:
|
||
|
||
- If it has a ``__wrapped__`` attribute, return
|
||
``signature(object.__wrapped__)``
|
||
|
||
- Or else construct a new ``Signature`` object and return it
|
||
|
||
- If the object is a method or a classmethod, construct and return
|
||
a new ``Signature`` object, with its first parameter (usually
|
||
``self`` or ``cls``) removed
|
||
|
||
- If the object is a staticmethod, construct and return
|
||
a new ``Signature`` object
|
||
|
||
- If the object is an instance of ``functools.partial``, construct
|
||
a new ``Signature`` from its ``partial.func`` attribute, and
|
||
account for already bound ``partial.args`` and ``partial.kwargs``
|
||
|
||
- If the object is a class or metaclass:
|
||
|
||
- If the object's type has a ``__call__`` method defined in
|
||
its MRO, return a Signature for it
|
||
|
||
- If the object has a ``__new__`` method defined in its class,
|
||
return a Signature object for it
|
||
|
||
- If the object has a ``__init__`` method defined in its class,
|
||
return a Signature object for it
|
||
|
||
- Return ``signature(object.__call__)``
|
||
|
||
Note, that the ``Signature`` object is created in a lazy manner, and
|
||
is not automatically cached. If, however, the Signature object was
|
||
explicitly cached by the user, ``signature()`` returns a new deepcopy
|
||
of it on each invocation.
|
||
|
||
An implementation for Python 3.3 can be found at [#impl]_.
|
||
The python issue tracking the patch is [#issue]_.
|
||
|
||
|
||
Design Considerations
|
||
=====================
|
||
|
||
No implicit caching of Signature objects
|
||
----------------------------------------
|
||
|
||
The first PEP design had a provision for implicit caching of ``Signature``
|
||
objects in the ``inspect.signature()`` function. However, this has the
|
||
following downsides:
|
||
|
||
* If the ``Signature`` object is cached then any changes to the function
|
||
it describes will not be reflected in it. However, If the caching is
|
||
needed, it can be always done manually and explicitly
|
||
|
||
* It is better to reserve the ``__signature__`` attribute for the cases
|
||
when there is a need to explicitly set to a ``Signature`` object that
|
||
is different from the actual one
|
||
|
||
|
||
Examples
|
||
========
|
||
|
||
Visualizing Callable Objects' Signature
|
||
---------------------------------------
|
||
::
|
||
|
||
from inspect import signature
|
||
from functools import partial, wraps
|
||
|
||
|
||
class FooMeta(type):
|
||
def __new__(mcls, name, bases, dct, *, bar:bool=False):
|
||
return super().__new__(mcls, name, bases, dct)
|
||
|
||
def __init__(cls, name, bases, dct, **kwargs):
|
||
return super().__init__(name, bases, dct)
|
||
|
||
|
||
class Foo(metaclass=FooMeta):
|
||
def __init__(self, spam:int=42):
|
||
self.spam = spam
|
||
|
||
def __call__(self, a, b, *, c) -> tuple:
|
||
return a, b, c
|
||
|
||
|
||
print('FooMeta >', str(signature(FooMeta)))
|
||
print('Foo >', str(signature(Foo)))
|
||
print('Foo.__call__ >', str(signature(Foo.__call__)))
|
||
print('Foo().__call__ >', str(signature(Foo().__call__)))
|
||
print('partial(Foo().__call__, 1, c=3) >',
|
||
str(signature(partial(Foo().__call__, 1, c=3))))
|
||
print('partial(partial(Foo().__call__, 1, c=3), 2, c=20) >',
|
||
str(signature(partial(partial(Foo().__call__, 1, c=3), 2, c=20))))
|
||
|
||
|
||
The script will output:
|
||
::
|
||
|
||
FooMeta > (name, bases, dct, *, bar:bool=False)
|
||
Foo > (spam:int=42)
|
||
Foo.__call__ > (self, a, b, *, c) -> tuple
|
||
Foo().__call__ > (a, b, *, c) -> tuple
|
||
partial(Foo().__call__, 1, c=3) > (b, *, c=3) -> tuple
|
||
partial(partial(Foo().__call__, 1, c=3), 2, c=20) > (*, c=20) -> tuple
|
||
|
||
|
||
Annotation Checker
|
||
------------------
|
||
::
|
||
|
||
import inspect
|
||
import functools
|
||
|
||
def checktypes(func):
|
||
'''Decorator to verify arguments and return types
|
||
|
||
Example:
|
||
|
||
>>> @checktypes
|
||
... def test(a:int, b:str) -> int:
|
||
... return int(a * b)
|
||
|
||
>>> test(10, '1')
|
||
1111111111
|
||
|
||
>>> test(10, 1)
|
||
Traceback (most recent call last):
|
||
...
|
||
ValueError: foo: wrong type of 'b' argument, 'str' expected, got 'int'
|
||
'''
|
||
|
||
sig = inspect.signature(func)
|
||
|
||
types = {}
|
||
for param in sig.parameters.values():
|
||
# Iterate through function's parameters and build the list of
|
||
# arguments types
|
||
try:
|
||
type_ = param.annotation
|
||
except AttributeError:
|
||
continue
|
||
else:
|
||
if not inspect.isclass(type_):
|
||
# Not a type, skip it
|
||
continue
|
||
|
||
types[param.name] = type_
|
||
|
||
# If the argument has a type specified, let's check that its
|
||
# default value (if present) conforms with the type.
|
||
try:
|
||
default = param.default
|
||
except AttributeError:
|
||
continue
|
||
else:
|
||
if not isinstance(default, type_):
|
||
raise ValueError("{func}: wrong type of a default value for {arg!r}". \
|
||
format(func=sig.qualname, arg=param.name))
|
||
|
||
def check_type(sig, arg_name, arg_type, arg_value):
|
||
# Internal function that incapsulates arguments type checking
|
||
if not isinstance(arg_value, arg_type):
|
||
raise ValueError("{func}: wrong type of {arg!r} argument, " \
|
||
"{exp!r} expected, got {got!r}". \
|
||
format(func=sig.qualname, arg=arg_name,
|
||
exp=arg_type.__name__, got=type(arg_value).__name__))
|
||
|
||
@functools.wraps(func)
|
||
def wrapper(*args, **kwargs):
|
||
# Let's bind the arguments
|
||
ba = sig.bind(*args, **kwargs)
|
||
for arg_name, arg in ba.arguments.items():
|
||
# And iterate through the bound arguments
|
||
try:
|
||
type_ = types[arg_name]
|
||
except KeyError:
|
||
continue
|
||
else:
|
||
# OK, we have a type for the argument, lets get the corresponding
|
||
# parameter description from the signature object
|
||
param = sig.parameters[arg_name]
|
||
if param.is_args:
|
||
# If this parameter is a variable-argument parameter,
|
||
# then we need to check each of its values
|
||
for value in arg:
|
||
check_type(sig, arg_name, type_, value)
|
||
elif param.is_kwargs:
|
||
# If this parameter is a variable-keyword-argument parameter:
|
||
for subname, value in arg.items():
|
||
check_type(sig, arg_name + ':' + subname, type_, value)
|
||
else:
|
||
# And, finally, if this parameter a regular one:
|
||
check_type(sig, arg_name, type_, arg)
|
||
|
||
result = func(*ba.args, **ba.kwargs)
|
||
# The last bit - let's check that the result is correct
|
||
try:
|
||
return_type = sig.return_annotation
|
||
except AttributeError:
|
||
# Looks like we don't have any restriction on the return type
|
||
pass
|
||
else:
|
||
if isinstance(return_type, type) and not isinstance(result, return_type):
|
||
raise ValueError('{func}: wrong return type, {exp} expected, got {got}'. \
|
||
format(func=sig.qualname, exp=return_type.__name__,
|
||
got=type(result).__name__))
|
||
return result
|
||
|
||
return wrapper
|
||
|
||
|
||
References
|
||
==========
|
||
|
||
.. [#impl] pep362 branch (https://bitbucket.org/1st1/cpython/overview)
|
||
.. [#issue] issue 15008 (http://bugs.python.org/issue15008)
|
||
|
||
|
||
Copyright
|
||
=========
|
||
|
||
This document has been placed in the public domain.
|
||
|
||
|
||
..
|
||
Local Variables:
|
||
mode: indented-text
|
||
indent-tabs-mode: nil
|
||
sentence-end-double-space: t
|
||
fill-column: 70
|
||
coding: utf-8
|
||
End:
|