Skip to content

Commit a6fffba

Browse files
Implement experimental PEP 671 late-bound argument defaults
1 parent 637fb36 commit a6fffba

46 files changed

Lines changed: 1654 additions & 162 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎Doc/howto/index.rst‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Python Library Reference.
3232
instrumentation.rst
3333
perf_profiling.rst
3434
annotations.rst
35+
late-bound-defaults.rst
3536
isolating-extensions.rst
3637
timerfd.rst
3738
mro.rst

‎Doc/howto/late-bound-defaults.rst‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
.. _late-bound-defaults-howto:
2+
3+
Experimental late-bound argument defaults
4+
========================================
5+
6+
.. warning::
7+
8+
This branch implements draft :pep:`671` for experimentation. The PEP has
9+
not been accepted, and this syntax is not available in released Python.
10+
11+
A default written with ``=`` is evaluated when a function is defined. A default
12+
written with ``=>`` is evaluated in the function's local scope when the argument
13+
is omitted::
14+
15+
def add_item(item, target=>[]):
16+
target.append(item)
17+
return target
18+
19+
assert add_item(1) == [1]
20+
assert add_item(2) == [2]
21+
22+
An explicitly supplied argument, including ``None`` or ``Ellipsis``, suppresses
23+
evaluation of its default. Late-bound defaults are evaluated from left to right
24+
and can refer to earlier arguments::
25+
26+
def window(items, start=0, stop=>len(items)):
27+
return items[start:stop]
28+
29+
Positional-only and keyword-only parameters, annotated parameters, methods,
30+
generic functions, and lambdas support the same syntax. All supplied arguments
31+
and early-bound defaults are bound before late-bound defaults are evaluated.
32+
An unbound reference raises :exc:`UnboundLocalError`::
33+
34+
def invalid(value=>value):
35+
return value
36+
37+
# invalid() raises UnboundLocalError; invalid(3) returns 3.
38+
39+
Coroutine and generator prologues run when execution starts, as does their
40+
function body. An argument error is reported before any default is evaluated.
41+
42+
Introspection
43+
-------------
44+
45+
``function.__defaults__`` and ``function.__kwdefaults__`` contain ``Ellipsis``
46+
placeholders for late-bound parameters. ``function.__defaults_extra__`` holds a
47+
parallel tuple of normalized expression strings (or ``None`` for early-bound
48+
entries), and ``function.__kwdefaults_extra__`` holds expression strings keyed
49+
by keyword-only parameter name. Both metadata attributes are ``None`` when no
50+
corresponding late-bound defaults exist. These attributes can be replaced or
51+
deleted using the same tuple/dict restrictions as the ordinary defaults.
52+
53+
``inspect.signature()`` displays the ``=>`` expression. Its parameters expose
54+
the source string through ``Parameter.late_default``. ``getfullargspec()`` keeps
55+
its existing seven-field result; its defaults contain the same placeholders as
56+
the function attributes. Signature binding does not execute expressions.
57+
58+
``ast.arg.late_default`` is an optional string containing the normalized source
59+
expression. The actual expression remains in ``arguments.defaults`` or
60+
``arguments.kw_defaults``. This preserves the existing defaults fields and
61+
allows :func:`ast.unparse` to reconstruct the new syntax.
62+
63+
Implementation notes
64+
--------------------
65+
66+
The call binder leaves omitted late-bound arguments unbound. Function prologues
67+
use ``QUERY_FAST`` and ``QUERY_DEREF`` to determine which expressions to evaluate.
68+
``QUERY_DEREF`` supports parameters captured by closures. Expression descriptions
69+
are captured before AST constant folding. Code objects using late-bound defaults
70+
carry the experimental ``CO_LATE_DEFAULTS`` flag.
71+
72+
Existing call specializations are disabled for these functions because they
73+
assume all arguments are bound before entering a frame. The compiler also treats
74+
all parameters as potentially unbound during local-variable analysis. More
75+
precise analysis and specialized call paths are future performance work.
76+
77+
Two metadata pointers are added to each function object. This prototype changes
78+
the internal function layout and bytecode magic number; extensions must be
79+
rebuilt for it. It does not add public C API entry points.

‎Grammar/python.gram‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -369,13 +369,17 @@ param_no_default_star_annotation[arg_ty]:
369369
| a=param_star_annotation ',' tc=TYPE_COMMENT? { _PyPegen_add_type_comment_to_arg(p, a, tc) }
370370
| a=param_star_annotation tc=TYPE_COMMENT? &')' { _PyPegen_add_type_comment_to_arg(p, a, tc) }
371371
param_with_default[NameDefaultPair*]:
372+
| a=param '=' '>' c=expression ',' tc=TYPE_COMMENT? { _PyPegen_late_default_pair(p, a, c, tc) }
373+
| a=param '=' '>' c=expression tc=TYPE_COMMENT? &')' { _PyPegen_late_default_pair(p, a, c, tc) }
372374
| a=param c=default ',' tc=TYPE_COMMENT? { _PyPegen_name_default_pair(p, a, c, tc) }
373375
| a=param c=default tc=TYPE_COMMENT? &')' { _PyPegen_name_default_pair(p, a, c, tc) }
374376
param_maybe_default[NameDefaultPair*]:
377+
| a=param '=' '>' c=expression ',' tc=TYPE_COMMENT? { _PyPegen_late_default_pair(p, a, c, tc) }
378+
| a=param '=' '>' c=expression tc=TYPE_COMMENT? &')' { _PyPegen_late_default_pair(p, a, c, tc) }
375379
| a=param c=default? ',' tc=TYPE_COMMENT? { _PyPegen_name_default_pair(p, a, c, tc) }
376380
| a=param c=default? tc=TYPE_COMMENT? &')' { _PyPegen_name_default_pair(p, a, c, tc) }
377-
param[arg_ty]: a=NAME b=annotation? { _PyAST_arg(a->v.Name.id, b, NULL, EXTRA) }
378-
param_star_annotation[arg_ty]: a=NAME b=star_annotation { _PyAST_arg(a->v.Name.id, b, NULL, EXTRA) }
381+
param[arg_ty]: a=NAME b=annotation? { _PyAST_arg(a->v.Name.id, b, NULL, NULL, EXTRA) }
382+
param_star_annotation[arg_ty]: a=NAME b=star_annotation { _PyAST_arg(a->v.Name.id, b, NULL, NULL, EXTRA) }
379383
annotation[expr_ty]: ':' a=expression { a }
380384
star_annotation[expr_ty]: ':' a=star_expression { a }
381385
default[expr_ty]: '=' a=expression { a } | invalid_default
@@ -941,12 +945,16 @@ lambda_param_no_default[arg_ty]:
941945
| a=lambda_param ',' { a }
942946
| a=lambda_param &':' { a }
943947
lambda_param_with_default[NameDefaultPair*]:
948+
| a=lambda_param '=' '>' c=expression ',' { _PyPegen_late_default_pair(p, a, c, NULL) }
949+
| a=lambda_param '=' '>' c=expression &':' { _PyPegen_late_default_pair(p, a, c, NULL) }
944950
| a=lambda_param c=default ',' { _PyPegen_name_default_pair(p, a, c, NULL) }
945951
| a=lambda_param c=default &':' { _PyPegen_name_default_pair(p, a, c, NULL) }
946952
lambda_param_maybe_default[NameDefaultPair*]:
953+
| a=lambda_param '=' '>' c=expression ',' { _PyPegen_late_default_pair(p, a, c, NULL) }
954+
| a=lambda_param '=' '>' c=expression &':' { _PyPegen_late_default_pair(p, a, c, NULL) }
947955
| a=lambda_param c=default? ',' { _PyPegen_name_default_pair(p, a, c, NULL) }
948956
| a=lambda_param c=default? &':' { _PyPegen_name_default_pair(p, a, c, NULL) }
949-
lambda_param[arg_ty]: a=NAME { _PyAST_arg(a->v.Name.id, NULL, NULL, EXTRA) }
957+
lambda_param[arg_ty]: a=NAME { _PyAST_arg(a->v.Name.id, NULL, NULL, NULL, EXTRA) }
950958

951959
# LITERALS
952960
# ========

‎Include/cpython/code.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ struct PyCodeObject _PyCode_DEF(1);
131131
#define CO_COROUTINE 0x0080
132132
#define CO_ITERABLE_COROUTINE 0x0100
133133
#define CO_ASYNC_GENERATOR 0x0200
134+
/* Experimental PEP 671 call-time default evaluation. */
135+
#define CO_LATE_DEFAULTS 0x0400
134136

135137
/* bpo-39562: These constant values are changed in Python 3.9
136138
to prevent collision with compiler flags. CO_FUTURE_ and PyCF_

‎Include/cpython/funcobject.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ typedef struct {
3636
typedef struct {
3737
PyObject_HEAD
3838
_Py_COMMON_FIELDS(func_)
39+
PyObject *func_defaults_extra; /* NULL or tuple of expression strings */
40+
PyObject *func_kwdefaults_extra; /* NULL or dict of expression strings */
3941
PyObject *func_doc; /* The __doc__ attribute, can be anything */
4042
PyObject *func_dict; /* The __dict__ attribute, a dict or NULL */
4143
PyObject *func_weakreflist; /* List of weak references */

‎Include/internal/pycore_ast.h‎

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Include/internal/pycore_ast_state.h‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Include/internal/pycore_global_objects_fini_generated.h‎

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Include/internal/pycore_global_strings.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,15 @@ struct _Py_global_strings {
4343
STRUCT_FOR_STR(dbl_open_br, "{{")
4444
STRUCT_FOR_STR(dbl_percent, "%%")
4545
STRUCT_FOR_STR(defaults, ".defaults")
46+
STRUCT_FOR_STR(defaults_extra, ".defaults_extra")
4647
STRUCT_FOR_STR(dot_locals, ".<locals>")
4748
STRUCT_FOR_STR(empty, "")
4849
STRUCT_FOR_STR(format, ".format")
4950
STRUCT_FOR_STR(gc, "<GC>")
5051
STRUCT_FOR_STR(generic_base, ".generic_base")
5152
STRUCT_FOR_STR(json_decoder, "json.decoder")
5253
STRUCT_FOR_STR(kwdefaults, ".kwdefaults")
54+
STRUCT_FOR_STR(kwdefaults_extra, ".kwdefaults_extra")
5355
STRUCT_FOR_STR(list_err, "list index out of range")
5456
STRUCT_FOR_STR(native, "<native>")
5557
STRUCT_FOR_STR(str_replace_inf, "1e309")

‎Include/internal/pycore_magic_number.h‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,8 @@ Known values:
313313
314314
*/
315315

316-
#define PYC_MAGIC_NUMBER 3706
316+
/* 3707: experimental PEP 671 query opcodes and function metadata. */
317+
#define PYC_MAGIC_NUMBER 3707
317318
/* This is equivalent to converting PYC_MAGIC_NUMBER to 2 bytes
318319
(little-endian) and then appending b'\r\n'. */
319320
#define PYC_MAGIC_NUMBER_TOKEN \

0 commit comments

Comments
 (0)