@@ -1272,6 +1272,163 @@ def __iter__(self):
12721272 self .assertTryCastSuccess (NamedObject , MyNamedMapping ("Isabelle" ))
12731273 self .assertTryCastFailure (ValuedObject , MyNamedMapping ("Isabelle" ))
12741274
1275+ if sys .version_info >= (3 , 11 ):
1276+
1277+ def test_typeddict_generic (self ) -> None :
1278+ class Point (RichTypedDict , Generic [_T ]):
1279+ x : _T
1280+ y : _T
1281+
1282+ # Point[int]
1283+ self .assertTryCastSuccess (Point [int ], {"x" : 1 , "y" : 2 })
1284+ self .assertTryCastFailure (Point [int ], {"x" : 1.5 , "y" : 2 })
1285+ self .assertTryCastFailure (Point [int ], {"x" : 1 , "y" : 2.5 })
1286+ self .assertTryCastFailure (Point [int ], {"x" : "a" , "y" : "b" })
1287+ self .assertTryCastFailure (Point [int ], {"x" : 1 }) # missing y
1288+ self .assertTryCastSuccess (
1289+ Point [int ], {"x" : 1 , "y" : 2 , "z" : 3 }
1290+ ) # extra key ok
1291+
1292+ # Point[str]
1293+ self .assertTryCastSuccess (Point [str ], {"x" : "a" , "y" : "b" })
1294+ self .assertTryCastFailure (Point [str ], {"x" : 1 , "y" : 2 })
1295+ self .assertTryCastFailure (Point [str ], {"x" : "a" , "y" : 2 })
1296+
1297+ # Point[float]
1298+ self .assertTryCastSuccess (Point [float ], {"x" : 1.5 , "y" : 2.5 })
1299+ self .assertTryCastSuccess (
1300+ Point [float ], {"x" : 1 , "y" : 2 }
1301+ ) # int -> float coercion
1302+
1303+ # Multiple type parameters
1304+ K = TypeVar ("K" )
1305+ V = TypeVar ("V" )
1306+
1307+ class Entry (RichTypedDict , Generic [K , V ]):
1308+ key : K
1309+ value : V
1310+
1311+ # Entry[str, int]
1312+ self .assertTryCastSuccess (Entry [str , int ], {"key" : "name" , "value" : 42 })
1313+ self .assertTryCastFailure (Entry [str , int ], {"key" : 1 , "value" : 42 })
1314+ self .assertTryCastFailure (Entry [str , int ], {"key" : "name" , "value" : "42" })
1315+
1316+ # Entry[int, str]
1317+ self .assertTryCastSuccess (Entry [int , str ], {"key" : 1 , "value" : "42" })
1318+ self .assertTryCastFailure (Entry [int , str ], {"key" : "1" , "value" : "42" })
1319+
1320+ # Nested generics
1321+ self .assertTryCastSuccess (
1322+ Entry [str , List [int ]], {"key" : "numbers" , "value" : [1 , 2 , 3 ]}
1323+ )
1324+ self .assertTryCastFailure (
1325+ Entry [str , List [int ]], {"key" : "numbers" , "value" : [1 , "2" , 3 ]}
1326+ )
1327+
1328+ def test_typeddict_generic_with_partial (self ) -> None :
1329+ class PartialPoint (RichTypedDict , Generic [_T ], total = False ):
1330+ x : _T
1331+ y : _T
1332+
1333+ # PartialPoint[int]
1334+ self .assertTryCastSuccess (PartialPoint [int ], {"x" : 1 , "y" : 2 })
1335+ self .assertTryCastSuccess (PartialPoint [int ], {"x" : 1 })
1336+ self .assertTryCastSuccess (PartialPoint [int ], {"y" : 2 })
1337+ self .assertTryCastSuccess (PartialPoint [int ], {})
1338+ self .assertTryCastFailure (PartialPoint [int ], {"x" : "a" , "y" : 2 })
1339+
1340+ def test_typeddict_generic_with_union (self ) -> None :
1341+ class Container (RichTypedDict , Generic [_T ]):
1342+ value : Union [_T , None ]
1343+
1344+ # Container[int] - Basic Union[T, None]
1345+ self .assertTryCastSuccess (Container [int ], {"value" : 42 })
1346+ self .assertTryCastSuccess (Container [int ], {"value" : None })
1347+ self .assertTryCastFailure (Container [int ], {"value" : "42" })
1348+
1349+ # Container[str]
1350+ self .assertTryCastSuccess (Container [str ], {"value" : "hello" })
1351+ self .assertTryCastSuccess (Container [str ], {"value" : None })
1352+ self .assertTryCastFailure (Container [str ], {"value" : 42 })
1353+
1354+ # Multiple types with Union
1355+ T1 = TypeVar ("T1" )
1356+ T2 = TypeVar ("T2" )
1357+
1358+ class TwoValueContainer (RichTypedDict , Generic [T1 , T2 ]):
1359+ first : Union [T1 , str ]
1360+ second : Union [T2 , int ]
1361+
1362+ # TwoValueContainer[int, str]
1363+ self .assertTryCastSuccess (
1364+ TwoValueContainer [int , str ], {"first" : 42 , "second" : "hello" }
1365+ )
1366+ self .assertTryCastSuccess (
1367+ TwoValueContainer [int , str ], {"first" : "fallback" , "second" : "hello" }
1368+ )
1369+ self .assertTryCastSuccess (
1370+ TwoValueContainer [int , str ], {"first" : 42 , "second" : 99 }
1371+ )
1372+ self .assertTryCastFailure (
1373+ TwoValueContainer [int , str ], {"first" : 1.5 , "second" : "hello" }
1374+ )
1375+
1376+ # Union[T, str] where T is also str (edge case)
1377+ self .assertTryCastSuccess (
1378+ TwoValueContainer [str , str ], {"first" : "a" , "second" : "b" }
1379+ )
1380+
1381+ def test_typeddict_generic_with_union_pipe_syntax (self ) -> None :
1382+ # Test Python 3.10+ pipe syntax for unions
1383+ assert sys .version_info >= (3 , 10 )
1384+
1385+ # NOTE: Use exec() to avoid syntax errors in older Python versions
1386+ exec (
1387+ dedent (
1388+ """
1389+ class PipeContainer(RichTypedDict, Generic[_T]):
1390+ value: _T | None
1391+
1392+ # PipeContainer[int]
1393+ self.assertTryCastSuccess(PipeContainer[int], {"value": 42})
1394+ self.assertTryCastSuccess(PipeContainer[int], {"value": None})
1395+ self.assertTryCastFailure(PipeContainer[int], {"value": "42"})
1396+
1397+ # PipeContainer[List[str]]
1398+ self.assertTryCastSuccess(PipeContainer[List[str]], {"value": ["a", "b"]})
1399+ self.assertTryCastSuccess(PipeContainer[List[str]], {"value": None})
1400+ self.assertTryCastFailure(PipeContainer[List[str]], {"value": [1, 2]})
1401+ """
1402+ ),
1403+ {
1404+ "RichTypedDict" : RichTypedDict ,
1405+ "Generic" : Generic ,
1406+ "_T" : _T ,
1407+ "self" : self ,
1408+ "List" : List ,
1409+ },
1410+ )
1411+
1412+ def test_typeddict_generic_with_complex_unions (self ) -> None :
1413+ # Test more complex Union patterns with TypeVars
1414+ T = TypeVar ("T" )
1415+
1416+ class ComplexContainer (RichTypedDict , Generic [T ]):
1417+ value : Union [T , List [T ], None ]
1418+
1419+ # ComplexContainer[int] - value can be int, List[int], or None
1420+ self .assertTryCastSuccess (ComplexContainer [int ], {"value" : 42 })
1421+ self .assertTryCastSuccess (ComplexContainer [int ], {"value" : [1 , 2 , 3 ]})
1422+ self .assertTryCastSuccess (ComplexContainer [int ], {"value" : None })
1423+ self .assertTryCastFailure (ComplexContainer [int ], {"value" : "string" })
1424+ self .assertTryCastFailure (ComplexContainer [int ], {"value" : [1 , "mixed" ]})
1425+
1426+ # ComplexContainer[str]
1427+ self .assertTryCastSuccess (ComplexContainer [str ], {"value" : "hello" })
1428+ self .assertTryCastSuccess (ComplexContainer [str ], {"value" : ["a" , "b" ]})
1429+ self .assertTryCastSuccess (ComplexContainer [str ], {"value" : None })
1430+ self .assertTryCastFailure (ComplexContainer [str ], {"value" : [1 , 2 ]})
1431+
12751432 # === Tuples (Heterogeneous) ===
12761433
12771434 if sys .version_info >= (3 , 9 ):
@@ -1910,15 +2067,22 @@ class Point2D(mypy_extensions.TypedDict): # type: ignore[reportGeneralTypeIssue
19102067 class Point3D (Point2D , total = False ): # type: ignore[reportGeneralTypeIssues] # pyright
19112068 z : int
19122069
1913- self .assertRaisesRegex (
1914- TypeNotSupportedError ,
1915- (
1916- "trycast cannot determine which keys are required.*?"
1917- "Suggest use a typing(_extensions)?.TypedDict.*?"
1918- "strict=False"
1919- ),
1920- lambda : trycast (Point3D , {"x" : 1 , "y" : 2 }, strict = True ),
1921- )
2070+ if sys .version_info [:2 ] >= (3 , 14 ):
2071+ self .assertRaisesRegex (
2072+ TypeNotSupportedError ,
2073+ "trycast cannot determine which keys exist" ,
2074+ lambda : trycast (Point3D , {"x" : 1 , "y" : 2 }, strict = True ),
2075+ )
2076+ else :
2077+ self .assertRaisesRegex (
2078+ TypeNotSupportedError ,
2079+ (
2080+ "trycast cannot determine which keys are required.*?"
2081+ "Suggest use a typing(_extensions)?.TypedDict.*?"
2082+ "strict=False"
2083+ ),
2084+ lambda : trycast (Point3D , {"x" : 1 , "y" : 2 }, strict = True ),
2085+ )
19222086
19232087 # NOTE: Cannot combine the following two if-checks with an `and`
19242088 # because that is too complicated for Pyre to understand.
@@ -1966,29 +2130,62 @@ class MaybePoint1D(mypy_extensions.TypedDict, total=False): # type: ignore[repo
19662130 class TaggedMaybePoint1D (MaybePoint1D ):
19672131 name : str
19682132
1969- self .assertTryCastSuccess (Point2D , {"x" : 1 , "y" : 2 }, strict = False )
1970- self .assertTryCastFailure (Point2D , {"x" : 1 , "y" : "string" }, strict = False )
1971- self .assertTryCastFailure (Point2D , {"x" : 1 }, strict = False )
1972-
1973- self .assertTryCastSuccess (Point3D , {"x" : 1 , "y" : 2 , "z" : 3 }, strict = False )
1974- self .assertTryCastFailure (
1975- Point3D , {"x" : 1 , "y" : 2 , "z" : "string" }, strict = False
1976- )
1977- self .assertTryCastSuccess (Point3D , {"x" : 1 , "y" : 2 }, strict = False )
1978- self .assertTryCastSuccess (Point3D , {"x" : 1 }, strict = False ) # surprise!
1979- self .assertTryCastSuccess (Point3D , {"q" : 1 }, strict = False ) # surprise!
1980-
1981- self .assertTryCastSuccess (MaybePoint1D , {"x" : 1 }, strict = False )
1982- self .assertTryCastFailure (MaybePoint1D , {"x" : "string" }, strict = False )
1983- self .assertTryCastSuccess (MaybePoint1D , {}, strict = False )
1984- self .assertTryCastSuccess (MaybePoint1D , {"q" : 1 }, strict = False ) # surprise!
1985-
1986- self .assertTryCastSuccess (
1987- TaggedMaybePoint1D , {"x" : 1 , "name" : "one" }, strict = False
1988- )
1989- self .assertTryCastFailure (
1990- TaggedMaybePoint1D , {"name" : "one" }, strict = False
1991- ) # surprise!
2133+ @contextmanager
2134+ def assert_raises_if_python_3_14_or_later () -> Iterator [None ]:
2135+ try :
2136+ yield
2137+ except TypeNotSupportedError as e :
2138+ if (
2139+ "cannot determine which keys exist on a mypy_extensions.TypedDict"
2140+ in str (e )
2141+ ):
2142+ if sys .version_info [:2 ] >= (3 , 14 ):
2143+ pass # expected
2144+ else :
2145+ raise
2146+ else :
2147+ raise
2148+ else :
2149+ if sys .version_info [:2 ] >= (3 , 14 ):
2150+ raise AssertionError ("Expected TypeNotSupportedError to be raised" )
2151+
2152+ with assert_raises_if_python_3_14_or_later ():
2153+ self .assertTryCastSuccess (Point2D , {"x" : 1 , "y" : 2 }, strict = False )
2154+ with assert_raises_if_python_3_14_or_later ():
2155+ self .assertTryCastFailure (Point2D , {"x" : 1 , "y" : "string" }, strict = False )
2156+ with assert_raises_if_python_3_14_or_later ():
2157+ self .assertTryCastFailure (Point2D , {"x" : 1 }, strict = False )
2158+
2159+ with assert_raises_if_python_3_14_or_later ():
2160+ self .assertTryCastSuccess (Point3D , {"x" : 1 , "y" : 2 , "z" : 3 }, strict = False )
2161+ with assert_raises_if_python_3_14_or_later ():
2162+ self .assertTryCastFailure (
2163+ Point3D , {"x" : 1 , "y" : 2 , "z" : "string" }, strict = False
2164+ )
2165+ with assert_raises_if_python_3_14_or_later ():
2166+ self .assertTryCastSuccess (Point3D , {"x" : 1 , "y" : 2 }, strict = False )
2167+ with assert_raises_if_python_3_14_or_later ():
2168+ self .assertTryCastSuccess (Point3D , {"x" : 1 }, strict = False ) # surprise!
2169+ with assert_raises_if_python_3_14_or_later ():
2170+ self .assertTryCastSuccess (Point3D , {"q" : 1 }, strict = False ) # surprise!
2171+
2172+ with assert_raises_if_python_3_14_or_later ():
2173+ self .assertTryCastSuccess (MaybePoint1D , {"x" : 1 }, strict = False )
2174+ with assert_raises_if_python_3_14_or_later ():
2175+ self .assertTryCastFailure (MaybePoint1D , {"x" : "string" }, strict = False )
2176+ with assert_raises_if_python_3_14_or_later ():
2177+ self .assertTryCastSuccess (MaybePoint1D , {}, strict = False )
2178+ with assert_raises_if_python_3_14_or_later ():
2179+ self .assertTryCastSuccess (MaybePoint1D , {"q" : 1 }, strict = False ) # surprise!
2180+
2181+ with assert_raises_if_python_3_14_or_later ():
2182+ self .assertTryCastSuccess (
2183+ TaggedMaybePoint1D , {"x" : 1 , "name" : "one" }, strict = False
2184+ )
2185+ with assert_raises_if_python_3_14_or_later ():
2186+ self .assertTryCastFailure (
2187+ TaggedMaybePoint1D , {"name" : "one" }, strict = False
2188+ ) # surprise!
19922189
19932190 # NOTE: Cannot combine the following two if-checks with an `and`
19942191 # because that is too complicated for Pyre to understand.
0 commit comments