Skip to content

Commit 77f4696

Browse files
committed
feat: SCIMException optional scim_ctx param.
1 parent 2da6d56 commit 77f4696

3 files changed

Lines changed: 68 additions & 3 deletions

File tree

doc/changelog.rst

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

4+
[Unreleased]
5+
------------
6+
7+
Added
8+
^^^^^
9+
10+
- :class:`~scim2_models.SCIMException` now accepts an optional ``scim_ctx`` parameter to indicate the SCIM context in which the exception occurred.
11+
412
[0.6.3] - 2026-01-29
513
--------------------
614

scim2_models/exceptions.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
from pydantic_core import PydanticCustomError
1212

13+
from .context import Context
14+
1315
if TYPE_CHECKING:
1416
from .messages.error import Error
1517

@@ -18,15 +20,25 @@ class SCIMException(Exception):
1820
"""Base exception for SCIM protocol errors.
1921
2022
Each subclass corresponds to a scimType defined in :rfc:`RFC 7644 Table 9 <7644#section-3.12>`.
23+
24+
:param detail: The error detail message.
25+
:param scim_ctx: The SCIM context in which the exception occurred.
2126
"""
2227

2328
status: int = 400
2429
scim_type: str = ""
2530
_default_detail: str = "A SCIM error occurred"
2631

27-
def __init__(self, *, detail: str | None = None, **context: Any):
32+
def __init__(
33+
self,
34+
*,
35+
detail: str | None = None,
36+
scim_ctx: Context | None = None,
37+
**context: Any,
38+
):
2839
self.context = context
2940
self._detail = detail
41+
self.scim_ctx = scim_ctx
3042
super().__init__(detail or self._default_detail)
3143

3244
@property
@@ -53,10 +65,13 @@ def as_pydantic_error(self) -> PydanticCustomError:
5365
)
5466

5567
@classmethod
56-
def from_error(cls, error: "Error") -> "SCIMException":
68+
def from_error(
69+
cls, error: "Error", scim_ctx: Context | None = None
70+
) -> "SCIMException":
5771
"""Create an exception from a SCIM Error object.
5872
5973
:param error: The SCIM Error object to convert.
74+
:param scim_ctx: The SCIM context in which the exception occurred.
6075
:return: The appropriate SCIMException subclass instance.
6176
"""
6277
from .messages.error import Error
@@ -65,7 +80,7 @@ def from_error(cls, error: "Error") -> "SCIMException":
6580
raise TypeError(f"Expected Error, got {type(error).__name__}")
6681

6782
exception_class = _SCIM_TYPE_TO_EXCEPTION.get(error.scim_type or "", cls)
68-
return exception_class(detail=error.detail)
83+
return exception_class(detail=error.detail, scim_ctx=scim_ctx)
6984

7085

7186
class InvalidFilterException(SCIMException):

tests/test_exceptions.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from pydantic import ValidationError
77
from pydantic import field_validator
88

9+
from scim2_models import Context
910
from scim2_models import Error
1011
from scim2_models import InvalidFilterException
1112
from scim2_models import InvalidPathException
@@ -429,3 +430,44 @@ def test_from_error_type_error():
429430
"""from_error() raises TypeError for non-Error input."""
430431
with pytest.raises(TypeError, match="Expected Error"):
431432
SCIMException.from_error("not an error")
433+
434+
435+
def test_scim_ctx_default_none():
436+
"""SCIMException has scim_ctx=None by default."""
437+
exc = SCIMException()
438+
assert exc.scim_ctx is None
439+
440+
441+
def test_scim_ctx_set_directly():
442+
"""SCIMException can have scim_ctx set directly."""
443+
exc = SCIMException(detail="Test error", scim_ctx=Context.RESOURCE_CREATION_REQUEST)
444+
assert exc.scim_ctx == Context.RESOURCE_CREATION_REQUEST
445+
assert exc.detail == "Test error"
446+
447+
448+
def test_scim_ctx_on_subclass():
449+
"""Exception subclasses can have scim_ctx set."""
450+
exc = InvalidValueException(
451+
detail="Invalid input",
452+
attribute="userName",
453+
scim_ctx=Context.RESOURCE_CREATION_REQUEST,
454+
)
455+
assert exc.scim_ctx == Context.RESOURCE_CREATION_REQUEST
456+
assert exc.attribute == "userName"
457+
458+
459+
def test_from_error_with_scim_ctx():
460+
"""from_error() passes scim_ctx to the created exception."""
461+
error = Error(status=400, scim_type="invalidValue", detail="Missing required")
462+
exc = SCIMException.from_error(error, scim_ctx=Context.RESOURCE_CREATION_RESPONSE)
463+
assert isinstance(exc, InvalidValueException)
464+
assert exc.scim_ctx == Context.RESOURCE_CREATION_RESPONSE
465+
assert exc.detail == "Missing required"
466+
467+
468+
def test_from_error_without_scim_ctx():
469+
"""from_error() creates exception with scim_ctx=None when not provided."""
470+
error = Error(status=400, scim_type="invalidFilter", detail="Bad filter")
471+
exc = SCIMException.from_error(error)
472+
assert isinstance(exc, InvalidFilterException)
473+
assert exc.scim_ctx is None

0 commit comments

Comments
 (0)