Skip to content

Commit 189d4f1

Browse files
committed
Fix conditional execution of class and module scoped annotations
1 parent 052cb71 commit 189d4f1

10 files changed

Lines changed: 419 additions & 58 deletions

Include/internal/pycore_compile.h

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,9 @@ void _PyCompile_ExitScope(struct _PyCompiler *c);
122122
Py_ssize_t _PyCompile_AddConst(struct _PyCompiler *c, PyObject *o);
123123
_PyInstructionSequence *_PyCompile_InstrSequence(struct _PyCompiler *c);
124124
int _PyCompile_FutureFeatures(struct _PyCompiler *c);
125-
PyObject *_PyCompile_DeferredAnnotations(struct _PyCompiler *c);
125+
void _PyCompile_DeferredAnnotations(
126+
struct _PyCompiler *c, PyObject **deferred_annotations,
127+
PyObject **conditional_annotation_indices);
126128
PyObject *_PyCompile_Mangle(struct _PyCompiler *c, PyObject *name);
127129
PyObject *_PyCompile_MaybeMangle(struct _PyCompiler *c, PyObject *name);
128130
int _PyCompile_MaybeAddStaticAttributeToClass(struct _PyCompiler *c, expr_ty e);
@@ -166,13 +168,16 @@ int _PyCompile_TweakInlinedComprehensionScopes(struct _PyCompiler *c, _Py_Source
166168
_PyCompile_InlinedComprehensionState *state);
167169
int _PyCompile_RevertInlinedComprehensionScopes(struct _PyCompiler *c, _Py_SourceLocation loc,
168170
_PyCompile_InlinedComprehensionState *state);
169-
int _PyCompile_AddDeferredAnnotaion(struct _PyCompiler *c, stmt_ty s);
171+
int _PyCompile_AddDeferredAnnotation(struct _PyCompiler *c, stmt_ty s,
172+
PyObject **conditional_annotation_index);
173+
void _PyCompile_EnterConditionalBlock(struct _PyCompiler *c);
174+
void _PyCompile_LeaveConditionalBlock(struct _PyCompiler *c);
170175

171176
int _PyCodegen_AddReturnAtEnd(struct _PyCompiler *c, int addNone);
172177
int _PyCodegen_EnterAnonymousScope(struct _PyCompiler* c, mod_ty mod);
173178
int _PyCodegen_Expression(struct _PyCompiler *c, expr_ty e);
174-
int _PyCodegen_Body(struct _PyCompiler *c, _Py_SourceLocation loc, asdl_stmt_seq *stmts,
175-
bool is_interactive);
179+
int _PyCodegen_Module(struct _PyCompiler *c, _Py_SourceLocation loc, asdl_stmt_seq *stmts,
180+
bool is_interactive);
176181

177182
/* Utility for a number of growing arrays used in the compiler */
178183
int _PyCompile_EnsureArrayLargeEnough(

Include/internal/pycore_global_objects_fini_generated.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_strings.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ struct _Py_global_strings {
9393
STRUCT_FOR_ID(__classdict__)
9494
STRUCT_FOR_ID(__classdictcell__)
9595
STRUCT_FOR_ID(__complex__)
96+
STRUCT_FOR_ID(__conditional_annotations__)
9697
STRUCT_FOR_ID(__contains__)
9798
STRUCT_FOR_ID(__ctypes_from_outparam__)
9899
STRUCT_FOR_ID(__del__)

Include/internal/pycore_runtime_init_generated.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_symtable.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ typedef struct _symtable_entry {
123123
enclosing class scope */
124124
unsigned ste_has_docstring : 1; /* true if docstring present */
125125
unsigned ste_method : 1; /* true if block is a function block defined in class scope */
126+
unsigned ste_has_conditional_annotations : 1; /* true if block has conditionally executed annotations */
127+
unsigned ste_in_conditional_block : 1; /* set while we are inside a conditionally executed block */
126128
int ste_comp_iter_expr; /* non-zero if visiting a comprehension range expression */
127129
_Py_SourceLocation ste_loc; /* source location of block */
128130
struct _symtable_entry *ste_annotation_block; /* symbol table entry for this entry's annotations */

Include/internal/pycore_unicodeobject_generated.h

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

Lib/test/test_type_annotations.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,3 +457,142 @@ class format: pass
457457
"cannot access free variable 'format' where it is not associated with a value in enclosing scope",
458458
):
459459
ns["f"].__annotations__
460+
461+
462+
class ConditionalAnnotationTests(unittest.TestCase):
463+
def check_scopes(self, code, true_annos, false_annos):
464+
for scope in ("class", "module"):
465+
for (cond, expected) in ((True, true_annos), (False, false_annos)):
466+
with self.subTest(scope=scope, cond=cond):
467+
code_to_run = code.format(cond=cond)
468+
if scope == "class":
469+
code_to_run = "class Cls:\n" + textwrap.indent(textwrap.dedent(code_to_run), " " * 4)
470+
ns = run_code(code_to_run)
471+
if scope == "class":
472+
self.assertEqual(ns["Cls"].__annotations__, expected)
473+
else:
474+
self.assertEqual(ns["__annotate__"](annotationlib.Format.VALUE),
475+
expected)
476+
477+
def test_with(self):
478+
code = """
479+
import contextlib
480+
class Swallower:
481+
def __enter__(self):
482+
pass
483+
484+
def __exit__(self, *args):
485+
return True
486+
487+
with Swallower():
488+
if {cond}:
489+
about_to_raise: int
490+
raise Exception
491+
in_with: "with"
492+
"""
493+
self.check_scopes(code, {"about_to_raise": int}, {"in_with": "with"})
494+
495+
def test_simple_if(self):
496+
code = """
497+
if {cond}:
498+
in_if: "if"
499+
else:
500+
in_if: "else"
501+
"""
502+
self.check_scopes(code, {"in_if": "if"}, {"in_if": "else"})
503+
504+
def test_try(self):
505+
code = """
506+
try:
507+
if {cond}:
508+
raise Exception
509+
in_try: "try"
510+
except Exception:
511+
in_except: "except"
512+
finally:
513+
in_finally: "finally"
514+
"""
515+
self.check_scopes(
516+
code,
517+
{"in_except": "except", "in_finally": "finally"},
518+
{"in_try": "try", "in_finally": "finally"}
519+
)
520+
521+
def test_try_star(self):
522+
code = """
523+
try:
524+
if {cond}:
525+
raise Exception
526+
in_try_star: "try"
527+
except* Exception:
528+
in_except_star: "except"
529+
finally:
530+
in_finally: "finally"
531+
"""
532+
self.check_scopes(
533+
code,
534+
{"in_except_star": "except", "in_finally": "finally"},
535+
{"in_try_star": "try", "in_finally": "finally"}
536+
)
537+
538+
def test_while(self):
539+
code = """
540+
while {cond}:
541+
in_while: "while"
542+
break
543+
else:
544+
in_else: "else"
545+
"""
546+
self.check_scopes(
547+
code,
548+
{"in_while": "while"},
549+
{"in_else": "else"}
550+
)
551+
552+
def test_for(self):
553+
code = """
554+
for _ in ([1] if {cond} else []):
555+
in_for: "for"
556+
else:
557+
in_else: "else"
558+
"""
559+
self.check_scopes(
560+
code,
561+
{"in_for": "for", "in_else": "else"},
562+
{"in_else": "else"}
563+
)
564+
565+
def test_nesting_outer(self):
566+
code = """
567+
if {cond}:
568+
outer_before: "outer_before"
569+
if len:
570+
inner_if: "inner_if"
571+
else:
572+
inner_else: "inner_else"
573+
outer_after: "outer_after"
574+
"""
575+
self.check_scopes(
576+
code,
577+
{"outer_before": "outer_before", "inner_if": "inner_if",
578+
"outer_after": "outer_after"},
579+
{}
580+
)
581+
582+
def test_nesting_inner(self):
583+
code = """
584+
if len:
585+
outer_before: "outer_before"
586+
if {cond}:
587+
inner_if: "inner_if"
588+
else:
589+
inner_else: "inner_else"
590+
outer_after: "outer_after"
591+
"""
592+
self.check_scopes(
593+
code,
594+
{"outer_before": "outer_before", "inner_if": "inner_if",
595+
"outer_after": "outer_after"},
596+
{"outer_before": "outer_before", "inner_else": "inner_else",
597+
"outer_after": "outer_after"},
598+
)

0 commit comments

Comments
 (0)