Update for PEP 362 by Yury Selivanov (mostly) and Larry Hastings.

This commit is contained in:
Brett Cannon 2012-06-05 20:20:53 -04:00
parent 6e2ef0fce7
commit e8131ca8b6
1 changed files with 258 additions and 246 deletions

View File

@ -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: