Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
Changelog
=========

[Unreleased]
------------

Fixed
^^^^^
- :meth:`~scim2_models.SCIMException.from_error` keeps the :class:`~scim2_models.Error`
object it is built from, and :meth:`~scim2_models.SCIMException.to_error` gives it back.
The status and the scimType a server sent used to be replaced by the ones of the
exception class, which dropped everything it has no class for, such as a ``429`` status
or a vendor specific scimType.

[0.8.0] - 2026-09-20
--------------------

Expand Down
23 changes: 21 additions & 2 deletions scim2_models/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def __init__(
self.context = context
self._detail = detail
self.scim_ctx = scim_ctx
self._error: Error | None = None
super().__init__(detail or self._default_detail)

@property
Expand All @@ -47,9 +48,16 @@ def detail(self) -> str:
return self._detail or self._default_detail

def to_error(self) -> "Error":
"""Convert this exception to a SCIM Error response object."""
"""Convert this exception to a SCIM Error response object.

Exceptions built by :meth:`~scim2_models.SCIMException.from_error` return
the very object they were built from.
"""
from .messages.error import Error

if self._error is not None:
return self._error

return Error(
status=self.status,
scim_type=self.scim_type or None,
Expand All @@ -70,6 +78,11 @@ def from_error(
) -> "SCIMException":
"""Create an exception from a SCIM Error object.

The error object is kept as-is, and
:meth:`~scim2_models.SCIMException.to_error` gives it back. This matters for
the values no exception class describes, such as a ``429`` status or a
vendor specific scimType.

:param error: The SCIM Error object to convert.
:param scim_ctx: The SCIM context in which the exception occurred.
:return: The appropriate SCIMException subclass instance.
Expand All @@ -80,7 +93,13 @@ def from_error(
raise TypeError(f"Expected Error, got {type(error).__name__}")

exception_class = _SCIM_TYPE_TO_EXCEPTION.get(error.scim_type or "", cls)
return exception_class(detail=error.detail, scim_ctx=scim_ctx)
exc = exception_class(detail=error.detail, scim_ctx=scim_ctx)
exc._error = error
if error.status is not None:
exc.status = error.status
if error.scim_type:
exc.scim_type = error.scim_type
return exc


class InvalidFilterException(SCIMException):
Expand Down
22 changes: 22 additions & 0 deletions tests/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,3 +471,25 @@ def test_from_error_without_scim_ctx():
exc = SCIMException.from_error(error)
assert isinstance(exc, InvalidFilterException)
assert exc.scim_ctx is None


def test_from_error_keeps_the_error_object():
"""to_error() gives back the very object from_error() was built from."""
error = Error(status=404, detail="Resource unknown not found")
exc = SCIMException.from_error(error)
assert exc.to_error() is error


def test_from_error_keeps_what_no_exception_class_describes():
"""from_error() keeps a status and a scimType that match no exception class."""
error = Error(status=429, scim_type="tooManyRequests", detail="Slow down")
exc = SCIMException.from_error(error)
assert exc.status == 429
assert exc.scim_type == "tooManyRequests"


def test_from_error_without_status():
"""from_error() keeps the class status when the Error object carries none."""
error = Error(scim_type="uniqueness", detail="Duplicate userName")
exc = SCIMException.from_error(error)
assert exc.status == 409
Loading