format.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662
  1. """Formatting operations."""
  2. from __future__ import annotations
  3. import base64
  4. import io
  5. import json
  6. import os
  7. import os.path as op
  8. import re
  9. import sys
  10. import types as builtin_types
  11. from typing import TYPE_CHECKING, Any, Callable, Type, Union
  12. import plotly.graph_objects as go
  13. from plotly.graph_objects import Figure
  14. from plotly.io import to_json
  15. from reflex import constants
  16. from reflex.utils import exceptions, types
  17. from reflex.vars import Var
  18. if TYPE_CHECKING:
  19. from reflex.components.component import ComponentStyle
  20. from reflex.event import EventChain, EventHandler, EventSpec
  21. WRAP_MAP = {
  22. "{": "}",
  23. "(": ")",
  24. "[": "]",
  25. "<": ">",
  26. '"': '"',
  27. "'": "'",
  28. "`": "`",
  29. }
  30. def get_close_char(open: str, close: str | None = None) -> str:
  31. """Check if the given character is a valid brace.
  32. Args:
  33. open: The open character.
  34. close: The close character if provided.
  35. Returns:
  36. The close character.
  37. Raises:
  38. ValueError: If the open character is not a valid brace.
  39. """
  40. if close is not None:
  41. return close
  42. if open not in WRAP_MAP:
  43. raise ValueError(f"Invalid wrap open: {open}, must be one of {WRAP_MAP.keys()}")
  44. return WRAP_MAP[open]
  45. def is_wrapped(text: str, open: str, close: str | None = None) -> bool:
  46. """Check if the given text is wrapped in the given open and close characters.
  47. Args:
  48. text: The text to check.
  49. open: The open character.
  50. close: The close character.
  51. Returns:
  52. Whether the text is wrapped.
  53. """
  54. close = get_close_char(open, close)
  55. return text.startswith(open) and text.endswith(close)
  56. def wrap(
  57. text: str,
  58. open: str,
  59. close: str | None = None,
  60. check_first: bool = True,
  61. num: int = 1,
  62. ) -> str:
  63. """Wrap the given text in the given open and close characters.
  64. Args:
  65. text: The text to wrap.
  66. open: The open character.
  67. close: The close character.
  68. check_first: Whether to check if the text is already wrapped.
  69. num: The number of times to wrap the text.
  70. Returns:
  71. The wrapped text.
  72. """
  73. close = get_close_char(open, close)
  74. # If desired, check if the text is already wrapped in braces.
  75. if check_first and is_wrapped(text=text, open=open, close=close):
  76. return text
  77. # Wrap the text in braces.
  78. return f"{open * num}{text}{close * num}"
  79. def indent(text: str, indent_level: int = 2) -> str:
  80. """Indent the given text by the given indent level.
  81. Args:
  82. text: The text to indent.
  83. indent_level: The indent level.
  84. Returns:
  85. The indented text.
  86. """
  87. lines = text.splitlines()
  88. if len(lines) < 2:
  89. return text
  90. return os.linesep.join(f"{' ' * indent_level}{line}" for line in lines) + os.linesep
  91. def to_snake_case(text: str) -> str:
  92. """Convert a string to snake case.
  93. The words in the text are converted to lowercase and
  94. separated by underscores.
  95. Args:
  96. text: The string to convert.
  97. Returns:
  98. The snake case string.
  99. """
  100. s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", text)
  101. return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
  102. def to_camel_case(text: str) -> str:
  103. """Convert a string to camel case.
  104. The first word in the text is converted to lowercase and
  105. the rest of the words are converted to title case, removing underscores.
  106. Args:
  107. text: The string to convert.
  108. Returns:
  109. The camel case string.
  110. """
  111. if "_" not in text:
  112. return text
  113. camel = "".join(
  114. word.capitalize() if i > 0 else word.lower()
  115. for i, word in enumerate(text.lstrip("_").split("_"))
  116. )
  117. prefix = "_" if text.startswith("_") else ""
  118. return prefix + camel
  119. def to_title_case(text: str) -> str:
  120. """Convert a string from snake case to title case.
  121. Args:
  122. text: The string to convert.
  123. Returns:
  124. The title case string.
  125. """
  126. return "".join(word.capitalize() for word in text.split("_"))
  127. def to_kebab_case(text: str) -> str:
  128. """Convert a string to kebab case.
  129. The words in the text are converted to lowercase and
  130. separated by hyphens.
  131. Args:
  132. text: The string to convert.
  133. Returns:
  134. The title case string.
  135. """
  136. return to_snake_case(text).replace("_", "-")
  137. def format_string(string: str) -> str:
  138. """Format the given string as a JS string literal..
  139. Args:
  140. string: The string to format.
  141. Returns:
  142. The formatted string.
  143. """
  144. # Escape backticks.
  145. string = string.replace(r"\`", "`")
  146. string = string.replace("`", r"\`")
  147. # Wrap the string so it looks like {`string`}.
  148. string = wrap(string, "`")
  149. string = wrap(string, "{")
  150. return string
  151. def format_var(var: Var) -> str:
  152. """Format the given Var as a javascript value.
  153. Args:
  154. var: The Var to format.
  155. Returns:
  156. The formatted Var.
  157. """
  158. if not var.is_local or var.is_string:
  159. return str(var)
  160. if types._issubclass(var.type_, str):
  161. return format_string(var.full_name)
  162. if is_wrapped(var.full_name, "{"):
  163. return var.full_name
  164. return json_dumps(var.full_name)
  165. def format_route(route: str, format_case=True) -> str:
  166. """Format the given route.
  167. Args:
  168. route: The route to format.
  169. format_case: whether to format case to kebab case.
  170. Returns:
  171. The formatted route.
  172. """
  173. route = route.strip("/")
  174. # Strip the route and format casing.
  175. if format_case:
  176. route = to_kebab_case(route)
  177. # If the route is empty, return the index route.
  178. if route == "":
  179. return constants.INDEX_ROUTE
  180. return route
  181. def format_cond(
  182. cond: str,
  183. true_value: str,
  184. false_value: str = '""',
  185. is_prop=False,
  186. ) -> str:
  187. """Format a conditional expression.
  188. Args:
  189. cond: The cond.
  190. true_value: The value to return if the cond is true.
  191. false_value: The value to return if the cond is false.
  192. is_prop: Whether the cond is a prop
  193. Returns:
  194. The formatted conditional expression.
  195. """
  196. # Import here to avoid circular imports.
  197. from reflex.vars import Var
  198. # Use Python truthiness.
  199. cond = f"isTrue({cond})"
  200. # Format prop conds.
  201. if is_prop:
  202. prop1 = Var.create_safe(true_value, is_string=type(true_value) is str).set(
  203. is_local=True
  204. ) # type: ignore
  205. prop2 = Var.create_safe(false_value, is_string=type(false_value) is str).set(
  206. is_local=True
  207. ) # type: ignore
  208. return f"{cond} ? {prop1} : {prop2}".replace("{", "").replace("}", "")
  209. # Format component conds.
  210. return wrap(f"{cond} ? {true_value} : {false_value}", "{")
  211. def format_prop(
  212. prop: Union[Var, EventChain, ComponentStyle, str],
  213. ) -> Union[int, float, str]:
  214. """Format a prop.
  215. Args:
  216. prop: The prop to format.
  217. Returns:
  218. The formatted prop to display within a tag.
  219. Raises:
  220. exceptions.InvalidStylePropError: If the style prop value is not a valid type.
  221. TypeError: If the prop is not valid.
  222. """
  223. # import here to avoid circular import.
  224. from reflex.event import EVENT_ARG, EventChain
  225. try:
  226. # Handle var props.
  227. if isinstance(prop, Var):
  228. if not prop.is_local or prop.is_string:
  229. return str(prop)
  230. if types._issubclass(prop.type_, str):
  231. return format_string(prop.full_name)
  232. prop = prop.full_name
  233. # Handle event props.
  234. elif isinstance(prop, EventChain):
  235. chain = ",".join([format_event(event) for event in prop.events])
  236. event = f"Event([{chain}], {EVENT_ARG})"
  237. prop = f"{EVENT_ARG} => {event}"
  238. # Handle other types.
  239. elif isinstance(prop, str):
  240. if is_wrapped(prop, "{"):
  241. return prop
  242. return json_dumps(prop)
  243. elif isinstance(prop, Figure):
  244. prop = json.loads(to_json(prop))["data"] # type: ignore
  245. # For dictionaries, convert any properties to strings.
  246. elif isinstance(prop, dict):
  247. prop = format_dict(prop)
  248. else:
  249. # Dump the prop as JSON.
  250. prop = json_dumps(prop)
  251. except exceptions.InvalidStylePropError:
  252. raise
  253. except TypeError as e:
  254. raise TypeError(f"Could not format prop: {prop} of type {type(prop)}") from e
  255. # Wrap the variable in braces.
  256. assert isinstance(prop, str), "The prop must be a string."
  257. return wrap(prop, "{", check_first=False)
  258. def get_event_handler_parts(handler: EventHandler) -> tuple[str, str]:
  259. """Get the state and function name of an event handler.
  260. Args:
  261. handler: The event handler to get the parts of.
  262. Returns:
  263. The state and function name.
  264. """
  265. # Get the class that defines the event handler.
  266. parts = handler.fn.__qualname__.split(".")
  267. # If there's no enclosing class, just return the function name.
  268. if len(parts) == 1:
  269. return ("", parts[-1])
  270. # Get the state and the function name.
  271. state_name, name = parts[-2:]
  272. # Construct the full event handler name.
  273. try:
  274. # Try to get the state from the module.
  275. state = vars(sys.modules[handler.fn.__module__])[state_name]
  276. except Exception:
  277. # If the state isn't in the module, just return the function name.
  278. return ("", handler.fn.__qualname__)
  279. return (state.get_full_name(), name)
  280. def format_event_handler(handler: EventHandler) -> str:
  281. """Format an event handler.
  282. Args:
  283. handler: The event handler to format.
  284. Returns:
  285. The formatted function.
  286. """
  287. state, name = get_event_handler_parts(handler)
  288. if state == "":
  289. return name
  290. return f"{state}.{name}"
  291. def format_event(event_spec: EventSpec) -> str:
  292. """Format an event.
  293. Args:
  294. event_spec: The event to format.
  295. Returns:
  296. The compiled event.
  297. """
  298. args = ",".join(
  299. [
  300. ":".join(
  301. (name.name, json.dumps(val.name) if val.is_string else val.full_name)
  302. )
  303. for name, val in event_spec.args
  304. ]
  305. )
  306. event_args = [
  307. wrap(format_event_handler(event_spec.handler), '"'),
  308. ]
  309. event_args.append(wrap(args, "{"))
  310. if event_spec.client_handler_name:
  311. event_args.append(wrap(event_spec.client_handler_name, '"'))
  312. return f"E({', '.join(event_args)})"
  313. def format_event_chain(
  314. event_chain: EventChain | Var[EventChain],
  315. event_arg: Var | None = None,
  316. ) -> str:
  317. """Format an event chain as a javascript invocation.
  318. Args:
  319. event_chain: The event chain to queue on the frontend.
  320. event_arg: The browser-native event (only used to preventDefault).
  321. Returns:
  322. Compiled javascript code to queue the given event chain on the frontend.
  323. Raises:
  324. ValueError: When the given event chain is not a valid event chain.
  325. """
  326. if isinstance(event_chain, Var):
  327. from reflex.event import EventChain
  328. if event_chain.type_ is not EventChain:
  329. raise ValueError(f"Invalid event chain: {event_chain}")
  330. return "".join(
  331. [
  332. "(() => {",
  333. format_var(event_chain),
  334. f"; preventDefault({format_var(event_arg)})" if event_arg else "",
  335. "})()",
  336. ]
  337. )
  338. chain = ",".join([format_event(event) for event in event_chain.events])
  339. return "".join(
  340. [
  341. f"Event([{chain}]",
  342. f", {format_var(event_arg)}" if event_arg else "",
  343. ")",
  344. ]
  345. )
  346. def format_query_params(router_data: dict[str, Any]) -> dict[str, str]:
  347. """Convert back query params name to python-friendly case.
  348. Args:
  349. router_data: the router_data dict containing the query params
  350. Returns:
  351. The reformatted query params
  352. """
  353. params = router_data[constants.RouteVar.QUERY]
  354. return {k.replace("-", "_"): v for k, v in params.items()}
  355. def format_dataframe_values(value: Type) -> list[Any]:
  356. """Format dataframe values.
  357. Args:
  358. value: The value to format.
  359. Returns:
  360. Format data
  361. """
  362. if not types.is_dataframe(type(value)):
  363. return value
  364. format_data = []
  365. for data in list(value.values.tolist()):
  366. element = []
  367. for d in data:
  368. element.append(str(d) if isinstance(d, (list, tuple)) else d)
  369. format_data.append(element)
  370. return format_data
  371. def format_image_data(value: Type) -> str:
  372. """Format image data.
  373. Args:
  374. value: The value to format.
  375. Returns:
  376. Format data
  377. """
  378. buff = io.BytesIO()
  379. value.save(buff, format="PNG")
  380. image_bytes = buff.getvalue()
  381. base64_image = base64.b64encode(image_bytes).decode("utf-8")
  382. return f"data:image/png;base64,{base64_image}"
  383. def format_state(value: Any) -> Any:
  384. """Recursively format values in the given state.
  385. Args:
  386. value: The state to format.
  387. Returns:
  388. The formatted state.
  389. Raises:
  390. TypeError: If the given value is not a valid state.
  391. """
  392. # Handle dicts.
  393. if isinstance(value, dict):
  394. return {k: format_state(v) for k, v in value.items()}
  395. # Handle lists, sets, typles.
  396. if isinstance(value, types.StateIterBases):
  397. return [format_state(v) for v in value]
  398. # Return state vars as is.
  399. if isinstance(value, types.StateBases):
  400. return value
  401. # Convert plotly figures to JSON.
  402. if isinstance(value, go.Figure):
  403. return json.loads(to_json(value))["data"] # type: ignore
  404. # Convert pandas dataframes to JSON.
  405. if types.is_dataframe(type(value)):
  406. return {
  407. "columns": value.columns.tolist(),
  408. "data": format_dataframe_values(value),
  409. }
  410. # Convert datetime objects to str.
  411. if types.is_datetime(type(value)):
  412. return str(value)
  413. # Convert Image objects to base64.
  414. if types.is_image(type(value)):
  415. return format_image_data(value) # type: ignore
  416. raise TypeError(
  417. "State vars must be primitive Python types, "
  418. "or subclasses of rx.Base. "
  419. f"Got var of type {type(value)}."
  420. )
  421. def format_ref(ref: str) -> str:
  422. """Format a ref.
  423. Args:
  424. ref: The ref to format.
  425. Returns:
  426. The formatted ref.
  427. """
  428. # Replace all non-word characters with underscores.
  429. clean_ref = re.sub(r"[^\w]+", "_", ref)
  430. return f"ref_{clean_ref}"
  431. def format_array_ref(refs: str, idx: Var | None) -> str:
  432. """Format a ref accessed by array.
  433. Args:
  434. refs : The ref array to access.
  435. idx : The index of the ref in the array.
  436. Returns:
  437. The formatted ref.
  438. """
  439. clean_ref = re.sub(r"[^\w]+", "_", refs)
  440. if idx is not None:
  441. idx.is_local = True
  442. return f"refs_{clean_ref}[{idx}]"
  443. return f"refs_{clean_ref}"
  444. def format_dict(prop: ComponentStyle) -> str:
  445. """Format a dict with vars potentially as values.
  446. Args:
  447. prop: The dict to format.
  448. Returns:
  449. The formatted dict.
  450. Raises:
  451. InvalidStylePropError: If a style prop has a callable value
  452. """
  453. # Import here to avoid circular imports.
  454. from reflex.event import EventHandler
  455. from reflex.vars import Var
  456. prop_dict = {}
  457. # Convert any var keys to strings.
  458. for key, value in prop.items():
  459. if issubclass(type(value), Callable):
  460. raise exceptions.InvalidStylePropError(
  461. f"The style prop `{to_snake_case(key)}` cannot have " # type: ignore
  462. f"`{value.fn.__qualname__ if isinstance(value, EventHandler) else value.__qualname__ if isinstance(value, builtin_types.FunctionType) else value}`, "
  463. f"an event handler or callable as its value"
  464. )
  465. prop_dict[key] = str(value) if isinstance(value, Var) else value
  466. # Dump the dict to a string.
  467. fprop = json_dumps(prop_dict)
  468. def unescape_double_quotes_in_var(m: re.Match) -> str:
  469. # Since the outer quotes are removed, the inner escaped quotes must be unescaped.
  470. return re.sub('\\\\"', '"', m.group(1))
  471. # This substitution is necessary to unwrap var values.
  472. fprop = re.sub(
  473. pattern=r"""
  474. (?<!\\) # must NOT start with a backslash
  475. " # match opening double quote of JSON value
  476. {(.*?)} # extract the value between curly braces (non-greedy)
  477. " # match must end with an unescaped double quote
  478. """,
  479. repl=unescape_double_quotes_in_var,
  480. string=fprop,
  481. flags=re.VERBOSE,
  482. )
  483. # Return the formatted dict.
  484. return fprop
  485. def format_breadcrumbs(route: str) -> list[tuple[str, str]]:
  486. """Take a route and return a list of tuple for use in breadcrumb.
  487. Args:
  488. route: The route to transform.
  489. Returns:
  490. list[tuple[str, str]]: the list of tuples for the breadcrumb.
  491. """
  492. route_parts = route.lstrip("/").split("/")
  493. # create and return breadcrumbs
  494. return [
  495. (part, op.join("/", *route_parts[: i + 1]))
  496. for i, part in enumerate(route_parts)
  497. ]
  498. def json_dumps(obj: Any) -> str:
  499. """Takes an object and returns a jsonified string.
  500. Args:
  501. obj: The object to be serialized.
  502. Returns:
  503. A string
  504. """
  505. return json.dumps(obj, ensure_ascii=False, default=list)