utils.py 2.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  1. """Internal utilities for working with Pydantic types and data."""
  2. from __future__ import annotations
  3. import json
  4. from contextlib import suppress
  5. from functools import lru_cache
  6. from typing import TYPE_CHECKING
  7. import pydantic
  8. from packaging.version import Version
  9. from pydantic import ValidationError
  10. if TYPE_CHECKING:
  11. from typing import Any, Final
  12. from pydantic import BaseModel
  13. IS_PYDANTIC_V2: Final[bool] = Version(pydantic.VERSION).major >= 2
  14. @lru_cache
  15. def gql_typename(cls: type[BaseModel]) -> str:
  16. """Get the GraphQL typename for a Pydantic model."""
  17. if (field := cls.model_fields.get("typename__")) and (typename := field.default):
  18. return typename
  19. raise TypeError(f"Cannot extract GraphQL typename from: {cls.__qualname__!r}.")
  20. if IS_PYDANTIC_V2:
  21. import pydantic_core # pydantic_core is only installed by pydantic v2
  22. def from_json(s: str | bytes) -> Any:
  23. """Quickly deserialize a JSON string to a Python object."""
  24. return pydantic_core.from_json(s)
  25. def to_json(v: Any) -> str:
  26. """Quickly serialize a (possibly Pydantic) object to a JSON string."""
  27. return pydantic_core.to_json(v, by_alias=True, round_trip=True).decode("utf-8")
  28. def pydantic_isinstance(
  29. v: Any, classinfo: type[BaseModel] | tuple[type[BaseModel], ...]
  30. ) -> bool:
  31. """Return True if the object could be parsed into the given Pydantic type.
  32. This is like a more lenient version of `isinstance()` for use with Pydantic.
  33. In Pydantic v2, should be fast since the underlying implementation is in Rust,
  34. and it may be preferable over `try:...except ValidationError:...`.
  35. See: https://docs.pydantic.dev/latest/api/pydantic_core/#pydantic_core.SchemaValidator.isinstance_python
  36. """
  37. if isinstance(classinfo, tuple):
  38. return any(
  39. cls.__pydantic_validator__.isinstance_python(v) for cls in classinfo
  40. )
  41. cls = classinfo
  42. return cls.__pydantic_validator__.isinstance_python(v)
  43. else:
  44. # Pydantic v1 fallback implementations.
  45. # These may be noticeably slower, but their primary goal is to ensure
  46. # compatibility with Pydantic v1 so long as we need to support it.
  47. from pydantic.json import pydantic_encoder # Only valid in pydantic v1
  48. def from_json(s: str | bytes) -> Any:
  49. return json.loads(s)
  50. def to_json(v: Any) -> str:
  51. return json.dumps(v, default=pydantic_encoder)
  52. def pydantic_isinstance(
  53. v: Any, classinfo: type[BaseModel] | tuple[type[BaseModel], ...]
  54. ) -> bool:
  55. classes = classinfo if isinstance(classinfo, tuple) else (classinfo,)
  56. for cls in classes:
  57. with suppress(ValidationError):
  58. cls.model_validate(v)
  59. return True
  60. return False