99import pickle
1010from string .templatelib import Template , Interpolation
1111import typing
12+ import sys
1213import unittest
1314from annotationlib import (
1415 Format ,
@@ -755,6 +756,8 @@ def test_stringized_annotations_in_module(self):
755756
756757 for kwargs in [
757758 {"eval_str" : True },
759+ {"eval_str" : True , "globals" : isa .__dict__ , "locals" : {}},
760+ {"eval_str" : True , "globals" : {}, "locals" : isa .__dict__ },
758761 {"format" : Format .VALUE , "eval_str" : True },
759762 ]:
760763 with self .subTest (** kwargs ):
@@ -788,7 +791,7 @@ def test_stringized_annotations_in_empty_module(self):
788791 self .assertEqual (get_annotations (isa2 , eval_str = False ), {})
789792
790793 def test_stringized_annotations_with_star_unpack (self ):
791- def f (* args : * tuple [int , ...]): ...
794+ def f (* args : " *tuple[int, ...]" ): ...
792795 self .assertEqual (get_annotations (f , eval_str = True ),
793796 {'args' : (* tuple [int , ...],)[0 ]})
794797
@@ -811,6 +814,44 @@ def test_stringized_annotations_on_wrapper(self):
811814 {"a" : "int" , "b" : "str" , "return" : "MyClass" },
812815 )
813816
817+ def test_stringized_annotations_on_partial_wrapper (self ):
818+ isa = inspect_stringized_annotations
819+
820+ def times_three_str (fn : typing .Callable [[str ], isa .MyClass ]):
821+ @functools .wraps (fn )
822+ def wrapper (b : "str" ) -> "MyClass" :
823+ return fn (b * 3 )
824+
825+ return wrapper
826+
827+ wrapped = times_three_str (functools .partial (isa .function , 1 ))
828+ self .assertEqual (wrapped ("x" ), isa .MyClass (1 , "xxx" ))
829+ self .assertIsNot (wrapped .__globals__ , isa .function .__globals__ )
830+ self .assertEqual (
831+ get_annotations (wrapped , eval_str = True ),
832+ {"b" : str , "return" : isa .MyClass },
833+ )
834+ self .assertEqual (
835+ get_annotations (wrapped , eval_str = False ),
836+ {"b" : "str" , "return" : "MyClass" },
837+ )
838+
839+ # If functools is not loaded, names will be evaluated in the current
840+ # module instead of being unwrapped to the original.
841+ functools_mod = sys .modules ["functools" ]
842+ del sys .modules ["functools" ]
843+
844+ self .assertEqual (
845+ get_annotations (wrapped , eval_str = True ),
846+ {"b" : str , "return" : MyClass },
847+ )
848+ self .assertEqual (
849+ get_annotations (wrapped , eval_str = False ),
850+ {"b" : "str" , "return" : "MyClass" },
851+ )
852+
853+ sys .modules ["functools" ] = functools_mod
854+
814855 def test_stringized_annotations_on_class (self ):
815856 isa = inspect_stringized_annotations
816857 # test that local namespace lookups work
@@ -823,6 +864,16 @@ def test_stringized_annotations_on_class(self):
823864 {"x" : int },
824865 )
825866
867+ def test_stringized_annotations_on_custom_object (self ):
868+ class HasAnnotations :
869+ @property
870+ def __annotations__ (self ):
871+ return {"x" : "int" }
872+
873+ ha = HasAnnotations ()
874+ self .assertEqual (get_annotations (ha ), {"x" : "int" })
875+ self .assertEqual (get_annotations (ha , eval_str = True ), {"x" : int })
876+
826877 def test_stringized_annotation_permutations (self ):
827878 def define_class (name , has_future , has_annos , base_text , extra_names = None ):
828879 lines = []
@@ -990,6 +1041,23 @@ def __annotate__(self):
9901041 {"x" : "int" },
9911042 )
9921043
1044+ def test_non_dict_annotate (self ):
1045+ class WeirdAnnotate :
1046+ def __annotate__ (self , * args , ** kwargs ):
1047+ return "not a dict"
1048+
1049+ wa = WeirdAnnotate ()
1050+ for format in Format :
1051+ if format == Format .VALUE_WITH_FAKE_GLOBALS :
1052+ continue
1053+ with (
1054+ self .subTest (format = format ),
1055+ self .assertRaisesRegex (
1056+ ValueError , r".*__annotate__ returned a non-dict"
1057+ ),
1058+ ):
1059+ get_annotations (wa , format = format )
1060+
9931061 def test_no_annotations (self ):
9941062 class CustomClass :
9951063 pass
0 commit comments