Skip to content

Commit 922049b

Browse files
gh-130907: Treat all module-level annotations as conditional (#131550)
1 parent 5bf0f36 commit 922049b

21 files changed

+221
-53
lines changed

Include/internal/pycore_compile.h

+2
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ int _PyCompile_EnterScope(struct _PyCompiler *c, identifier name, int scope_type
133133
void _PyCompile_ExitScope(struct _PyCompiler *c);
134134
Py_ssize_t _PyCompile_AddConst(struct _PyCompiler *c, PyObject *o);
135135
_PyInstructionSequence *_PyCompile_InstrSequence(struct _PyCompiler *c);
136+
int _PyCompile_StartAnnotationSetup(struct _PyCompiler *c);
137+
int _PyCompile_EndAnnotationSetup(struct _PyCompiler *c);
136138
int _PyCompile_FutureFeatures(struct _PyCompiler *c);
137139
void _PyCompile_DeferredAnnotations(
138140
struct _PyCompiler *c, PyObject **deferred_annotations,

Include/internal/pycore_instruction_sequence.h

+5
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ typedef struct instruction_sequence {
4545

4646
/* PyList of instruction sequences of nested functions */
4747
PyObject *s_nested;
48+
49+
/* Code for creating annotations, spliced into the main sequence later */
50+
struct instruction_sequence *s_annotations_code;
4851
} _PyInstructionSequence;
4952

5053
typedef struct {
@@ -66,6 +69,8 @@ _PyJumpTargetLabel _PyInstructionSequence_NewLabel(_PyInstructionSequence *seq);
6669
int _PyInstructionSequence_ApplyLabelMap(_PyInstructionSequence *seq);
6770
int _PyInstructionSequence_InsertInstruction(_PyInstructionSequence *seq, int pos,
6871
int opcode, int oparg, _Py_SourceLocation loc);
72+
int _PyInstructionSequence_SetAnnotationsCode(_PyInstructionSequence *seq,
73+
_PyInstructionSequence *annotations);
6974
int _PyInstructionSequence_AddNested(_PyInstructionSequence *seq, _PyInstructionSequence *nested);
7075
void PyInstructionSequence_Fini(_PyInstructionSequence *seq);
7176

Include/internal/pycore_opcode_metadata.h

+16-8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/opcode_ids.h

+11-10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/_opcode_metadata.py

+11-10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_compiler_codegen.py

+4
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ def test_if_expression(self):
2626
false_lbl = self.Label()
2727
expected = [
2828
('RESUME', 0, 0),
29+
('ANNOTATIONS_PLACEHOLDER', None),
2930
('LOAD_CONST', 0, 1),
3031
('TO_BOOL', 0, 1),
3132
('POP_JUMP_IF_FALSE', false_lbl := self.Label(), 1),
@@ -45,6 +46,7 @@ def test_for_loop(self):
4546
false_lbl = self.Label()
4647
expected = [
4748
('RESUME', 0, 0),
49+
('ANNOTATIONS_PLACEHOLDER', None),
4850
('LOAD_NAME', 0, 1),
4951
('GET_ITER', None, 1),
5052
loop_lbl := self.Label(),
@@ -73,6 +75,7 @@ def f(x):
7375
expected = [
7476
# Function definition
7577
('RESUME', 0),
78+
('ANNOTATIONS_PLACEHOLDER', None),
7679
('LOAD_CONST', 0),
7780
('MAKE_FUNCTION', None),
7881
('STORE_NAME', 0),
@@ -106,6 +109,7 @@ def g():
106109
expected = [
107110
# Function definition
108111
('RESUME', 0),
112+
('ANNOTATIONS_PLACEHOLDER', None),
109113
('LOAD_CONST', 0),
110114
('MAKE_FUNCTION', None),
111115
('STORE_NAME', 0),

Lib/test/test_dis.py

+29-16
Original file line numberDiff line numberDiff line change
@@ -381,24 +381,36 @@ def wrap_func_w_kwargs():
381381
# leading newline is for a reason (tests lineno)
382382

383383
dis_annot_stmt_str = """\
384-
0 RESUME 0
384+
-- MAKE_CELL 0 (__conditional_annotations__)
385385
386-
2 LOAD_SMALL_INT 1
387-
STORE_NAME 0 (x)
386+
0 RESUME 0
388387
389-
4 LOAD_SMALL_INT 1
390-
LOAD_NAME 1 (lst)
391-
LOAD_NAME 2 (fun)
392-
PUSH_NULL
393-
LOAD_SMALL_INT 0
394-
CALL 1
395-
STORE_SUBSCR
388+
2 LOAD_CONST 1 (<code object __annotate__ at 0x..., file "<dis>", line 2>)
389+
MAKE_FUNCTION
390+
STORE_NAME 4 (__annotate__)
391+
BUILD_SET 0
392+
STORE_NAME 0 (__conditional_annotations__)
393+
LOAD_SMALL_INT 1
394+
STORE_NAME 1 (x)
395+
LOAD_NAME 0 (__conditional_annotations__)
396+
LOAD_SMALL_INT 0
397+
SET_ADD 1
398+
POP_TOP
396399
397-
2 LOAD_CONST 1 (<code object __annotate__ at 0x..., file "<dis>", line 2>)
398-
MAKE_FUNCTION
399-
STORE_NAME 3 (__annotate__)
400-
LOAD_CONST 2 (None)
401-
RETURN_VALUE
400+
3 LOAD_NAME 0 (__conditional_annotations__)
401+
LOAD_SMALL_INT 1
402+
SET_ADD 1
403+
POP_TOP
404+
405+
4 LOAD_SMALL_INT 1
406+
LOAD_NAME 2 (lst)
407+
LOAD_NAME 3 (fun)
408+
PUSH_NULL
409+
LOAD_SMALL_INT 0
410+
CALL 1
411+
STORE_SUBSCR
412+
LOAD_CONST 2 (None)
413+
RETURN_VALUE
402414
"""
403415

404416
fn_with_annotate_str = """
@@ -995,7 +1007,8 @@ def test_boundaries(self):
9951007
def test_widths(self):
9961008
long_opcodes = set(['JUMP_BACKWARD_NO_INTERRUPT',
9971009
'LOAD_FAST_BORROW_LOAD_FAST_BORROW',
998-
'INSTRUMENTED_CALL_FUNCTION_EX'])
1010+
'INSTRUMENTED_CALL_FUNCTION_EX',
1011+
'ANNOTATIONS_PLACEHOLDER'])
9991012
for op, opname in enumerate(dis.opname):
10001013
if opname in long_opcodes or opname.startswith("INSTRUMENTED"):
10011014
continue

Lib/test/test_grammar.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -383,9 +383,10 @@ def test_var_annot_simple_exec(self):
383383
gns = {}; lns = {}
384384
exec("'docstring'\n"
385385
"x: int = 5\n", gns, lns)
386+
self.assertNotIn('__annotate__', gns)
387+
388+
gns.update(lns) # __annotate__ looks at globals
386389
self.assertEqual(lns["__annotate__"](annotationlib.Format.VALUE), {'x': int})
387-
with self.assertRaises(KeyError):
388-
gns['__annotate__']
389390

390391
def test_var_annot_rhs(self):
391392
ns = {}

Lib/test/test_type_annotations.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import textwrap
44
import types
55
import unittest
6-
from test.support import run_code, check_syntax_error, cpython_only
6+
from test.support import run_code, check_syntax_error, import_helper, cpython_only
77
from test.test_inspect import inspect_stringized_annotations
88

99

@@ -151,6 +151,14 @@ class D(metaclass=C):
151151
del D.__annotations__
152152
self.assertEqual(D.__annotations__, {})
153153

154+
def test_partially_executed_module(self):
155+
partialexe = import_helper.import_fresh_module("test.typinganndata.partialexecution")
156+
self.assertEqual(
157+
partialexe.a.__annotations__,
158+
{"v1": int, "v2": int},
159+
)
160+
self.assertEqual(partialexe.b.annos, {"v1": int})
161+
154162
@cpython_only
155163
def test_no_cell(self):
156164
# gh-130924: Test that uses of annotations in local scopes do not
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import a
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
v1: int
2+
3+
from . import b
4+
5+
v2: int
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from . import a
2+
3+
annos = a.__annotations__

Makefile.pre.in

+1
Original file line numberDiff line numberDiff line change
@@ -2679,6 +2679,7 @@ TESTSUBDIRS= idlelib/idle_test \
26792679
test/translationdata/getopt \
26802680
test/translationdata/optparse \
26812681
test/typinganndata \
2682+
test/typinganndata/partialexecution \
26822683
test/wheeldata \
26832684
test/xmltestdata \
26842685
test/xmltestdata/c14n-20 \
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
If the ``__annotations__`` of a module object are accessed while the
2+
module is executing, return the annotations that have been defined so far,
3+
without caching them.

Objects/moduleobject.c

+21-1
Original file line numberDiff line numberDiff line change
@@ -1246,6 +1246,25 @@ module_get_annotations(PyObject *self, void *Py_UNUSED(ignored))
12461246

12471247
PyObject *annotations;
12481248
if (PyDict_GetItemRef(dict, &_Py_ID(__annotations__), &annotations) == 0) {
1249+
PyObject *spec;
1250+
if (PyDict_GetItemRef(m->md_dict, &_Py_ID(__spec__), &spec) < 0) {
1251+
Py_DECREF(dict);
1252+
return NULL;
1253+
}
1254+
bool is_initializing = false;
1255+
if (spec != NULL) {
1256+
int rc = _PyModuleSpec_IsInitializing(spec);
1257+
if (rc < 0) {
1258+
Py_DECREF(spec);
1259+
Py_DECREF(dict);
1260+
return NULL;
1261+
}
1262+
Py_DECREF(spec);
1263+
if (rc) {
1264+
is_initializing = true;
1265+
}
1266+
}
1267+
12491268
PyObject *annotate;
12501269
int annotate_result = PyDict_GetItemRef(dict, &_Py_ID(__annotate__), &annotate);
12511270
if (annotate_result < 0) {
@@ -1273,7 +1292,8 @@ module_get_annotations(PyObject *self, void *Py_UNUSED(ignored))
12731292
annotations = PyDict_New();
12741293
}
12751294
Py_XDECREF(annotate);
1276-
if (annotations) {
1295+
// Do not cache annotations if the module is still initializing
1296+
if (annotations && !is_initializing) {
12771297
int result = PyDict_SetItem(
12781298
dict, &_Py_ID(__annotations__), annotations);
12791299
if (result) {

Python/bytecodes.c

+4
Original file line numberDiff line numberDiff line change
@@ -2026,6 +2026,10 @@ dummy_func(
20262026
}
20272027
}
20282028

2029+
pseudo(ANNOTATIONS_PLACEHOLDER, (--)) = {
2030+
NOP,
2031+
};
2032+
20292033
inst(DICT_UPDATE, (dict, unused[oparg - 1], update -- dict, unused[oparg - 1])) {
20302034
PyObject *dict_o = PyStackRef_AsPyObjectBorrow(dict);
20312035
PyObject *update_o = PyStackRef_AsPyObjectBorrow(update);

0 commit comments

Comments
 (0)