diff --git a/pep-0362.txt b/pep-0362.txt index 833947402..545b01a35 100644 --- a/pep-0362.txt +++ b/pep-0362.txt @@ -2,269 +2,313 @@ PEP: 362 Title: Function Signature Object Version: $Revision$ Last-Modified: $Date$ -Author: Brett Cannon , Jiwon Seo +Author: Brett Cannon , Jiwon Seo , + Yury Selivanov , Larry Hastings 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 + 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 + >>> @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 + >>> 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 + >>> 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., ``""`` 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: