3030
3131from IPython .utils .decorators import undoc
3232
33-
34- from typing import Self , LiteralString
33+ import types
34+ from typing import Self , LiteralString , get_type_hints
3535
3636if sys .version_info < (3 , 12 ):
3737 from typing_extensions import TypeAliasType
@@ -403,6 +403,9 @@ class EvaluationContext:
403403 class_transients : dict | None = None
404404 #: Instance variable name used in the method definition
405405 instance_arg_name : str | None = None
406+ #: Currently associated value
407+ #: Useful for adding items to _Duck on annotated assignment
408+ current_value : ast .AST | None = None
406409
407410 def replace (self , / , ** changes ):
408411 """Return a new copy of the context, with specified changes"""
@@ -566,6 +569,30 @@ def _validate_policy_overrides(
566569 return all_good
567570
568571
572+ def _is_type_annotation (obj ) -> bool :
573+ """
574+ Returns True if obj is a type annotation, False otherwise.
575+ """
576+ if isinstance (obj , type ):
577+ return True
578+ if isinstance (obj , types .GenericAlias ):
579+ return True
580+ if hasattr (types , "UnionType" ) and isinstance (obj , types .UnionType ):
581+ return True
582+ if isinstance (obj , (typing ._SpecialForm , typing ._BaseGenericAlias )):
583+ return True
584+ if isinstance (obj , typing .TypeVar ):
585+ return True
586+ # Types that support __class_getitem__
587+ if isinstance (obj , type ) and hasattr (obj , "__class_getitem__" ):
588+ return True
589+ # Fallback: check if get_origin returns something
590+ if hasattr (typing , "get_origin" ) and get_origin (obj ) is not None :
591+ return True
592+
593+ return False
594+
595+
569596def _handle_assign (node : ast .Assign , context : EvaluationContext ):
570597 value = eval_node (node .value , context )
571598 transient_locals = context .transient_locals
@@ -664,12 +691,17 @@ def _handle_assign(node: ast.Assign, context: EvaluationContext):
664691
665692
666693def _handle_annassign (node , context ):
667- annotation_value = _resolve_annotation (eval_node (node .annotation , context ), context )
668-
669- # Use Value for generic types
670- use_value = (
671- isinstance (annotation_value , GENERIC_CONTAINER_TYPES ) and node .value is not None
672- )
694+ context_with_value = context .replace (current_value = getattr (node , "value" , None ))
695+ annotation_result = eval_node (node .annotation , context_with_value )
696+ if _is_type_annotation (annotation_result ):
697+ annotation_value = _resolve_annotation (annotation_result , context )
698+ # Use Value for generic types
699+ use_value = (
700+ isinstance (annotation_value , GENERIC_CONTAINER_TYPES ) and node .value is not None
701+ )
702+ else :
703+ annotation_value = annotation_result
704+ use_value = False
673705
674706 # LOCAL VARIABLE
675707 if getattr (node , "simple" , False ) and isinstance (node .target , ast .Name ):
@@ -801,9 +833,12 @@ def eval_node(node: Union[ast.AST, None], context: EvaluationContext):
801833
802834 if is_property :
803835 if return_type is not None :
804- context .transient_locals [node .name ] = _resolve_annotation (
805- return_type , context
806- )
836+ if _is_type_annotation (return_type ):
837+ context .transient_locals [node .name ] = _resolve_annotation (
838+ return_type , context
839+ )
840+ else :
841+ context .transient_locals [node .name ] = return_type
807842 else :
808843 return_value = _infer_return_value (node , func_context )
809844 context .transient_locals [node .name ] = return_value
@@ -814,7 +849,10 @@ def dummy_function(*args, **kwargs):
814849 pass
815850
816851 if return_type is not None :
817- dummy_function .__annotations__ ["return" ] = return_type
852+ if _is_type_annotation (return_type ):
853+ dummy_function .__annotations__ ["return" ] = return_type
854+ else :
855+ dummy_function .__inferred_return__ = return_type
818856 else :
819857 inferred_return = _infer_return_value (node , func_context )
820858 if inferred_return is not None :
@@ -952,6 +990,29 @@ def dummy_function(*args, **kwargs):
952990 if isinstance (node , ast .BinOp ):
953991 left = eval_node (node .left , context )
954992 right = eval_node (node .right , context )
993+ if (
994+ isinstance (node .op , ast .BitOr )
995+ and _is_type_annotation (left )
996+ and _is_type_annotation (right )
997+ ):
998+ left_duck = (
999+ _Duck (dict .fromkeys (dir (left )))
1000+ if policy .can_call (left .__dir__ )
1001+ else _Duck ()
1002+ )
1003+ right_duck = (
1004+ _Duck (dict .fromkeys (dir (right )))
1005+ if policy .can_call (right .__dir__ )
1006+ else _Duck ()
1007+ )
1008+ value_node = context .current_value
1009+ if value_node is not None and isinstance (value_node , ast .Dict ):
1010+ if dict in [left , right ]:
1011+ return _merge_values (
1012+ [left_duck , right_duck , ast .literal_eval (value_node )],
1013+ policy = get_policy (context ),
1014+ )
1015+ return _merge_values ([left_duck , right_duck ], policy = get_policy (context ))
9551016 dunders = _find_dunder (node .op , BINARY_OP_DUNDERS )
9561017 if dunders :
9571018 if policy .can_operate (dunders , left , right ):
@@ -1061,6 +1122,22 @@ def dummy_function(*args, **kwargs):
10611122 value = eval_node (node .value , context )
10621123 if policy .can_get_attr (value , node .attr ):
10631124 return getattr (value , node .attr )
1125+ try :
1126+ cls = (
1127+ value if isinstance (value , type ) else getattr (value , "__class__" , None )
1128+ )
1129+ if cls is not None :
1130+ resolved_hints = get_type_hints (
1131+ cls ,
1132+ globalns = (context .globals or {}),
1133+ localns = (context .locals or {}),
1134+ )
1135+ if node .attr in resolved_hints :
1136+ annotated = resolved_hints [node .attr ]
1137+ return _resolve_annotation (annotated , context )
1138+ except Exception :
1139+ # Fall through to the guard rejection
1140+ pass
10641141 raise GuardRejection (
10651142 "Attribute access (`__getattr__`) for" ,
10661143 type (value ), # not joined to avoid calling `repr`
0 commit comments