Skip to content

Commit 0dd7a2a

Browse files
committed
fix: allow TypeVar as type parameter for PatchOp
1 parent d84c564 commit 0dd7a2a

File tree

3 files changed

+60
-4
lines changed

3 files changed

+60
-4
lines changed

doc/changelog.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
Changelog
22
=========
33

4+
[0.4.1] - Unreleased
5+
--------------------
6+
7+
Fixed
8+
^^^^^
9+
- Allow ``TypeVar`` as type parameters for :class:`~scim2_models.PatchOp`.
10+
411
[0.4.0] - 2025-07-23
512
--------------------
613

scim2_models/messages/patch_op.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ class PatchOp(Message, Generic[T]):
143143
- Using PatchOp without a type parameter raises TypeError
144144
"""
145145

146-
def __new__(cls, *args, **kwargs):
146+
def __new__(cls, *args: Any, **kwargs: Any):
147147
"""Create new PatchOp instance with type parameter validation.
148148
149149
Only handles the case of direct instantiation without type parameter (PatchOp()).
@@ -165,9 +165,23 @@ def __new__(cls, *args, **kwargs):
165165
def __class_getitem__(cls, item):
166166
"""Validate type parameter when creating parameterized type.
167167
168-
Ensures the type parameter is a concrete Resource subclass (not Resource itself).
169-
Rejects invalid types (str, int, etc.) and Union types.
168+
Ensures the type parameter is a concrete Resource subclass (not Resource itself)
169+
or a TypeVar bound to Resource. Rejects invalid types (str, int, etc.) and Union types.
170170
"""
171+
# Allow TypeVar as type parameter
172+
if isinstance(item, TypeVar):
173+
# Check if TypeVar is bound to Resource or its subclass
174+
if item.__bound__ is not None and (
175+
item.__bound__ is Resource
176+
or (isclass(item.__bound__) and issubclass(item.__bound__, Resource))
177+
):
178+
return super().__class_getitem__(item)
179+
else:
180+
raise TypeError(
181+
f"PatchOp TypeVar must be bound to Resource or its subclass, got {item}. "
182+
"Example: T = TypeVar('T', bound=Resource)"
183+
)
184+
171185
# Check if type parameter is a concrete Resource subclass (not Resource itself)
172186
if item is Resource:
173187
raise TypeError(
@@ -176,7 +190,7 @@ def __class_getitem__(cls, item):
176190
)
177191
if not (isclass(item) and issubclass(item, Resource) and item is not Resource):
178192
raise TypeError(
179-
f"PatchOp type parameter must be a concrete Resource subclass, got {item}. "
193+
f"PatchOp type parameter must be a concrete Resource subclass or TypeVar, got {item}. "
180194
"Use PatchOp[User], PatchOp[Group], etc."
181195
)
182196

tests/test_patch_op_validation.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,41 @@ def test_patch_error_handling_type_mismatch():
379379

380380

381381
T = TypeVar("T", bound=Resource)
382+
UserT = TypeVar("UserT", bound=User)
383+
UnboundT = TypeVar("UnboundT")
384+
385+
386+
def test_patch_op_with_typevar_bound_to_resource():
387+
"""Test that PatchOp accepts TypeVar bound to Resource."""
388+
# Should not raise any exception
389+
patch_type = PatchOp[T]
390+
assert patch_type is not None
391+
392+
393+
def test_patch_op_with_typevar_bound_to_resource_subclass():
394+
"""Test that PatchOp accepts TypeVar bound to Resource subclass."""
395+
# Should not raise any exception
396+
patch_type = PatchOp[UserT]
397+
assert patch_type is not None
398+
399+
400+
def test_patch_op_with_unbound_typevar():
401+
"""Test that PatchOp rejects unbound TypeVar."""
402+
with pytest.raises(
403+
TypeError,
404+
match="PatchOp TypeVar must be bound to Resource or its subclass, got ~UnboundT",
405+
):
406+
PatchOp[UnboundT]
407+
408+
409+
def test_patch_op_with_typevar_bound_to_non_resource():
410+
"""Test that PatchOp rejects TypeVar bound to non-Resource class."""
411+
NonResourceT = TypeVar("NonResourceT", bound=str)
412+
with pytest.raises(
413+
TypeError,
414+
match="PatchOp TypeVar must be bound to Resource or its subclass, got ~NonResourceT",
415+
):
416+
PatchOp[NonResourceT]
382417

383418

384419
def test_create_parent_object_return_none():

0 commit comments

Comments
 (0)