Update for PEP 362 by Yury Selivanov (mostly) and Larry Hastings.
This commit is contained in:
parent
6e2ef0fce7
commit
e8131ca8b6
504
pep-0362.txt
504
pep-0362.txt
|
@ -2,269 +2,313 @@ PEP: 362
|
|||
Title: Function Signature Object
|
||||
Version: $Revision$
|
||||
Last-Modified: $Date$
|
||||
Author: Brett Cannon <brett@python.org>, Jiwon Seo <seojiwon@gmail.com>
|
||||
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: 2.6
|
||||
Post-History: 05-Sep-2007
|
||||
Python-Version: 3.3
|
||||
Post-History: 04-Jun-2012
|
||||
|
||||
|
||||
Abstract
|
||||
========
|
||||
|
||||
Python has always supported powerful introspection capabilities,
|
||||
including that for functions and methods (for the rest of this PEP the
|
||||
word "function" refers to both functions and methods). Taking a
|
||||
function object, you can fully reconstruct the function's signature.
|
||||
Unfortunately it is a little unruly having to look at all the
|
||||
different attributes to pull together complete information for a
|
||||
function's signature.
|
||||
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 an object representation for function signatures.
|
||||
This should help facilitate introspection on functions for various
|
||||
uses. The introspection information contains all possible information
|
||||
about the parameters in a signature (including Python 3.0 features).
|
||||
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.
|
||||
|
||||
This object, though, is not meant to replace existing ways of
|
||||
introspection on a function's signature. The current solutions are
|
||||
there to make Python's execution work in an efficient manner. The
|
||||
proposed object representation is only meant to help make application
|
||||
code have an easier time to query a function on its signature.
|
||||
|
||||
|
||||
Purpose
|
||||
=======
|
||||
|
||||
An object representation of a function's call signature should provide
|
||||
an easy way to introspect what a function expects as arguments. It
|
||||
does not need to be a "live" representation, though; the signature can
|
||||
be inferred once and stored without changes to the signature object
|
||||
representation affecting the function it represents (but this is an
|
||||
`Open Issues`_).
|
||||
|
||||
Indirection of signature introspection can also occur. If a
|
||||
decorator took a decorated function's signature object and set it on
|
||||
the decorating function then introspection could be redirected to what
|
||||
is actually expected instead of the typical ``*args, **kwargs``
|
||||
signature of decorating functions.
|
||||
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
|
||||
================
|
||||
|
||||
The overall signature of an object is represented by the Signature
|
||||
object. This object is to store a `Parameter object`_ for each
|
||||
parameter in the signature. It is also to store any information
|
||||
about the function itself that is pertinent to the signature.
|
||||
A Signature object represents the overall signature of a function.
|
||||
It stores a `Parameter object`_ for each parameter accepted by the
|
||||
function, as well as information specific to the function itself.
|
||||
|
||||
A Signature object has the following structure attributes:
|
||||
A Signature object has the following public attributes and methods:
|
||||
|
||||
* name : str
|
||||
Name of the function. This is not fully qualified because
|
||||
function objects for methods do not know the class they are
|
||||
contained within. This makes functions and methods
|
||||
indistinguishable from one another when passed to decorators,
|
||||
preventing proper creation of a fully qualified name.
|
||||
* var_args : str
|
||||
Name of the variable positional parameter (i.e., ``*args``), if
|
||||
present, or the empty string.
|
||||
* var_kw_args : str
|
||||
Name of the variable keyword parameter (i.e., ``**kwargs``), if
|
||||
present, or the empty string.
|
||||
* var_annotations: dict(str, object)
|
||||
Dict that contains the annotations for the variable parameters.
|
||||
The keys are of the variable parameter with values of the
|
||||
annotation. If an annotation does not exist for a variable
|
||||
parameter then the key does not exist in the dict.
|
||||
Name of the function.
|
||||
* qualname : str
|
||||
Fully qualified name of the function.
|
||||
* return_annotation : object
|
||||
If present, the attribute is set to the annotation for the return
|
||||
type of the function.
|
||||
* parameters : list(Parameter)
|
||||
List of the parameters of the function as represented by
|
||||
Parameter objects in the order of its definition (keyword-only
|
||||
arguments are in the order listed by ``code.co_varnames``).
|
||||
* bind(\*args, \*\*kwargs) -> dict(str, object)
|
||||
Create a mapping from arguments to parameters. The keys are the
|
||||
names of the parameter that an argument maps to with the value
|
||||
being the value the parameter would have if this function was
|
||||
called with the given arguments.
|
||||
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.
|
||||
|
||||
Signature objects also have the following methods:
|
||||
Once a Signature object is created for a particular function,
|
||||
it's cached in the ``__signature__`` attribute of that function.
|
||||
|
||||
* __getitem__(self, key : str) -> Parameter
|
||||
Returns the Parameter object for the named parameter.
|
||||
* __iter__(self)
|
||||
Returns an iterator that returns Parameter objects in their
|
||||
sequential order based on their 'position' attribute.
|
||||
|
||||
The Signature object is stored in the ``__signature__`` attribute of
|
||||
a function. When it is to be created is discussed in
|
||||
`Open Issues`_.
|
||||
Changes to the Signature object, or to any of its data members,
|
||||
do not affect the function itself.
|
||||
|
||||
|
||||
Parameter Object
|
||||
================
|
||||
|
||||
A function's signature is made up of several parameters. Python's
|
||||
different kinds of parameters is quite large and rich and continues to
|
||||
grow. Parameter objects represent any possible parameter.
|
||||
|
||||
Originally the plan was to represent parameters using a list of
|
||||
parameter names on the Signature object along with various dicts keyed
|
||||
on parameter names to disseminate the various pieces of information
|
||||
one can know about a parameter. But the decision was made to
|
||||
incorporate all information about a parameter in a single object so
|
||||
as to make extending the information easier. This was originally put
|
||||
forth by Talin and the preferred form of Guido (as discussed at the
|
||||
2006 Google Sprint).
|
||||
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 | tuple(str))
|
||||
The name of the parameter as a string if it is not a tuple. If
|
||||
the argument is a tuple then a tuple of strings is used.
|
||||
* position : int
|
||||
The position of the parameter within the signature of the
|
||||
function (zero-indexed). For keyword-only parameters the position
|
||||
value is arbitrary while not conflicting with positional
|
||||
parameters. The suggestion of setting the attribute to None or -1
|
||||
to represent keyword-only parameters was rejected to prevent
|
||||
variable type usage and as a possible point of errors,
|
||||
respectively.
|
||||
* default_value : object
|
||||
The default value for the parameter, if present, else the
|
||||
attribute does not exist.
|
||||
* keyword_only : bool
|
||||
* 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.
|
||||
* annotation
|
||||
Set to the annotation for the parameter. If ``has_annotation`` is
|
||||
False then the attribute does not exist to prevent accidental use.
|
||||
* 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.
|
||||
|
||||
|
||||
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 should be used to invoke functions:
|
||||
::
|
||||
|
||||
def test(a, *, b):
|
||||
...
|
||||
|
||||
sig = signature(test)
|
||||
ba = sig.bind(10, b=20)
|
||||
test(*ba.args, **ba.kwargs)
|
||||
|
||||
|
||||
Implementation
|
||||
==============
|
||||
|
||||
An implementation can be found in Python's sandbox [#impl]_.
|
||||
There is a function named ``signature()`` which
|
||||
returns the value stored on the ``__signature__`` attribute if it
|
||||
exists, else it creates the Signature object for the
|
||||
function and sets ``__signature__``. For methods this is stored
|
||||
directly on the im_func function object since that is what decorators
|
||||
work with.
|
||||
An implementation for Python 3.3 can be found here: [#impl]_.
|
||||
A python issue was also created: [#issue]_.
|
||||
|
||||
The implementation adds a new function ``signature()`` to the
|
||||
``inspect`` module. ``signature()`` returns the value stored
|
||||
on the ``__signature__`` attribute if it exists, otherwise it
|
||||
creates the Signature object for the function and caches it in
|
||||
the function's ``__signature__``. (For methods this is stored
|
||||
directly in the ``__func__`` function object, since that is what
|
||||
decorators work with.)
|
||||
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
Function Signature Renderer
|
||||
---------------------------
|
||||
::
|
||||
|
||||
def render_signature(signature):
|
||||
'''Renders function definition by its signature.
|
||||
|
||||
Example:
|
||||
|
||||
>>> def test(a:'foo', *, b:'bar', c=True, **kwargs:None) -> 'spam':
|
||||
... pass
|
||||
|
||||
>>> render_signature(inspect.signature(test))
|
||||
test(a:'foo', *, b:'bar', c=True, **kwargs:None) -> 'spam'
|
||||
'''
|
||||
|
||||
result = []
|
||||
render_kw_only_separator = True
|
||||
for param in signature.parameters.values():
|
||||
formatted = param.name
|
||||
|
||||
# Add annotation and default value
|
||||
if hasattr(param, 'annotation'):
|
||||
formatted = '{}:{!r}'.format(formatted, param.annotation)
|
||||
if hasattr(param, 'default'):
|
||||
formatted = '{}={!r}'.format(formatted, param.default)
|
||||
|
||||
# Handle *args and **kwargs -like parameters
|
||||
if param.is_args:
|
||||
formatted = '*' + formatted
|
||||
elif param.is_kwargs:
|
||||
formatted = '**' + formatted
|
||||
|
||||
if param.is_args:
|
||||
# OK, we have an '*args'-like parameter, so we won't need
|
||||
# a '*' to separate keyword-only arguments
|
||||
render_kw_only_separator = False
|
||||
elif param.is_keyword_only and render_kw_only_separator:
|
||||
# We have a keyword-only parameter to render and we haven't
|
||||
# rendered an '*args'-like parameter before, so add a '*'
|
||||
# separator to the parameters list ("foo(arg1, *, arg2)" case)
|
||||
result.append('*')
|
||||
# This condition should be only triggered once, so
|
||||
# reset the flag
|
||||
render_kw_only_separator = False
|
||||
|
||||
result.append(formatted)
|
||||
|
||||
rendered = '{}({})'.format(signature.name, ', '.join(result))
|
||||
|
||||
if hasattr(signature, 'return_annotation'):
|
||||
rendered += ' -> {!r}'.format(signature.return_annotation)
|
||||
|
||||
return rendered
|
||||
|
||||
|
||||
Annotation Checker
|
||||
------------------
|
||||
::
|
||||
|
||||
def quack_check(fxn):
|
||||
"""Decorator to verify arguments and return value quack as they should.
|
||||
import inspect
|
||||
import functools
|
||||
|
||||
Positional arguments.
|
||||
>>> @quack_check
|
||||
... def one_arg(x:int): pass
|
||||
...
|
||||
>>> one_arg(42)
|
||||
>>> one_arg('a')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
TypeError: 'a' does not quack like a <type 'int'>
|
||||
def checktypes(func):
|
||||
'''Decorator to verify arguments and return types
|
||||
|
||||
Example:
|
||||
|
||||
*args
|
||||
>>> @quack_check
|
||||
... def var_args(*args:int): pass
|
||||
...
|
||||
>>> var_args(*[1,2,3])
|
||||
>>> var_args(*[1,'b',3])
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
TypeError: *args contains a a value that does not quack like a <type 'int'>
|
||||
>>> @checktypes
|
||||
... def test(a:int, b:str) -> int:
|
||||
... return int(a * b)
|
||||
|
||||
**kwargs
|
||||
>>> @quack_check
|
||||
... def var_kw_args(**kwargs:int): pass
|
||||
...
|
||||
>>> var_kw_args(**{'a': 1})
|
||||
>>> var_kw_args(**{'a': 'A'})
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
TypeError: **kwargs contains a value that does not quack like a <type 'int'>
|
||||
>>> test(10, '1')
|
||||
1111111111
|
||||
|
||||
Return annotations.
|
||||
>>> @quack_check
|
||||
... def returned(x) -> int: return x
|
||||
...
|
||||
>>> returned(42)
|
||||
42
|
||||
>>> returned('a')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
TypeError: the return value 'a' does not quack like a <type 'int'>
|
||||
>>> test(10, 1)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: foo: wrong type of 'b' argument, 'str' expected, got 'int'
|
||||
'''
|
||||
|
||||
"""
|
||||
# Get the signature; only needs to be calculated once.
|
||||
sig = Signature(fxn)
|
||||
def check(*args, **kwargs):
|
||||
# Find out the variable -> value bindings.
|
||||
bindings = sig.bind(*args, **kwargs)
|
||||
# Check *args for the proper quack.
|
||||
sig = inspect.signature(func)
|
||||
|
||||
types = {}
|
||||
for param in sig.parameters.values():
|
||||
# Iterate through function's parameters and build the list of
|
||||
# arguments types
|
||||
try:
|
||||
duck = sig.var_annotations[sig.var_args]
|
||||
except KeyError:
|
||||
pass
|
||||
type_ = param.annotation
|
||||
except AttributeError:
|
||||
continue
|
||||
else:
|
||||
# Check every value in *args.
|
||||
for value in bindings[sig.var_args]:
|
||||
if not isinstance(value, duck):
|
||||
raise TypeError("*%s contains a a value that does not "
|
||||
"quack like a %r" %
|
||||
(sig.var_args, duck))
|
||||
# Remove it from the bindings so as to not check it again.
|
||||
del bindings[sig.var_args]
|
||||
# **kwargs.
|
||||
try:
|
||||
duck = sig.var_annotations[sig.var_kw_args]
|
||||
except (KeyError, AttributeError):
|
||||
pass
|
||||
else:
|
||||
# Check every value in **kwargs.
|
||||
for value in bindings[sig.var_kw_args].values():
|
||||
if not isinstance(value, duck):
|
||||
raise TypeError("**%s contains a value that does not "
|
||||
"quack like a %r" %
|
||||
(sig.var_kw_args, duck))
|
||||
# Remove from bindings so as to not check again.
|
||||
del bindings[sig.var_kw_args]
|
||||
# For each remaining variable ...
|
||||
for var, value in bindings.items():
|
||||
# See if an annotation was set.
|
||||
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:
|
||||
duck = sig[var].annotation
|
||||
default = param.default
|
||||
except AttributeError:
|
||||
continue
|
||||
# Check that the value quacks like it should.
|
||||
if not isinstance(value, duck):
|
||||
raise TypeError('%r does not quack like a %s' % (value, duck))
|
||||
else:
|
||||
# All the ducks quack fine; let the call proceed.
|
||||
returned = fxn(*args, **kwargs)
|
||||
# Check the return value.
|
||||
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:
|
||||
if not isinstance(returned, sig.return_annotation):
|
||||
raise TypeError('the return value %r does not quack like '
|
||||
'a %r' % (returned,
|
||||
sig.return_annotation))
|
||||
except AttributeError:
|
||||
pass
|
||||
return returned
|
||||
# Full-featured version would set function metadata.
|
||||
return check
|
||||
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
|
||||
|
||||
|
||||
Open Issues
|
||||
|
@ -280,54 +324,23 @@ pass a function object to a function and that would generate the
|
|||
Signature object and store it to ``__signature__`` if
|
||||
needed, and then return the value of ``__signature__``.
|
||||
|
||||
|
||||
Should ``Signature.bind`` return Parameter objects as keys?
|
||||
-----------------------------------------------------------
|
||||
|
||||
Instead of returning a dict with keys consisting of the name of the
|
||||
parameters, would it be more useful to instead use Parameter
|
||||
objects? The name of the argument can easily be retrieved from the
|
||||
key (and the name would be used as the hash for a Parameter object).
|
||||
In the current implementation, signatures are created only on demand
|
||||
("lazy").
|
||||
|
||||
|
||||
Have ``var_args`` and ``_var_kw_args`` default to ``None``?
|
||||
------------------------------------------------------------
|
||||
Deprecate ``inspect.getfullargspec()`` and ``inspect.getcallargs()``?
|
||||
---------------------------------------------------------------------
|
||||
|
||||
It has been suggested by Fred Drake that these two attributes have a
|
||||
value of ``None`` instead of empty strings when they do not exist.
|
||||
The answer to this question will influence what the defaults are for
|
||||
other attributes as well.
|
||||
|
||||
|
||||
Deprecate ``inspect.getargspec()`` and ``.formatargspec()``?
|
||||
-------------------------------------------------------------
|
||||
|
||||
Since the Signature object replicates the use of ``getargspec()``
|
||||
from the ``inspect`` module it might make sense to deprecate it in
|
||||
2.6. ``formatargspec()`` could also go if Signature objects gained a
|
||||
__str__ representation.
|
||||
|
||||
Issue with that is types such as ``int``, when used as annotations,
|
||||
do not lend themselves for output (e.g., ``"<type 'int'>"`` is the
|
||||
string represenation for ``int``). The repr representation of types
|
||||
would need to change in order to make this reasonable.
|
||||
|
||||
|
||||
Have the objects be "live"?
|
||||
---------------------------
|
||||
|
||||
Jim Jewett pointed out that Signature and Parameter objects could be
|
||||
"live". That would mean requesting information would be done on the
|
||||
fly instead of caching it on the objects. It would also allow for
|
||||
mutating the function if the Signature or Parameter objects were
|
||||
mutated.
|
||||
Since the Signature object replicates the use of ``getfullargspec()``
|
||||
and ``getcallargs()`` from the ``inspect`` module it might make sense
|
||||
to begin deprecating them in 3.3.
|
||||
|
||||
|
||||
References
|
||||
==========
|
||||
|
||||
.. [#impl] pep362 directory in Python's sandbox
|
||||
(http://svn.python.org/view/sandbox/trunk/pep362/)
|
||||
.. [#impl] pep362 branch (https://bitbucket.org/1st1/cpython/overview)
|
||||
.. [#issue] issue 15008 (http://bugs.python.org/issue15008)
|
||||
|
||||
|
||||
Copyright
|
||||
|
@ -335,7 +348,6 @@ Copyright
|
|||
|
||||
This document has been placed in the public domain.
|
||||
|
||||
|
||||
|
||||
..
|
||||
Local Variables:
|
||||
|
|
Loading…
Reference in New Issue