From e1401f3afcb076ee41cab1d47fada9e77d8225f4 Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Thu, 25 Jun 2026 11:49:23 +0200 Subject: [PATCH] fix: handle parameterized generics in maybe_parse_user_type to avoid infinite recursion Passing a parameterized generic such as `tuple[int, int]` (types.GenericAlias) or `List[int]` (typing._GenericAlias) as the `type` argument to `field()` caused `maybe_parse_user_type` to loop forever. Both alias types satisfy `isinstance(t, Iterable)`, but iterating them yields starred self-references (e.g. `*tuple[int, int]`) that are themselves Iterable aliases of the same kind, so the recursive expansion never bottoms out. Fix: detect any object with an `__origin__` attribute that is not itself a plain type (covers both `types.GenericAlias` and `typing._GenericAlias`), call `typing.get_origin()` to extract the bare origin class, and return that. This makes `field(type=tuple[int, int])` behave like `field(type=tuple)` at runtime while avoiding the recursion. Closes #291 --- pyrsistent/_checked_types.py | 12 +++++++++++- tests/class_test.py | 14 ++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/pyrsistent/_checked_types.py b/pyrsistent/_checked_types.py index 56146b5..e50fffc 100644 --- a/pyrsistent/_checked_types.py +++ b/pyrsistent/_checked_types.py @@ -1,8 +1,9 @@ +import types as _builtin_types from enum import Enum from abc import abstractmethod, ABCMeta from collections.abc import Iterable -from typing import TypeVar, Generic +from typing import TypeVar, Generic, get_origin from pyrsistent._pmap import PMap, pmap from pyrsistent._pset import PSet, pset @@ -86,6 +87,15 @@ def maybe_parse_user_type(t): return [t] elif is_type and not is_iterable: return [t] + elif isinstance(t, _builtin_types.GenericAlias) or (not is_type and hasattr(t, '__origin__')): + # Parameterized generics like ``tuple[int, int]`` or ``List[str]`` are + # Iterable, but iterating them yields starred self-references that cause + # infinite recursion. Resolve to the bare origin type instead so that + # runtime ``isinstance`` checks work correctly. + origin = get_origin(t) + if origin is not None and isinstance(origin, type): + return [origin] + return [t] elif is_iterable: # Recur to validate contained types as well. ts = t diff --git a/tests/class_test.py b/tests/class_test.py index def8dce..594b48f 100644 --- a/tests/class_test.py +++ b/tests/class_test.py @@ -472,3 +472,17 @@ def __new__(cls, **kwargs): a = X(y=[]) b = a.set(y=None) assert a == b + + +def test_field_type_generic_alias(): + """Parameterized generics (e.g. tuple[int, int]) must not cause infinite recursion.""" + class Coords(PClass): + position = field(type=tuple[int, int]) + items = field(type=list[str]) + mapping = field(type=dict[str, int]) + + c = Coords(position=(1, 2), items=['a'], mapping={'x': 1}) + assert c.position == (1, 2) + + with pytest.raises(Exception): + Coords(position='not a tuple', items=[], mapping={})