11import collections
22import collections .abc
33import contextlib
4+ import dataclasses
45import functools
56import inspect
67import itertools
@@ -80,24 +81,34 @@ def _make_init_type(v):
8081 return typing .Literal [(v ,)]
8182
8283
84+ def cached_box (cls , * , ctx ):
85+ if str (cls ).startswith ('typemap.typing' ):
86+ return _apply_generic .box (cls )
87+ if cls in ctx .box_cache :
88+ return ctx .box_cache [cls ]
89+ ctx .box_cache [cls ] = box = _apply_generic .box (cls )
90+ assert box .mro
91+ # if not all(b.mro for b in box.mro):
92+ # breakpoint()
93+ # assert all(b.mro for b in box.mro)
94+
95+ if new_box := _eval_init_subclass (box , ctx ):
96+ ctx .box_cache [cls ] = box = new_box
97+ return box
98+
99+
83100def get_annotated_type_hints (cls , * , ctx , ** kwargs ):
84101 """Get the type hints/quals for a cls annotated with definition site.
85102
86103 This traverses the mro and finds the definition site for each annotation.
87104 """
88105
89- # TODO: Cache the box (slash don't need it??)
90- box = _apply_generic .box (cls )
106+ box = cached_box (cls , ctx = ctx )
91107
92108 hints = {}
93109 for abox in reversed (box .mro ):
94110 acls = abox .alias_type ()
95111
96- if abox is box and (updated_cls := _eval_init_subclass (box , ctx )):
97- # For the class itself, apply all UpdateClass from
98- # ancesstors' __init_subclass__ to get the final type.
99- abox = _apply_generic .box (updated_cls )
100-
101112 annos , _ = _apply_generic .get_local_defns (abox )
102113 for k , ty in annos .items ():
103114 quals = set ()
@@ -128,18 +139,12 @@ def get_annotated_type_hints(cls, *, ctx, **kwargs):
128139
129140
130141def get_annotated_method_hints (cls , * , ctx ):
131- # TODO: Cache the box (slash don't need it??)
132- box = _apply_generic .box (cls )
142+ box = cached_box (cls , ctx = ctx )
133143
134144 hints = {}
135145 for abox in reversed (box .mro ):
136146 acls = abox .alias_type ()
137147
138- if abox is box and (updated_cls := _eval_init_subclass (box , ctx )):
139- # For the class itself, apply all UpdateClass from
140- # ancesstors' __init_subclass__ to get the final type.
141- abox = _apply_generic .box (updated_cls )
142-
143148 _ , dct = _apply_generic .get_local_defns (abox )
144149 for name , attr in dct .items ():
145150 if isinstance (
@@ -166,25 +171,38 @@ def get_annotated_method_hints(cls, *, ctx):
166171
167172def _eval_init_subclass (
168173 box : _apply_generic .Boxed , ctx : typing .Any
169- ) -> type | None :
174+ ) -> _apply_generic . Boxed :
170175 """Get type after all __init_subclass__ with UpdateClass are evaluated."""
171- for abox in reversed (box .mro [1 :]): # Skip the type itself
172- if ms := _get_update_class_members (box .cls , abox .alias_type (), ctx = ctx ):
173- return _create_updated_class (box .cls , ms , ctx = ctx )
174-
175- return None
176+ for abox in box .mro [1 :]: # Skip the type itself
177+ with _child_context () as ctx :
178+ if ms := _get_update_class_members (
179+ box .cls , abox .alias_type (), ctx = ctx
180+ ):
181+ nbox = _apply_generic .box (
182+ _create_updated_class (box .cls , ms , ctx = ctx )
183+ )
184+ # We want to preserve the original cls for Members output
185+ box = dataclasses .replace (nbox , orig_cls = box .canonical_cls )
186+ ctx .box_cache [box .cls ] = box
187+ return box
176188
177189
178190def _get_update_class_members (
179191 cls : type , base : type , ctx : typing .Any
180192) -> list [Member ] | None :
181- if (
182- (init_subclass := base .__dict__ .get ("__init_subclass__" ))
183- # XXX: We're using get_type_hints now to evaluate hints but
184- # we should have our own generic infrastructure instead.
185- # (I'm working on it -sully)
186- and (init_subclass_annos := typing .get_type_hints (init_subclass ))
187- and (ret_annotation := init_subclass_annos .get ("return" ))
193+ init_subclass = base .__dict__ .get ("__init_subclass__" )
194+ if not init_subclass :
195+ return None
196+ init_subclass = inspect .unwrap (init_subclass )
197+
198+ args = {}
199+ if type_params := getattr (init_subclass , '__type_params__' , None ):
200+ args [str (type_params [0 ])] = cls
201+
202+ init_subclass_annos = _apply_generic .get_annotations (init_subclass , args )
203+
204+ if init_subclass_annos and (
205+ ret_annotation := init_subclass_annos .get ("return" )
188206 ):
189207 # Substitute the cls type var with the current class
190208 # This may not happen if cls is not generic!
@@ -210,12 +228,7 @@ def _get_update_class_members(
210228 )
211229
212230 # Evaluate the return annotation
213- # Do it in a child context, so the evaluations are isolated. For
214- # example, if the return annotation uses Attrs[MyClass], we want
215- # Attrs[MyClass] to be evaluated with the updated class, not the
216- # original.
217- with _child_context () as ctx :
218- evaled_ret = _eval_types (ret_annotation , ctx = ctx )
231+ evaled_ret = _eval_types (ret_annotation , ctx = ctx )
219232
220233 # If the result is an UpdateClass, return the members
221234 if (
0 commit comments