|
1 | 1 | import sys |
2 | | -from typing import Any, Dict, ForwardRef, Generic, List, Mapping, TypeVar, Union, get_args, get_origin |
| 2 | +from datetime import datetime, timedelta |
| 3 | +from typing import ( |
| 4 | + Any, |
| 5 | + Callable, |
| 6 | + Dict, |
| 7 | + ForwardRef, |
| 8 | + Generic, |
| 9 | + List, |
| 10 | + Mapping, |
| 11 | + Optional, |
| 12 | + Set, |
| 13 | + Tuple, |
| 14 | + TypeVar, |
| 15 | + Union, |
| 16 | + get_args, |
| 17 | + get_origin, |
| 18 | +) |
3 | 19 | from unittest import TestCase |
4 | 20 |
|
5 | 21 | import numpy as np |
|
9 | 25 | import csp |
10 | 26 | from csp import dynamic_demultiplex, ts |
11 | 27 | from csp.impl.types.common_definitions import OutputBasket, Outputs |
| 28 | +from csp.impl.types.container_type_normalizer import ContainerTypeNormalizer |
12 | 29 | from csp.impl.types.pydantic_type_resolver import TVarValidationContext |
13 | 30 | from csp.impl.types.pydantic_types import DynamicBasketPydantic |
14 | 31 | from csp.impl.types.tstype import TsType |
| 32 | +from csp.impl.types.typing_utils import FastList |
| 33 | +from csp.typing import Numpy1DArray, NumpyNDArray |
15 | 34 |
|
16 | 35 | T = TypeVar("T") |
17 | 36 | U = TypeVar("U") |
@@ -165,3 +184,117 @@ def test_validate(self): |
165 | 184 | dynamic_basket = dynamic_demultiplex(csp.const(1.0), csp.const("A")) |
166 | 185 | ta.validate_python(dynamic_basket) |
167 | 186 | self.assertRaises(Exception, ta.validate_python, {csp.const("A"): csp.const(1.0)}) |
| 187 | + |
| 188 | + |
| 189 | +class TestTsTypePep585Equivalence(TestCase): |
| 190 | + """PEP 585 builtin generics (list[int], dict[str, int], ...) must normalize to the same TsType as |
| 191 | + their typing equivalents (typing.List[int], typing.Dict[str, int], ...) so that tooling that rewrites |
| 192 | + typing.List -> list (e.g. ruff UP006) does not change TsType equality/hashing.""" |
| 193 | + |
| 194 | + def test_equality_and_hash_parity(self): |
| 195 | + cases = [ |
| 196 | + (list[int], List[int]), |
| 197 | + (dict[str, int], Dict[str, int]), |
| 198 | + (set[int], Set[int]), |
| 199 | + (tuple[int, str], Tuple[int, str]), |
| 200 | + (tuple[int, ...], Tuple[int, ...]), |
| 201 | + ] |
| 202 | + for builtin, typing_form in cases: |
| 203 | + with self.subTest(builtin=builtin): |
| 204 | + self.assertEqual(ts[builtin], ts[typing_form]) |
| 205 | + self.assertEqual(hash(ts[builtin]), hash(ts[typing_form])) |
| 206 | + |
| 207 | + def test_nested_equality(self): |
| 208 | + self.assertEqual(ts[list[dict[str, int]]], ts[List[Dict[str, int]]]) |
| 209 | + self.assertEqual( |
| 210 | + ts[dict[str, list[tuple[int, set[str]]]]], |
| 211 | + ts[Dict[str, List[Tuple[int, Set[str]]]]], |
| 212 | + ) |
| 213 | + self.assertEqual( |
| 214 | + hash(ts[list[dict[str, int]]]), |
| 215 | + hash(ts[List[Dict[str, int]]]), |
| 216 | + ) |
| 217 | + |
| 218 | + def test_canonicalizes_to_typing_form(self): |
| 219 | + # The canonical inner type is the typing form (the representation csp inference already emits). |
| 220 | + self.assertEqual(ts[list[int]].typ, List[int]) |
| 221 | + self.assertEqual(ts[dict[str, int]].typ, Dict[str, int]) |
| 222 | + self.assertEqual(ts[set[int]].typ, Set[int]) |
| 223 | + self.assertEqual(ts[tuple[int, str]].typ, Tuple[int, str]) |
| 224 | + |
| 225 | + def test_const_inference_matches_modernized_annotation(self): |
| 226 | + # csp.const infers typing.List[...]; a modernized (ruff UP006) annotation is ts[list[...]]. |
| 227 | + # These previously compared unequal, which broke strict-equality checks (e.g. csp-gateway channels). |
| 228 | + self.assertEqual(csp.const([1, 2, 3]).tstype, ts[list[int]]) |
| 229 | + self.assertEqual(csp.const({"a": 1}).tstype, ts[dict[str, int]]) |
| 230 | + |
| 231 | + def test_preserves_non_builtin_generics(self): |
| 232 | + # FastList / csp numpy array types share machinery with typing generics but must NOT be collapsed. |
| 233 | + self.assertIs(ContainerTypeNormalizer.normalize_type(FastList[int]).__origin__, FastList) |
| 234 | + self.assertIs(ContainerTypeNormalizer.normalize_type(Numpy1DArray[float]).__origin__, Numpy1DArray) |
| 235 | + self.assertIs(ContainerTypeNormalizer.normalize_type(NumpyNDArray[float]).__origin__, NumpyNDArray) |
| 236 | + |
| 237 | + def test_node_binding_builtin_generic(self): |
| 238 | + # A builtin-generic-annotated node input accepts an edge whose inferred type uses the typing form. |
| 239 | + @csp.node |
| 240 | + def consume(x: ts[list[int]]) -> ts[int]: |
| 241 | + if csp.ticked(x): |
| 242 | + return len(x) |
| 243 | + |
| 244 | + @csp.graph |
| 245 | + def g(): |
| 246 | + csp.add_graph_output("o", consume(csp.const([1, 2, 3]))) |
| 247 | + |
| 248 | + results = csp.run(g, starttime=datetime(2020, 1, 1), endtime=timedelta(days=1)) |
| 249 | + self.assertEqual(results["o"][0][1], 3) |
| 250 | + |
| 251 | + def test_builtin_generic_nested_in_union(self): |
| 252 | + # A builtin container nested inside a union (typing.Optional or PEP 604 X | None) is canonicalized. |
| 253 | + self.assertEqual(ts[list[int] | None], ts[Optional[List[int]]]) |
| 254 | + self.assertEqual(ts[Optional[list[int]]], ts[Optional[List[int]]]) |
| 255 | + self.assertEqual(ts[dict[str, list[int]] | None], ts[Optional[Dict[str, List[int]]]]) |
| 256 | + self.assertEqual(hash(ts[list[int] | None]), hash(ts[Optional[List[int]]])) |
| 257 | + |
| 258 | + def test_builtin_generic_nested_in_preserved_wrapper(self): |
| 259 | + # The outer wrapper (Mapping / FastList / Callable) is preserved, but builtin containers nested |
| 260 | + # inside it are still canonicalized so equality holds after a typing.List -> list rewrite. |
| 261 | + self.assertEqual(ts[Mapping[str, list[int]]], ts[Mapping[str, List[int]]]) |
| 262 | + self.assertEqual(ts[FastList[list[int]]], ts[FastList[List[int]]]) |
| 263 | + self.assertEqual(ts[Callable[[list[int]], dict[str, int]]], ts[Callable[[List[int]], Dict[str, int]]]) |
| 264 | + # ... and the wrapper origin is not collapsed into typing.List/dict/etc. |
| 265 | + self.assertIs(ContainerTypeNormalizer.normalize_type(FastList[list[int]]).__origin__, FastList) |
| 266 | + |
| 267 | + def test_string_arg_canonicalizes_to_forward_ref(self): |
| 268 | + # PEP 585 builtins store a bare string arg (list["T"]) while typing stores a ForwardRef; both must |
| 269 | + # normalize to the same stable ForwardRef (not a freshly-minted TypeVar on each call). |
| 270 | + self.assertEqual(ts[list["T"]], ts[list["T"]]) |
| 271 | + self.assertEqual(ts[list["T"]], ts[List["T"]]) |
| 272 | + self.assertEqual(ContainerTypeNormalizer.normalize_type(list["T"]).__args__[0], ForwardRef("T")) |
| 273 | + |
| 274 | + def test_identity_preserved_when_unchanged(self): |
| 275 | + # Nothing to canonicalize -> return the very same object, since some call sites (csp.snap |
| 276 | + # validation) compare normalized types with `is`. |
| 277 | + self.assertIs(ContainerTypeNormalizer.normalize_type(List[int]), List[int]) |
| 278 | + self.assertIs(ContainerTypeNormalizer.normalize_type(List["Foo"]), List["Foo"]) |
| 279 | + self.assertIs(ContainerTypeNormalizer.normalize_type(Optional[int]), Optional[int]) |
| 280 | + self.assertIs(ContainerTypeNormalizer.normalize_type(Dict[str, List[int]]), Dict[str, List[int]]) |
| 281 | + |
| 282 | + def test_empty_tuple(self): |
| 283 | + self.assertEqual(ts[tuple[()]], ts[Tuple[()]]) |
| 284 | + |
| 285 | + def test_callable_inner_canonicalized(self): |
| 286 | + import collections.abc as abc |
| 287 | + |
| 288 | + normalize = ContainerTypeNormalizer.normalize_type |
| 289 | + # typing.Callable: builtin containers nested in params/return canonicalize identically for both |
| 290 | + # spellings, so the modernized and classic annotations stay equal. |
| 291 | + self.assertEqual( |
| 292 | + normalize(Callable[[list[int]], dict[str, int]]), |
| 293 | + normalize(Callable[[List[int]], Dict[str, int]]), |
| 294 | + ) |
| 295 | + # collections.abc.Callable (the ruff UP006 rewrite of typing.Callable) must reconstruct without |
| 296 | + # error for finite, empty and ellipsis parameter lists, canonicalizing nested builtins while |
| 297 | + # keeping its own origin. |
| 298 | + self.assertEqual(normalize(abc.Callable[[list[int]], str]), abc.Callable[[List[int]], str]) |
| 299 | + self.assertEqual(normalize(abc.Callable[[], list[int]]), abc.Callable[[], List[int]]) |
| 300 | + self.assertEqual(normalize(abc.Callable[..., list[int]]), abc.Callable[..., List[int]]) |
0 commit comments