|
4 | 4 |
|
5 | 5 | import collections |
6 | 6 | import contextlib |
| 7 | +import importlib |
7 | 8 | import warnings |
8 | 9 | from collections.abc import Sequence |
9 | 10 | from typing import Any, Generic, TypeVar, Union |
@@ -261,9 +262,67 @@ def extend(self, other: Sequence[T]) -> None: |
261 | 262 | def set_fields(self, index: int, **kwargs) -> None: |
262 | 263 | """Assign the values of an existing object's attributes using keyword arguments.""" |
263 | 264 | self._validate_name_field(kwargs) |
264 | | - class_handle = self.data[index].__class__ |
265 | | - new_fields = {**self.data[index].__dict__, **kwargs} |
266 | | - self.data[index] = class_handle(**new_fields) |
| 265 | + pydantic_object = False |
| 266 | + |
| 267 | + if importlib.util.find_spec("pydantic"): |
| 268 | + # Pydantic is installed, so set up a context manager that will |
| 269 | + # suppress custom validation errors until all fields have been set. |
| 270 | + from pydantic import BaseModel, ValidationError |
| 271 | + |
| 272 | + if isinstance(self.data[index], BaseModel): |
| 273 | + pydantic_object = True |
| 274 | + |
| 275 | + # Define a custom context manager |
| 276 | + class SuppressCustomValidation(contextlib.AbstractContextManager): |
| 277 | + """Context manager to suppress "value_error" based validation errors in pydantic. |
| 278 | +
|
| 279 | + This validation context is necessary because errors can occur whilst individual |
| 280 | + model values are set, which are resolved when all of the input values are set. |
| 281 | +
|
| 282 | + After the exception is suppressed, execution proceeds with the next |
| 283 | + statement following the with statement. |
| 284 | +
|
| 285 | + with SuppressCustomValidation(): |
| 286 | + setattr(self.data[index], key, value) |
| 287 | + # Execution still resumes here if the attribute cannot be set |
| 288 | + """ |
| 289 | + |
| 290 | + def __init__(self): |
| 291 | + pass |
| 292 | + |
| 293 | + def __enter__(self): |
| 294 | + pass |
| 295 | + |
| 296 | + def __exit__(self, exctype, excinst, exctb): |
| 297 | + # If the return of __exit__ is True or truthy, the exception is suppressed. |
| 298 | + # Otherwise, the default behaviour of raising the exception applies. |
| 299 | + # |
| 300 | + # To suppress errors arising from field and model validators in pydantic, |
| 301 | + # we will examine the validation errors raised. If all of the errors |
| 302 | + # listed in the exception have the type "value_error", this indicates |
| 303 | + # they have arisen from field or model validators and will be suppressed. |
| 304 | + # Otherwise, they will be raised. |
| 305 | + if exctype is None: |
| 306 | + return |
| 307 | + if issubclass(exctype, ValidationError) and all( |
| 308 | + [error["type"] == "value_error" for error in excinst.errors()] |
| 309 | + ): |
| 310 | + return True |
| 311 | + return False |
| 312 | + |
| 313 | + validation_context = SuppressCustomValidation() |
| 314 | + else: |
| 315 | + validation_context = contextlib.nullcontext() |
| 316 | + |
| 317 | + for key, value in kwargs.items(): |
| 318 | + with validation_context: |
| 319 | + setattr(self.data[index], key, value) |
| 320 | + |
| 321 | + # We have suppressed custom validation errors for pydantic objects. |
| 322 | + # We now must revalidate the pydantic model outside the validation context |
| 323 | + # to catch any errors that remain after setting all of the fields. |
| 324 | + if pydantic_object: |
| 325 | + self._class_handle.model_validate(self.data[index]) |
267 | 326 |
|
268 | 327 | def get_names(self) -> list[str]: |
269 | 328 | """Return a list of the values of the name_field attribute of each class object in the list. |
|
0 commit comments