-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
PEP 677: Runtime Behavior Specification #2237
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
b25eec4
89c313f
2cdf37d
d46ec9c
e8c71b8
c1df8f5
6af28c3
d7b87de
b6aef56
9ecdc43
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -635,24 +635,107 @@ callable types and ``=>`` for lambdas. | |
| Runtime Behavior | ||
| ---------------- | ||
|
|
||
| Our tentative plan is that: | ||
|
|
||
| - The ``__repr__`` will show an arrow syntax literal. | ||
| - We will provide a new API where the runtime data structure can be | ||
| accessed in the same manner as the AST data structure. | ||
| - We will ensure that we provide an API that is backward-compatible | ||
| with ``typing.Callable`` and ``typing.Concatenate``, specifically | ||
| the behavior of ``__args__`` and ``__parameters__``. | ||
| The new AST nodes need to evaluate to runtime types, and we have two goals for the | ||
| behavior of these runtime types: | ||
|
|
||
| - They should expose a structured API that is descriptive and powerful | ||
| enough to be compatible with extending the type to include new features | ||
| like named and variadic arguments. | ||
| - They should also expose an API that is fully backward-compatible with | ||
| ``typing.Callable``. | ||
|
|
||
| Evaluation and Structured API | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
|
||
| We intend to create new builtin types to which the new AST nodes will | ||
| evaluate, exposing them in the ``types`` module. | ||
|
|
||
| Our plan is to expose a structured API as if they were defined as follows:: | ||
|
|
||
| class CallableType: | ||
| is_async: bool | ||
| arguments: Ellipsis | tuple[CallableTypeArgument] | ||
| return_type:: Typing.type | ||
stroxler marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| class CallableTypeArgument: | ||
| kind: CallableTypeArgumentKind | ||
| annotation: typing.Type | typing.TypeVar | typing.ParamSpec | ||
stroxler marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| class CallableTypeArgumentKind(Enum): | ||
|
||
| POSITIONAL_ONLY: int = ... | ||
| PARAM_SPEC: int = ... | ||
|
|
||
|
|
||
| The evaluation rules are expressed in terms of the following | ||
| pseudocode:: | ||
|
|
||
| def evaluate_callable_type(callable_type): | ||
stroxler marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return CallableType( | ||
| is_async=isinstance(callable_type, ast.AsyncCallableType), | ||
| arguments=evaluate_arguments(callable_type.arguments), | ||
| return_type=evaluate_expression(callable_type.returns), | ||
| ) | ||
|
|
||
| def evaluate_arguments(arguments): | ||
| match arguments: | ||
| case ast.AnyArguments(): | ||
| return Ellipsis | ||
| case ast.ArgumentsList(posonlyargs): | ||
| return tuple( | ||
| evaluate_arg(arg) for arg in args | ||
| ) | ||
| case ast.ArgumentsListConcatenation(posonlyargs, param_spec): | ||
| return tuple( | ||
| *(evaluate_arg(arg) for arg in args), | ||
| evaluate_arg(arg=param_spec, kind=PARAM_SPEC) | ||
| ) | ||
| if isinstance(arguments, Any | ||
| return Ellipsis | ||
|
|
||
| def evaluate_arg(arg, kind=POSITIONAL_ONLY): | ||
| return CallableTypeArgument( | ||
| kind=POSITIONAL_ONLY, | ||
| annotation=evaluate_expression(value) | ||
| ) | ||
|
|
||
|
|
||
| Backward-Compatible API | ||
| ~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
|
||
| Because these details are still under debate we are currently | ||
| maintaining `a separate doc | ||
| <https://docs.google.com/document/d/15nmTDA_39Lo-EULQQwdwYx_Q1IYX4dD5WPnHbFG71Lk/edit>`_ | ||
| with details about the new builtins, the evaluation model, how to | ||
| provide both a backward-compatible and more structured API, and | ||
| possible alternatives to the current plan. | ||
| To get backward compatibility with the existing ``types.Callable`` API, | ||
| which relies on fields ``__args__`` and ``__parameters__``, we can define | ||
| them as if they were written in terms of the following:: | ||
|
|
||
| import itertools | ||
| import typing | ||
|
|
||
| def get_args( | ||
| t: CallableType | ||
| ): | ||
stroxler marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return_type_arg = ( | ||
| typing.Awaitable[t.return_type] | ||
| if t.is_async | ||
| else t.return_type | ||
| ) | ||
| arguments = t.arguments | ||
| if isinstance(arguments, Ellipsis): | ||
| argument_args = (Ellipsis,) | ||
| else: | ||
| argument_args = (arg.annotation for arg in arguments) | ||
| return ( | ||
| *arguments_args, | ||
| return_type_arg | ||
| ) | ||
|
|
||
| Additional Behaviors of ``types.CallableType`` | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
|
||
| - The ``__eq__`` method should treat equivalent ``typing.Callable`` | ||
| values as equal to values constructed using the builtin syntax, and | ||
| otherwise should behave like the ``__eq__`` of ``typing.Callable``. | ||
stroxler marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| - The ``__repr__`` method should produce an arrow syntax representation that, | ||
| when evaluated, gives us back an equal ``types.CallableType`` instance. | ||
|
|
||
| Once the plan is finalized we will include a full specification of | ||
| runtime behavior in this section of the PEP. | ||
|
|
||
| Rejected Alternatives | ||
| ===================== | ||
|
|
@@ -908,9 +991,9 @@ We rejected this change because: | |
| syntax errors. | ||
| - Moreover, if a type is complicated enough that readability is a concern | ||
| we can always use type aliases, for example:: | ||
|
|
||
| IntToIntFunction: (int) -> int | ||
|
|
||
| def make_adder() -> IntToIntFunction: | ||
| return lambda x: x + 1 | ||
|
|
||
|
|
@@ -991,6 +1074,46 @@ Moreover, none of these ideas help as much with reducing verbosity | |
| as the current proposal, nor do they introduce as strong a visual cue | ||
| as the ``->`` between the parameter types and the return type. | ||
|
|
||
| Alternative Runtime Behaviors | ||
| ----------------------------- | ||
|
|
||
| The hard requirements on our runtime API are that: | ||
|
|
||
| - It must preserve backward compatibility with ``typing.Callable`` via | ||
| ``__args__`` and ``__params__``. | ||
| - It must provide a structured API, which should be extensible if | ||
| in the future we try to support named and variadic arguments. | ||
|
|
||
| Alternative APIs | ||
| ~~~~~~~~~~~~~~~~ | ||
|
|
||
| We considered having the runtime data ``types.CallableType`` use a | ||
| more structured API where there would be separate fields for | ||
| ``posonlyargs`` and ``param_spec``. The current proposal was | ||
| was inspired by the ``inspect.Signature`` type. | ||
|
|
||
| We use "argument" in our field and type names, unlike "parameter" | ||
| as in ``inspect.Signature``, in order to avoid confusion with | ||
| the ``callable_type.__parameters__`` field from the legacy API | ||
| that refers to type parameters rather than callable parameters. | ||
|
|
||
| Using the plain return type in ``__args__`` for async types | ||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
|
||
| It is debatable whether we are required to preserve backward compatiblity | ||
| of ``__args__`` for async callable types like ``async (int) -> str``. The | ||
| reason is that one could argue they are not expressible directly | ||
| using ``typing.Callable``, and therefore it would be fine to set | ||
| ``__args__`` as ``(int, int)`` rather than ``(int, typing.Awaitable[int])``. | ||
|
|
||
| But we believe this would be problematic. By preserving the appearance | ||
| of a backward-compatible API while actually breaking its semantics on | ||
| async types, we would cause runtime type libraries that attempt to | ||
| interpret ``Callable`` using ``__args__`` to fail silently. | ||
|
|
||
| It is for this reason that we automatically wrap the return type in | ||
| ``Awaitable``. | ||
stroxler marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| Backward Compatibility | ||
| ====================== | ||
|
|
||
|
|
@@ -1033,10 +1156,11 @@ Open Issues | |
| Details of the Runtime API | ||
| -------------------------- | ||
|
|
||
| Once we have finalized all details of the runtime behavior, we | ||
| will need to add a full specification of the behavior to the | ||
| `Runtime Behavior`_ section of this PEP as well as include that | ||
| behavior in our reference implementation. | ||
| We have attempted to provide a complete behavior specification in | ||
| the `Runtime Behavior`_ section of this PEP. | ||
|
|
||
| But there are probably more details that we will not realize we | ||
| need to define until we build a full reference implementation. | ||
|
|
||
| Optimizing ``SyntaxError`` messages | ||
| ----------------------------------- | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.