5
5
import collections
6
6
from collections import defaultdict
7
7
import collections .abc
8
+ import copy
8
9
from functools import lru_cache
9
10
import inspect
10
11
import pickle
17
18
from typing import TypeVar , Optional , Union , Any , AnyStr
18
19
from typing import T , KT , VT # Not in __all__.
19
20
from typing import Tuple , List , Dict , Iterable , Iterator , Callable
20
- from typing import Generic , NamedTuple
21
+ from typing import Generic
21
22
from typing import no_type_check
22
23
import typing_extensions
23
24
from typing_extensions import NoReturn , ClassVar , Final , IntVar , Literal , Type , NewType , TypedDict , Self
27
28
from typing_extensions import TypeVarTuple , Unpack , dataclass_transform , reveal_type , Never , assert_never , LiteralString
28
29
from typing_extensions import assert_type , get_type_hints , get_origin , get_args
29
30
from typing_extensions import clear_overloads , get_overloads , overload
31
+ from typing_extensions import NamedTuple
30
32
31
33
# Flags used to mark tests that only apply after a specific
32
34
# version of the typing module.
33
35
TYPING_3_8_0 = sys .version_info [:3 ] >= (3 , 8 , 0 )
36
+ TYPING_3_9_0 = sys .version_info [:3 ] >= (3 , 9 , 0 )
34
37
TYPING_3_10_0 = sys .version_info [:3 ] >= (3 , 10 , 0 )
35
38
36
39
# 3.11 makes runtime type checks (_type_check) more lenient.
@@ -2874,7 +2877,7 @@ def test_typing_extensions_defers_when_possible(self):
2874
2877
if sys .version_info < (3 , 10 ):
2875
2878
exclude |= {'get_args' , 'get_origin' }
2876
2879
if sys .version_info < (3 , 11 ):
2877
- exclude . add ( 'final' )
2880
+ exclude |= { 'final' , 'NamedTuple' }
2878
2881
for item in typing_extensions .__all__ :
2879
2882
if item not in exclude and hasattr (typing , item ):
2880
2883
self .assertIs (
@@ -2892,6 +2895,305 @@ def test_typing_extensions_compiles_with_opt(self):
2892
2895
self .fail ('Module does not compile with optimize=2 (-OO flag).' )
2893
2896
2894
2897
2898
+ class CoolEmployee (NamedTuple ):
2899
+ name : str
2900
+ cool : int
2901
+
2902
+
2903
+ class CoolEmployeeWithDefault (NamedTuple ):
2904
+ name : str
2905
+ cool : int = 0
2906
+
2907
+
2908
+ class XMeth (NamedTuple ):
2909
+ x : int
2910
+
2911
+ def double (self ):
2912
+ return 2 * self .x
2913
+
2914
+
2915
+ class XRepr (NamedTuple ):
2916
+ x : int
2917
+ y : int = 1
2918
+
2919
+ def __str__ (self ):
2920
+ return f'{ self .x } -> { self .y } '
2921
+
2922
+ def __add__ (self , other ):
2923
+ return 0
2924
+
2925
+
2926
+ @skipIf (TYPING_3_11_0 , "These invariants should all be tested upstream on 3.11+" )
2927
+ class NamedTupleTests (BaseTestCase ):
2928
+ class NestedEmployee (NamedTuple ):
2929
+ name : str
2930
+ cool : int
2931
+
2932
+ def test_basics (self ):
2933
+ Emp = NamedTuple ('Emp' , [('name' , str ), ('id' , int )])
2934
+ self .assertIsSubclass (Emp , tuple )
2935
+ joe = Emp ('Joe' , 42 )
2936
+ jim = Emp (name = 'Jim' , id = 1 )
2937
+ self .assertIsInstance (joe , Emp )
2938
+ self .assertIsInstance (joe , tuple )
2939
+ self .assertEqual (joe .name , 'Joe' )
2940
+ self .assertEqual (joe .id , 42 )
2941
+ self .assertEqual (jim .name , 'Jim' )
2942
+ self .assertEqual (jim .id , 1 )
2943
+ self .assertEqual (Emp .__name__ , 'Emp' )
2944
+ self .assertEqual (Emp ._fields , ('name' , 'id' ))
2945
+ self .assertEqual (Emp .__annotations__ ,
2946
+ collections .OrderedDict ([('name' , str ), ('id' , int )]))
2947
+
2948
+ def test_annotation_usage (self ):
2949
+ tim = CoolEmployee ('Tim' , 9000 )
2950
+ self .assertIsInstance (tim , CoolEmployee )
2951
+ self .assertIsInstance (tim , tuple )
2952
+ self .assertEqual (tim .name , 'Tim' )
2953
+ self .assertEqual (tim .cool , 9000 )
2954
+ self .assertEqual (CoolEmployee .__name__ , 'CoolEmployee' )
2955
+ self .assertEqual (CoolEmployee ._fields , ('name' , 'cool' ))
2956
+ self .assertEqual (CoolEmployee .__annotations__ ,
2957
+ collections .OrderedDict (name = str , cool = int ))
2958
+
2959
+ def test_annotation_usage_with_default (self ):
2960
+ jelle = CoolEmployeeWithDefault ('Jelle' )
2961
+ self .assertIsInstance (jelle , CoolEmployeeWithDefault )
2962
+ self .assertIsInstance (jelle , tuple )
2963
+ self .assertEqual (jelle .name , 'Jelle' )
2964
+ self .assertEqual (jelle .cool , 0 )
2965
+ cooler_employee = CoolEmployeeWithDefault ('Sjoerd' , 1 )
2966
+ self .assertEqual (cooler_employee .cool , 1 )
2967
+
2968
+ self .assertEqual (CoolEmployeeWithDefault .__name__ , 'CoolEmployeeWithDefault' )
2969
+ self .assertEqual (CoolEmployeeWithDefault ._fields , ('name' , 'cool' ))
2970
+ self .assertEqual (CoolEmployeeWithDefault .__annotations__ ,
2971
+ dict (name = str , cool = int ))
2972
+
2973
+ with self .assertRaisesRegex (
2974
+ TypeError ,
2975
+ 'Non-default namedtuple field y cannot follow default field x'
2976
+ ):
2977
+ class NonDefaultAfterDefault (NamedTuple ):
2978
+ x : int = 3
2979
+ y : int
2980
+
2981
+ @skipUnless (
2982
+ (
2983
+ TYPING_3_8_0
2984
+ or hasattr (CoolEmployeeWithDefault , '_field_defaults' )
2985
+ ),
2986
+ '"_field_defaults" attribute was added in a micro version of 3.7'
2987
+ )
2988
+ def test_field_defaults (self ):
2989
+ self .assertEqual (CoolEmployeeWithDefault ._field_defaults , dict (cool = 0 ))
2990
+
2991
+ def test_annotation_usage_with_methods (self ):
2992
+ self .assertEqual (XMeth (1 ).double (), 2 )
2993
+ self .assertEqual (XMeth (42 ).x , XMeth (42 )[0 ])
2994
+ self .assertEqual (str (XRepr (42 )), '42 -> 1' )
2995
+ self .assertEqual (XRepr (1 , 2 ) + XRepr (3 ), 0 )
2996
+
2997
+ bad_overwrite_error_message = 'Cannot overwrite NamedTuple attribute'
2998
+
2999
+ with self .assertRaisesRegex (AttributeError , bad_overwrite_error_message ):
3000
+ class XMethBad (NamedTuple ):
3001
+ x : int
3002
+ def _fields (self ):
3003
+ return 'no chance for this'
3004
+
3005
+ with self .assertRaisesRegex (AttributeError , bad_overwrite_error_message ):
3006
+ class XMethBad2 (NamedTuple ):
3007
+ x : int
3008
+ def _source (self ):
3009
+ return 'no chance for this as well'
3010
+
3011
+ def test_multiple_inheritance (self ):
3012
+ class A :
3013
+ pass
3014
+ with self .assertRaisesRegex (
3015
+ TypeError ,
3016
+ 'can only inherit from a NamedTuple type and Generic'
3017
+ ):
3018
+ class X (NamedTuple , A ):
3019
+ x : int
3020
+
3021
+ with self .assertRaisesRegex (
3022
+ TypeError ,
3023
+ 'can only inherit from a NamedTuple type and Generic'
3024
+ ):
3025
+ class X (NamedTuple , tuple ):
3026
+ x : int
3027
+
3028
+ with self .assertRaisesRegex (TypeError , 'duplicate base class' ):
3029
+ class X (NamedTuple , NamedTuple ):
3030
+ x : int
3031
+
3032
+ class A (NamedTuple ):
3033
+ x : int
3034
+ with self .assertRaisesRegex (
3035
+ TypeError ,
3036
+ 'can only inherit from a NamedTuple type and Generic'
3037
+ ):
3038
+ class X (NamedTuple , A ):
3039
+ y : str
3040
+
3041
+ def test_generic (self ):
3042
+ class X (NamedTuple , Generic [T ]):
3043
+ x : T
3044
+ self .assertEqual (X .__bases__ , (tuple , Generic ))
3045
+ self .assertEqual (X .__orig_bases__ , (NamedTuple , Generic [T ]))
3046
+ self .assertEqual (X .__mro__ , (X , tuple , Generic , object ))
3047
+
3048
+ class Y (Generic [T ], NamedTuple ):
3049
+ x : T
3050
+ self .assertEqual (Y .__bases__ , (Generic , tuple ))
3051
+ self .assertEqual (Y .__orig_bases__ , (Generic [T ], NamedTuple ))
3052
+ self .assertEqual (Y .__mro__ , (Y , Generic , tuple , object ))
3053
+
3054
+ for G in X , Y :
3055
+ with self .subTest (type = G ):
3056
+ self .assertEqual (G .__parameters__ , (T ,))
3057
+ A = G [int ]
3058
+ self .assertIs (A .__origin__ , G )
3059
+ self .assertEqual (A .__args__ , (int ,))
3060
+ self .assertEqual (A .__parameters__ , ())
3061
+
3062
+ a = A (3 )
3063
+ self .assertIs (type (a ), G )
3064
+ self .assertEqual (a .x , 3 )
3065
+
3066
+ with self .assertRaisesRegex (TypeError , 'Too many parameters' ):
3067
+ G [int , str ]
3068
+
3069
+ @skipUnless (TYPING_3_9_0 , "tuple.__class_getitem__ was added in 3.9" )
3070
+ def test_non_generic_subscript_py39_plus (self ):
3071
+ # For backward compatibility, subscription works
3072
+ # on arbitrary NamedTuple types.
3073
+ class Group (NamedTuple ):
3074
+ key : T
3075
+ group : list [T ]
3076
+ A = Group [int ]
3077
+ self .assertEqual (A .__origin__ , Group )
3078
+ self .assertEqual (A .__parameters__ , ())
3079
+ self .assertEqual (A .__args__ , (int ,))
3080
+ a = A (1 , [2 ])
3081
+ self .assertIs (type (a ), Group )
3082
+ self .assertEqual (a , (1 , [2 ]))
3083
+
3084
+ @skipIf (TYPING_3_9_0 , "Test isn't relevant to 3.9+" )
3085
+ def test_non_generic_subscript_error_message_py38_minus (self ):
3086
+ class Group (NamedTuple ):
3087
+ key : T
3088
+ group : List [T ]
3089
+
3090
+ with self .assertRaisesRegex (TypeError , 'not subscriptable' ):
3091
+ Group [int ]
3092
+
3093
+ for attr in ('__args__' , '__origin__' , '__parameters__' ):
3094
+ with self .subTest (attr = attr ):
3095
+ self .assertFalse (hasattr (Group , attr ))
3096
+
3097
+ def test_namedtuple_keyword_usage (self ):
3098
+ LocalEmployee = NamedTuple ("LocalEmployee" , name = str , age = int )
3099
+ nick = LocalEmployee ('Nick' , 25 )
3100
+ self .assertIsInstance (nick , tuple )
3101
+ self .assertEqual (nick .name , 'Nick' )
3102
+ self .assertEqual (LocalEmployee .__name__ , 'LocalEmployee' )
3103
+ self .assertEqual (LocalEmployee ._fields , ('name' , 'age' ))
3104
+ self .assertEqual (LocalEmployee .__annotations__ , dict (name = str , age = int ))
3105
+ with self .assertRaisesRegex (
3106
+ TypeError ,
3107
+ 'Either list of fields or keywords can be provided to NamedTuple, not both'
3108
+ ):
3109
+ NamedTuple ('Name' , [('x' , int )], y = str )
3110
+
3111
+ def test_namedtuple_special_keyword_names (self ):
3112
+ NT = NamedTuple ("NT" , cls = type , self = object , typename = str , fields = list )
3113
+ self .assertEqual (NT .__name__ , 'NT' )
3114
+ self .assertEqual (NT ._fields , ('cls' , 'self' , 'typename' , 'fields' ))
3115
+ a = NT (cls = str , self = 42 , typename = 'foo' , fields = [('bar' , tuple )])
3116
+ self .assertEqual (a .cls , str )
3117
+ self .assertEqual (a .self , 42 )
3118
+ self .assertEqual (a .typename , 'foo' )
3119
+ self .assertEqual (a .fields , [('bar' , tuple )])
3120
+
3121
+ def test_empty_namedtuple (self ):
3122
+ NT = NamedTuple ('NT' )
3123
+
3124
+ class CNT (NamedTuple ):
3125
+ pass # empty body
3126
+
3127
+ for struct in [NT , CNT ]:
3128
+ with self .subTest (struct = struct ):
3129
+ self .assertEqual (struct ._fields , ())
3130
+ self .assertEqual (struct .__annotations__ , {})
3131
+ self .assertIsInstance (struct (), struct )
3132
+ # Attribute was added in a micro version of 3.7
3133
+ # and is tested more fully elsewhere
3134
+ if hasattr (struct , "_field_defaults" ):
3135
+ self .assertEqual (struct ._field_defaults , {})
3136
+
3137
+ def test_namedtuple_errors (self ):
3138
+ with self .assertRaises (TypeError ):
3139
+ NamedTuple .__new__ ()
3140
+ with self .assertRaises (TypeError ):
3141
+ NamedTuple ()
3142
+ with self .assertRaises (TypeError ):
3143
+ NamedTuple ('Emp' , [('name' , str )], None )
3144
+ with self .assertRaisesRegex (ValueError , 'cannot start with an underscore' ):
3145
+ NamedTuple ('Emp' , [('_name' , str )])
3146
+ with self .assertRaises (TypeError ):
3147
+ NamedTuple (typename = 'Emp' , name = str , id = int )
3148
+
3149
+ def test_copy_and_pickle (self ):
3150
+ global Emp # pickle wants to reference the class by name
3151
+ Emp = NamedTuple ('Emp' , [('name' , str ), ('cool' , int )])
3152
+ for cls in Emp , CoolEmployee , self .NestedEmployee :
3153
+ with self .subTest (cls = cls ):
3154
+ jane = cls ('jane' , 37 )
3155
+ for proto in range (pickle .HIGHEST_PROTOCOL + 1 ):
3156
+ z = pickle .dumps (jane , proto )
3157
+ jane2 = pickle .loads (z )
3158
+ self .assertEqual (jane2 , jane )
3159
+ self .assertIsInstance (jane2 , cls )
3160
+
3161
+ jane2 = copy .copy (jane )
3162
+ self .assertEqual (jane2 , jane )
3163
+ self .assertIsInstance (jane2 , cls )
3164
+
3165
+ jane2 = copy .deepcopy (jane )
3166
+ self .assertEqual (jane2 , jane )
3167
+ self .assertIsInstance (jane2 , cls )
3168
+
3169
+ def test_docstring (self ):
3170
+ self .assertEqual (NamedTuple .__doc__ , typing .NamedTuple .__doc__ )
3171
+ self .assertIsInstance (NamedTuple .__doc__ , str )
3172
+
3173
+ @skipUnless (TYPING_3_8_0 , "NamedTuple had a bad signature on <=3.7" )
3174
+ def test_signature_is_same_as_typing_NamedTuple (self ):
3175
+ self .assertEqual (inspect .signature (NamedTuple ), inspect .signature (typing .NamedTuple ))
3176
+
3177
+ @skipIf (TYPING_3_8_0 , "tests are only relevant to <=3.7" )
3178
+ def test_signature_on_37 (self ):
3179
+ self .assertIsInstance (inspect .signature (NamedTuple ), inspect .Signature )
3180
+ self .assertFalse (hasattr (NamedTuple , "__text_signature__" ))
3181
+
3182
+ @skipUnless (TYPING_3_9_0 , "NamedTuple was a class on 3.8 and lower" )
3183
+ def test_same_as_typing_NamedTuple_39_plus (self ):
3184
+ self .assertEqual (
3185
+ set (dir (NamedTuple )),
3186
+ set (dir (typing .NamedTuple )) | {"__text_signature__" }
3187
+ )
3188
+ self .assertIs (type (NamedTuple ), type (typing .NamedTuple ))
3189
+
3190
+ @skipIf (TYPING_3_9_0 , "tests are only relevant to <=3.8" )
3191
+ def test_same_as_typing_NamedTuple_38_minus (self ):
3192
+ self .assertEqual (
3193
+ self .NestedEmployee .__annotations__ ,
3194
+ self .NestedEmployee ._field_types
3195
+ )
3196
+
2895
3197
2896
3198
if __name__ == '__main__' :
2897
3199
main ()
0 commit comments