Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit727cdcb

Browse files
[3.14]gh-137530: generate an __annotate__ function for dataclasses __init__ (GH-137711) (#141352)
(cherry picked from commit12837c6)Co-authored-by: David Ellis <ducksual@gmail.com>
1 parent9221030 commit727cdcb

File tree

3 files changed

+219
-15
lines changed

3 files changed

+219
-15
lines changed

‎Lib/dataclasses.py‎

Lines changed: 80 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -441,9 +441,11 @@ def __init__(self, globals):
441441
self.locals= {}
442442
self.overwrite_errors= {}
443443
self.unconditional_adds= {}
444+
self.method_annotations= {}
444445

445446
defadd_fn(self,name,args,body,*,locals=None,return_type=MISSING,
446-
overwrite_error=False,unconditional_add=False,decorator=None):
447+
overwrite_error=False,unconditional_add=False,decorator=None,
448+
annotation_fields=None):
447449
iflocalsisnotNone:
448450
self.locals.update(locals)
449451

@@ -464,16 +466,14 @@ def add_fn(self, name, args, body, *, locals=None, return_type=MISSING,
464466

465467
self.names.append(name)
466468

467-
ifreturn_typeisnotMISSING:
468-
self.locals[f'__dataclass_{name}_return_type__']=return_type
469-
return_annotation=f'->__dataclass_{name}_return_type__'
470-
else:
471-
return_annotation=''
469+
ifannotation_fieldsisnotNone:
470+
self.method_annotations[name]= (annotation_fields,return_type)
471+
472472
args=','.join(args)
473473
body='\n'.join(body)
474474

475475
# Compute the text of the entire function, add it to the text we're generating.
476-
self.src.append(f'{f'{decorator}\n'ifdecoratorelse''} def{name}({args}){return_annotation}:\n{body}')
476+
self.src.append(f'{f'{decorator}\n'ifdecoratorelse''} def{name}({args}):\n{body}')
477477

478478
defadd_fns_to_class(self,cls):
479479
# The source to all of the functions we're generating.
@@ -509,6 +509,15 @@ def add_fns_to_class(self, cls):
509509
# Now that we've generated the functions, assign them into cls.
510510
forname,fninzip(self.names,fns):
511511
fn.__qualname__=f"{cls.__qualname__}.{fn.__name__}"
512+
513+
try:
514+
annotation_fields,return_type=self.method_annotations[name]
515+
exceptKeyError:
516+
pass
517+
else:
518+
annotate_fn=_make_annotate_function(cls,name,annotation_fields,return_type)
519+
fn.__annotate__=annotate_fn
520+
512521
ifself.unconditional_adds.get(name,False):
513522
setattr(cls,name,fn)
514523
else:
@@ -524,6 +533,44 @@ def add_fns_to_class(self, cls):
524533
raiseTypeError(error_msg)
525534

526535

536+
def_make_annotate_function(__class__,method_name,annotation_fields,return_type):
537+
# Create an __annotate__ function for a dataclass
538+
# Try to return annotations in the same format as they would be
539+
# from a regular __init__ function
540+
541+
def__annotate__(format,/):
542+
Format=annotationlib.Format
543+
matchformat:
544+
caseFormat.VALUE|Format.FORWARDREF|Format.STRING:
545+
cls_annotations= {}
546+
forbaseinreversed(__class__.__mro__):
547+
cls_annotations.update(
548+
annotationlib.get_annotations(base,format=format)
549+
)
550+
551+
new_annotations= {}
552+
forkinannotation_fields:
553+
new_annotations[k]=cls_annotations[k]
554+
555+
ifreturn_typeisnotMISSING:
556+
ifformat==Format.STRING:
557+
new_annotations["return"]=annotationlib.type_repr(return_type)
558+
else:
559+
new_annotations["return"]=return_type
560+
561+
returnnew_annotations
562+
563+
case _:
564+
raiseNotImplementedError(format)
565+
566+
# This is a flag for _add_slots to know it needs to regenerate this method
567+
# In order to remove references to the original class when it is replaced
568+
__annotate__.__generated_by_dataclasses__=True
569+
__annotate__.__qualname__=f"{__class__.__qualname__}.{method_name}.__annotate__"
570+
571+
return__annotate__
572+
573+
527574
def_field_assign(frozen,name,value,self_name):
528575
# If we're a frozen class, then assign to our fields in __init__
529576
# via object.__setattr__. Otherwise, just use a simple
@@ -612,7 +659,7 @@ def _init_param(f):
612659
eliff.default_factoryisnotMISSING:
613660
# There's a factory function. Set a marker.
614661
default='=__dataclass_HAS_DEFAULT_FACTORY__'
615-
returnf'{f.name}:__dataclass_type_{f.name}__{default}'
662+
returnf'{f.name}{default}'
616663

617664

618665
def_init_fn(fields,std_fields,kw_only_fields,frozen,has_post_init,
@@ -635,11 +682,10 @@ def _init_fn(fields, std_fields, kw_only_fields, frozen, has_post_init,
635682
raiseTypeError(f'non-default argument{f.name!r} '
636683
f'follows default argument{seen_default.name!r}')
637684

638-
locals= {**{f'__dataclass_type_{f.name}__':f.typeforfinfields},
639-
**{'__dataclass_HAS_DEFAULT_FACTORY__':_HAS_DEFAULT_FACTORY,
640-
'__dataclass_builtins_object__':object,
641-
}
642-
}
685+
annotation_fields= [f.nameforfinfieldsiff.init]
686+
687+
locals= {'__dataclass_HAS_DEFAULT_FACTORY__':_HAS_DEFAULT_FACTORY,
688+
'__dataclass_builtins_object__':object}
643689

644690
body_lines= []
645691
forfinfields:
@@ -670,7 +716,8 @@ def _init_fn(fields, std_fields, kw_only_fields, frozen, has_post_init,
670716
[self_name]+_init_params,
671717
body_lines,
672718
locals=locals,
673-
return_type=None)
719+
return_type=None,
720+
annotation_fields=annotation_fields)
674721

675722

676723
def_frozen_get_del_attr(cls,fields,func_builder):
@@ -1336,6 +1383,25 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields):
13361383
or_update_func_cell_for__class__(member.fdel,cls,newcls)):
13371384
break
13381385

1386+
# Get new annotations to remove references to the original class
1387+
# in forward references
1388+
newcls_ann=annotationlib.get_annotations(
1389+
newcls,format=annotationlib.Format.FORWARDREF)
1390+
1391+
# Fix references in dataclass Fields
1392+
forfingetattr(newcls,_FIELDS).values():
1393+
try:
1394+
ann=newcls_ann[f.name]
1395+
exceptKeyError:
1396+
pass
1397+
else:
1398+
f.type=ann
1399+
1400+
# Fix the class reference in the __annotate__ method
1401+
init_annotate=newcls.__init__.__annotate__
1402+
ifgetattr(init_annotate,"__generated_by_dataclasses__",False):
1403+
_update_func_cell_for__class__(init_annotate,cls,newcls)
1404+
13391405
returnnewcls
13401406

13411407

‎Lib/test/test_dataclasses/__init__.py‎

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2471,6 +2471,135 @@ def __init__(self, a):
24712471
self.assertEqual(D(5).a,10)
24722472

24732473

2474+
classTestInitAnnotate(unittest.TestCase):
2475+
# Tests for the generated __annotate__ function for __init__
2476+
# See: https://github.com/python/cpython/issues/137530
2477+
2478+
deftest_annotate_function(self):
2479+
# No forward references
2480+
@dataclass
2481+
classA:
2482+
a:int
2483+
2484+
value_annos=annotationlib.get_annotations(A.__init__,format=annotationlib.Format.VALUE)
2485+
forwardref_annos=annotationlib.get_annotations(A.__init__,format=annotationlib.Format.FORWARDREF)
2486+
string_annos=annotationlib.get_annotations(A.__init__,format=annotationlib.Format.STRING)
2487+
2488+
self.assertEqual(value_annos, {'a':int,'return':None})
2489+
self.assertEqual(forwardref_annos, {'a':int,'return':None})
2490+
self.assertEqual(string_annos, {'a':'int','return':'None'})
2491+
2492+
self.assertTrue(getattr(A.__init__.__annotate__,"__generated_by_dataclasses__"))
2493+
2494+
deftest_annotate_function_forwardref(self):
2495+
# With forward references
2496+
@dataclass
2497+
classB:
2498+
b:undefined
2499+
2500+
# VALUE annotations should raise while unresolvable
2501+
withself.assertRaises(NameError):
2502+
_=annotationlib.get_annotations(B.__init__,format=annotationlib.Format.VALUE)
2503+
2504+
forwardref_annos=annotationlib.get_annotations(B.__init__,format=annotationlib.Format.FORWARDREF)
2505+
string_annos=annotationlib.get_annotations(B.__init__,format=annotationlib.Format.STRING)
2506+
2507+
self.assertEqual(forwardref_annos, {'b':support.EqualToForwardRef('undefined',owner=B,is_class=True),'return':None})
2508+
self.assertEqual(string_annos, {'b':'undefined','return':'None'})
2509+
2510+
# Now VALUE and FORWARDREF should resolve, STRING should be unchanged
2511+
undefined=int
2512+
2513+
value_annos=annotationlib.get_annotations(B.__init__,format=annotationlib.Format.VALUE)
2514+
forwardref_annos=annotationlib.get_annotations(B.__init__,format=annotationlib.Format.FORWARDREF)
2515+
string_annos=annotationlib.get_annotations(B.__init__,format=annotationlib.Format.STRING)
2516+
2517+
self.assertEqual(value_annos, {'b':int,'return':None})
2518+
self.assertEqual(forwardref_annos, {'b':int,'return':None})
2519+
self.assertEqual(string_annos, {'b':'undefined','return':'None'})
2520+
2521+
deftest_annotate_function_init_false(self):
2522+
# Check `init=False` attributes don't get into the annotations of the __init__ function
2523+
@dataclass
2524+
classC:
2525+
c:str=field(init=False)
2526+
2527+
self.assertEqual(annotationlib.get_annotations(C.__init__), {'return':None})
2528+
2529+
deftest_annotate_function_contains_forwardref(self):
2530+
# Check string annotations on objects containing a ForwardRef
2531+
@dataclass
2532+
classD:
2533+
d:list[undefined]
2534+
2535+
withself.assertRaises(NameError):
2536+
annotationlib.get_annotations(D.__init__)
2537+
2538+
self.assertEqual(
2539+
annotationlib.get_annotations(D.__init__,format=annotationlib.Format.FORWARDREF),
2540+
{"d":list[support.EqualToForwardRef("undefined",is_class=True,owner=D)],"return":None}
2541+
)
2542+
2543+
self.assertEqual(
2544+
annotationlib.get_annotations(D.__init__,format=annotationlib.Format.STRING),
2545+
{"d":"list[undefined]","return":"None"}
2546+
)
2547+
2548+
# Now test when it is defined
2549+
undefined=str
2550+
2551+
# VALUE should now resolve
2552+
self.assertEqual(
2553+
annotationlib.get_annotations(D.__init__),
2554+
{"d":list[str],"return":None}
2555+
)
2556+
2557+
self.assertEqual(
2558+
annotationlib.get_annotations(D.__init__,format=annotationlib.Format.FORWARDREF),
2559+
{"d":list[str],"return":None}
2560+
)
2561+
2562+
self.assertEqual(
2563+
annotationlib.get_annotations(D.__init__,format=annotationlib.Format.STRING),
2564+
{"d":"list[undefined]","return":"None"}
2565+
)
2566+
2567+
deftest_annotate_function_not_replaced(self):
2568+
# Check that __annotate__ is not replaced on non-generated __init__ functions
2569+
@dataclass(slots=True)
2570+
classE:
2571+
x:str
2572+
def__init__(self,x:int)->None:
2573+
self.x=x
2574+
2575+
self.assertEqual(
2576+
annotationlib.get_annotations(E.__init__), {"x":int,"return":None}
2577+
)
2578+
2579+
self.assertFalse(hasattr(E.__init__.__annotate__,"__generated_by_dataclasses__"))
2580+
2581+
deftest_init_false_forwardref(self):
2582+
# Test forward references in fields not required for __init__ annotations.
2583+
2584+
# At the moment this raises a NameError for VALUE annotations even though the
2585+
# undefined annotation is not required for the __init__ annotations.
2586+
# Ideally this will be fixed but currently there is no good way to resolve this
2587+
2588+
@dataclass
2589+
classF:
2590+
not_in_init:list[undefined]=field(init=False,default=None)
2591+
in_init:int
2592+
2593+
annos=annotationlib.get_annotations(F.__init__,format=annotationlib.Format.FORWARDREF)
2594+
self.assertEqual(
2595+
annos,
2596+
{"in_init":int,"return":None},
2597+
)
2598+
2599+
withself.assertRaises(NameError):
2600+
annos=annotationlib.get_annotations(F.__init__)# NameError on not_in_init
2601+
2602+
24742603
classTestRepr(unittest.TestCase):
24752604
deftest_repr(self):
24762605
@dataclass
@@ -3831,7 +3960,15 @@ def method(self) -> int:
38313960

38323961
returnSlotsTest
38333962

3834-
formakein (make_simple,make_with_annotations,make_with_annotations_and_method):
3963+
defmake_with_forwardref():
3964+
@dataclass(slots=True)
3965+
classSlotsTest:
3966+
x:undefined
3967+
y:list[undefined]
3968+
3969+
returnSlotsTest
3970+
3971+
formakein (make_simple,make_with_annotations,make_with_annotations_and_method,make_with_forwardref):
38353972
withself.subTest(make=make):
38363973
C=make()
38373974
support.gc_collect()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:mod:`dataclasses` Fix annotations for generated ``__init__`` methods by replacing the annotations that were in-line in the generated source code with ``__annotate__`` functions attached to the methods.

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp