|
| 1 | +import warnings |
1 | 2 | from inspect import isclass |
2 | 3 | from typing import Any |
3 | 4 | from typing import Optional |
|
23 | 24 | from scim2_models.annotations import Required |
24 | 25 | from scim2_models.annotations import Returned |
25 | 26 | from scim2_models.context import Context |
| 27 | +from scim2_models.exceptions import MutabilityException |
26 | 28 | from scim2_models.utils import UNION_TYPES |
27 | 29 | from scim2_models.utils import _find_field_name |
28 | 30 | from scim2_models.utils import _normalize_attribute_name |
@@ -410,7 +412,10 @@ def check_replacement_request_mutability( |
410 | 412 | and issubclass(cls, Resource) |
411 | 413 | and original is not None |
412 | 414 | ): |
413 | | - cls._check_mutability_issues(original, obj) |
| 415 | + try: |
| 416 | + obj._check_immutable_fields(original) |
| 417 | + except MutabilityException as exc: |
| 418 | + raise exc.as_pydantic_error() from exc |
414 | 419 | return obj |
415 | 420 |
|
416 | 421 | @model_validator(mode="after") |
@@ -456,35 +461,30 @@ def check_primary_attribute_uniqueness(self, info: ValidationInfo) -> Self: |
456 | 461 |
|
457 | 462 | return self |
458 | 463 |
|
459 | | - @classmethod |
460 | | - def _check_mutability_issues( |
461 | | - cls, original: "BaseModel", replacement: "BaseModel" |
462 | | - ) -> None: |
463 | | - """Compare two instances, and check for differences of values on the fields marked as immutable.""" |
| 464 | + def _check_immutable_fields(self, original: Self) -> None: |
| 465 | + """Check that immutable fields have not been modified compared to *original*. |
| 466 | +
|
| 467 | + Recursively checks nested single-valued complex attributes. |
| 468 | + """ |
464 | 469 | from .attributes import is_complex_attribute |
465 | 470 |
|
466 | | - model = replacement.__class__ |
467 | | - for field_name in model.model_fields: |
468 | | - mutability = model.get_field_annotation(field_name, Mutability) |
| 471 | + for field_name in type(self).model_fields: |
| 472 | + mutability = type(self).get_field_annotation(field_name, Mutability) |
469 | 473 | if mutability == Mutability.immutable and getattr( |
470 | 474 | original, field_name |
471 | | - ) != getattr(replacement, field_name): |
472 | | - raise PydanticCustomError( |
473 | | - "mutability_error", |
474 | | - "Field '{field_name}' is immutable but the request value is different than the original value.", |
475 | | - {"field_name": field_name}, |
476 | | - ) |
| 475 | + ) != getattr(self, field_name): |
| 476 | + raise MutabilityException(attribute=field_name, mutability="immutable") |
477 | 477 |
|
478 | | - attr_type = model.get_field_root_type(field_name) |
| 478 | + attr_type = type(self).get_field_root_type(field_name) |
479 | 479 | if ( |
480 | 480 | attr_type |
481 | 481 | and is_complex_attribute(attr_type) |
482 | | - and not model.get_field_multiplicity(field_name) |
| 482 | + and not type(self).get_field_multiplicity(field_name) |
483 | 483 | ): |
484 | 484 | original_val = getattr(original, field_name) |
485 | | - replacement_value = getattr(replacement, field_name) |
486 | | - if original_val is not None and replacement_value is not None: |
487 | | - cls._check_mutability_issues(original_val, replacement_value) |
| 485 | + replacement_val = getattr(self, field_name) |
| 486 | + if original_val is not None and replacement_val is not None: |
| 487 | + replacement_val._check_immutable_fields(original_val) |
488 | 488 |
|
489 | 489 | def _set_complex_attribute_urns(self) -> None: |
490 | 490 | """Navigate through attributes and sub-attributes of type ComplexAttribute, and mark them with a '_attribute_urn' attribute. |
@@ -611,22 +611,30 @@ def model_validate( |
611 | 611 | original: Optional["BaseModel"] = None, |
612 | 612 | **kwargs: Any, |
613 | 613 | ) -> Self: |
614 | | - """Validate SCIM payloads and generate model representation by using Pydantic :code:`BaseModel.model_validate`. |
| 614 | + """Validate SCIM payloads and generate model representation by using Pydantic :meth:`~pydantic.BaseModel.model_validate`. |
615 | 615 |
|
616 | 616 | :param scim_ctx: The SCIM :class:`~scim2_models.Context` in which the validation happens. |
617 | 617 | :param original: If this parameter is set during :attr:`~Context.RESOURCE_REPLACEMENT_REQUEST`, |
618 | 618 | :attr:`~scim2_models.Mutability.immutable` parameters will be compared against the *original* model value. |
619 | 619 | An exception is raised if values are different. |
| 620 | +
|
| 621 | + .. deprecated:: 0.6.7 |
| 622 | + Use :meth:`replace` on the validated instance instead. |
| 623 | + Will be removed in 0.8.0. |
620 | 624 | """ |
| 625 | + if original is not None: |
| 626 | + warnings.warn( |
| 627 | + "The 'original' parameter is deprecated, " |
| 628 | + "use the 'replace' method on the validated instance instead. " |
| 629 | + "Will be removed in 0.8.0.", |
| 630 | + DeprecationWarning, |
| 631 | + stacklevel=2, |
| 632 | + ) |
| 633 | + |
621 | 634 | context = kwargs.setdefault("context", {}) |
622 | 635 | context.setdefault("scim", scim_ctx) |
623 | 636 | context.setdefault("original", original) |
624 | 637 |
|
625 | | - if scim_ctx == Context.RESOURCE_REPLACEMENT_REQUEST and original is None: |
626 | | - raise ValueError( |
627 | | - "Resource queries replacement validation must compare to an original resource" |
628 | | - ) |
629 | | - |
630 | 638 | return super().model_validate(*args, **kwargs) |
631 | 639 |
|
632 | 640 | def get_attribute_urn(self, field_name: str) -> str: |
|
0 commit comments