serializers.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. """Serializers used to convert Var types to JSON strings."""
  2. from __future__ import annotations
  3. import json
  4. import types as builtin_types
  5. import warnings
  6. from datetime import date, datetime, time, timedelta
  7. from enum import Enum
  8. from pathlib import Path
  9. from typing import Any, Callable, Dict, List, Set, Tuple, Type, Union, get_type_hints
  10. from reflex.base import Base
  11. from reflex.constants.colors import Color, format_color
  12. from reflex.utils import exceptions, types
  13. # Mapping from type to a serializer.
  14. # The serializer should convert the type to a JSON object.
  15. SerializedType = Union[str, bool, int, float, list, dict]
  16. Serializer = Callable[[Type], SerializedType]
  17. SERIALIZERS: dict[Type, Serializer] = {}
  18. def serializer(fn: Serializer) -> Serializer:
  19. """Decorator to add a serializer for a given type.
  20. Args:
  21. fn: The function to decorate.
  22. Returns:
  23. The decorated function.
  24. Raises:
  25. ValueError: If the function does not take a single argument.
  26. """
  27. # Get the global serializers.
  28. global SERIALIZERS
  29. # Check the type hints to get the type of the argument.
  30. type_hints = get_type_hints(fn)
  31. args = [arg for arg in type_hints if arg != "return"]
  32. # Make sure the function takes a single argument.
  33. if len(args) != 1:
  34. raise ValueError("Serializer must take a single argument.")
  35. # Get the type of the argument.
  36. type_ = type_hints[args[0]]
  37. # Make sure the type is not already registered.
  38. registered_fn = SERIALIZERS.get(type_)
  39. if registered_fn is not None and registered_fn != fn:
  40. raise ValueError(
  41. f"Serializer for type {type_} is already registered as {registered_fn.__qualname__}."
  42. )
  43. # Register the serializer.
  44. SERIALIZERS[type_] = fn
  45. # Return the function.
  46. return fn
  47. def serialize(value: Any) -> SerializedType | None:
  48. """Serialize the value to a JSON string.
  49. Args:
  50. value: The value to serialize.
  51. Returns:
  52. The serialized value, or None if a serializer is not found.
  53. """
  54. # Get the serializer for the type.
  55. serializer = get_serializer(type(value))
  56. # If there is no serializer, return None.
  57. if serializer is None:
  58. return None
  59. # Serialize the value.
  60. return serializer(value)
  61. def get_serializer(type_: Type) -> Serializer | None:
  62. """Get the serializer for the type.
  63. Args:
  64. type_: The type to get the serializer for.
  65. Returns:
  66. The serializer for the type, or None if there is no serializer.
  67. """
  68. global SERIALIZERS
  69. # First, check if the type is registered.
  70. serializer = SERIALIZERS.get(type_)
  71. if serializer is not None:
  72. return serializer
  73. # If the type is not registered, check if it is a subclass of a registered type.
  74. for registered_type, serializer in reversed(SERIALIZERS.items()):
  75. if types._issubclass(type_, registered_type):
  76. return serializer
  77. # If there is no serializer, return None.
  78. return None
  79. def has_serializer(type_: Type) -> bool:
  80. """Check if there is a serializer for the type.
  81. Args:
  82. type_: The type to check.
  83. Returns:
  84. Whether there is a serializer for the type.
  85. """
  86. return get_serializer(type_) is not None
  87. @serializer
  88. def serialize_type(value: type) -> str:
  89. """Serialize a python type.
  90. Args:
  91. value: the type to serialize.
  92. Returns:
  93. The serialized type.
  94. """
  95. return value.__name__
  96. @serializer
  97. def serialize_str(value: str) -> str:
  98. """Serialize a string.
  99. Args:
  100. value: The string to serialize.
  101. Returns:
  102. The serialized string.
  103. """
  104. return value
  105. @serializer
  106. def serialize_primitive(value: Union[bool, int, float, None]) -> str:
  107. """Serialize a primitive type.
  108. Args:
  109. value: The number/bool/None to serialize.
  110. Returns:
  111. The serialized number/bool/None.
  112. """
  113. from reflex.utils import format
  114. return format.json_dumps(value)
  115. @serializer
  116. def serialize_base(value: Base) -> str:
  117. """Serialize a Base instance.
  118. Args:
  119. value : The Base to serialize.
  120. Returns:
  121. The serialized Base.
  122. """
  123. return value.json()
  124. @serializer
  125. def serialize_list(value: Union[List, Tuple, Set]) -> str:
  126. """Serialize a list to a JSON string.
  127. Args:
  128. value: The list to serialize.
  129. Returns:
  130. The serialized list.
  131. """
  132. from reflex.utils import format
  133. # Dump the list to a string.
  134. fprop = format.json_dumps(list(value))
  135. # Unwrap var values.
  136. return format.unwrap_vars(fprop)
  137. @serializer
  138. def serialize_dict(prop: Dict[str, Any]) -> str:
  139. """Serialize a dictionary to a JSON string.
  140. Args:
  141. prop: The dictionary to serialize.
  142. Returns:
  143. The serialized dictionary.
  144. Raises:
  145. InvalidStylePropError: If the style prop is invalid.
  146. """
  147. # Import here to avoid circular imports.
  148. from reflex.event import EventHandler
  149. from reflex.utils import format
  150. prop_dict = {}
  151. for key, value in prop.items():
  152. if types._issubclass(type(value), Callable):
  153. raise exceptions.InvalidStylePropError(
  154. f"The style prop `{format.to_snake_case(key)}` cannot have " # type: ignore
  155. f"`{value.fn.__qualname__ if isinstance(value, EventHandler) else value.__qualname__ if isinstance(value, builtin_types.FunctionType) else value}`, "
  156. f"an event handler or callable as its value"
  157. )
  158. prop_dict[key] = value
  159. # Dump the dict to a string.
  160. fprop = format.json_dumps(prop_dict)
  161. # Unwrap var values.
  162. return format.unwrap_vars(fprop)
  163. @serializer
  164. def serialize_datetime(dt: Union[date, datetime, time, timedelta]) -> str:
  165. """Serialize a datetime to a JSON string.
  166. Args:
  167. dt: The datetime to serialize.
  168. Returns:
  169. The serialized datetime.
  170. """
  171. return str(dt)
  172. @serializer
  173. def serialize_path(path: Path):
  174. """Serialize a pathlib.Path to a JSON string.
  175. Args:
  176. path: The path to serialize.
  177. Returns:
  178. The serialized path.
  179. """
  180. return str(path.as_posix())
  181. @serializer
  182. def serialize_enum(en: Enum) -> str:
  183. """Serialize a enum to a JSON string.
  184. Args:
  185. en: The enum to serialize.
  186. Returns:
  187. The serialized enum.
  188. """
  189. return en.value
  190. @serializer
  191. def serialize_color(color: Color) -> str:
  192. """Serialize a color.
  193. Args:
  194. color: The color to serialize.
  195. Returns:
  196. The serialized color.
  197. """
  198. return format_color(color.color, color.shade, color.alpha)
  199. try:
  200. from pandas import DataFrame
  201. def format_dataframe_values(df: DataFrame) -> List[List[Any]]:
  202. """Format dataframe values to a list of lists.
  203. Args:
  204. df: The dataframe to format.
  205. Returns:
  206. The dataframe as a list of lists.
  207. """
  208. return [
  209. [str(d) if isinstance(d, (list, tuple)) else d for d in data]
  210. for data in list(df.values.tolist())
  211. ]
  212. @serializer
  213. def serialize_dataframe(df: DataFrame) -> dict:
  214. """Serialize a pandas dataframe.
  215. Args:
  216. df: The dataframe to serialize.
  217. Returns:
  218. The serialized dataframe.
  219. """
  220. return {
  221. "columns": df.columns.tolist(),
  222. "data": format_dataframe_values(df),
  223. }
  224. except ImportError:
  225. pass
  226. try:
  227. from plotly.graph_objects import Figure, layout
  228. from plotly.io import to_json
  229. @serializer
  230. def serialize_figure(figure: Figure) -> dict:
  231. """Serialize a plotly figure.
  232. Args:
  233. figure: The figure to serialize.
  234. Returns:
  235. The serialized figure.
  236. """
  237. return json.loads(str(to_json(figure)))
  238. @serializer
  239. def serialize_template(template: layout.Template) -> dict:
  240. """Serialize a plotly template.
  241. Args:
  242. template: The template to serialize.
  243. Returns:
  244. The serialized template.
  245. """
  246. return {
  247. "data": json.loads(str(to_json(template.data))),
  248. "layout": json.loads(str(to_json(template.layout))),
  249. }
  250. except ImportError:
  251. pass
  252. try:
  253. import base64
  254. import io
  255. from PIL.Image import MIME
  256. from PIL.Image import Image as Img
  257. @serializer
  258. def serialize_image(image: Img) -> str:
  259. """Serialize a plotly figure.
  260. Args:
  261. image: The image to serialize.
  262. Returns:
  263. The serialized image.
  264. """
  265. buff = io.BytesIO()
  266. image_format = getattr(image, "format", None) or "PNG"
  267. image.save(buff, format=image_format)
  268. image_bytes = buff.getvalue()
  269. base64_image = base64.b64encode(image_bytes).decode("utf-8")
  270. try:
  271. # Newer method to get the mime type, but does not always work.
  272. mime_type = image.get_format_mimetype() # type: ignore
  273. except AttributeError:
  274. try:
  275. # Fallback method
  276. mime_type = MIME[image_format]
  277. except KeyError:
  278. # Unknown mime_type: warn and return image/png and hope the browser can sort it out.
  279. warnings.warn( # noqa: B028
  280. f"Unknown mime type for {image} {image_format}. Defaulting to image/png"
  281. )
  282. mime_type = "image/png"
  283. return f"data:{mime_type};base64,{base64_image}"
  284. except ImportError:
  285. pass