format.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. """Formatting operations."""
  2. from __future__ import annotations
  3. import json
  4. import os
  5. import re
  6. import sys
  7. from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type
  8. import plotly.graph_objects as go
  9. from plotly.io import to_json
  10. from pynecone import constants
  11. from pynecone.utils import types
  12. if TYPE_CHECKING:
  13. from pynecone.event import EventChain, EventHandler, EventSpec
  14. WRAP_MAP = {
  15. "{": "}",
  16. "(": ")",
  17. "[": "]",
  18. "<": ">",
  19. '"': '"',
  20. "'": "'",
  21. "`": "`",
  22. }
  23. def get_close_char(open: str, close: Optional[str] = None) -> str:
  24. """Check if the given character is a valid brace.
  25. Args:
  26. open: The open character.
  27. close: The close character if provided.
  28. Returns:
  29. The close character.
  30. Raises:
  31. ValueError: If the open character is not a valid brace.
  32. """
  33. if close is not None:
  34. return close
  35. if open not in WRAP_MAP:
  36. raise ValueError(f"Invalid wrap open: {open}, must be one of {WRAP_MAP.keys()}")
  37. return WRAP_MAP[open]
  38. def is_wrapped(text: str, open: str, close: Optional[str] = None) -> bool:
  39. """Check if the given text is wrapped in the given open and close characters.
  40. Args:
  41. text: The text to check.
  42. open: The open character.
  43. close: The close character.
  44. Returns:
  45. Whether the text is wrapped.
  46. """
  47. close = get_close_char(open, close)
  48. return text.startswith(open) and text.endswith(close)
  49. def wrap(
  50. text: str,
  51. open: str,
  52. close: Optional[str] = None,
  53. check_first: bool = True,
  54. num: int = 1,
  55. ) -> str:
  56. """Wrap the given text in the given open and close characters.
  57. Args:
  58. text: The text to wrap.
  59. open: The open character.
  60. close: The close character.
  61. check_first: Whether to check if the text is already wrapped.
  62. num: The number of times to wrap the text.
  63. Returns:
  64. The wrapped text.
  65. """
  66. close = get_close_char(open, close)
  67. # If desired, check if the text is already wrapped in braces.
  68. if check_first and is_wrapped(text=text, open=open, close=close):
  69. return text
  70. # Wrap the text in braces.
  71. return f"{open * num}{text}{close * num}"
  72. def indent(text: str, indent_level: int = 2) -> str:
  73. """Indent the given text by the given indent level.
  74. Args:
  75. text: The text to indent.
  76. indent_level: The indent level.
  77. Returns:
  78. The indented text.
  79. """
  80. lines = text.splitlines()
  81. if len(lines) < 2:
  82. return text
  83. return os.linesep.join(f"{' ' * indent_level}{line}" for line in lines) + os.linesep
  84. def to_snake_case(text: str) -> str:
  85. """Convert a string to snake case.
  86. The words in the text are converted to lowercase and
  87. separated by underscores.
  88. Args:
  89. text: The string to convert.
  90. Returns:
  91. The snake case string.
  92. """
  93. s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", text)
  94. return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
  95. def to_camel_case(text: str) -> str:
  96. """Convert a string to camel case.
  97. The first word in the text is converted to lowercase and
  98. the rest of the words are converted to title case, removing underscores.
  99. Args:
  100. text: The string to convert.
  101. Returns:
  102. The camel case string.
  103. """
  104. if "_" not in text:
  105. return text
  106. camel = "".join(
  107. word.capitalize() if i > 0 else word.lower()
  108. for i, word in enumerate(text.lstrip("_").split("_"))
  109. )
  110. prefix = "_" if text.startswith("_") else ""
  111. return prefix + camel
  112. def to_title_case(text: str) -> str:
  113. """Convert a string from snake case to title case.
  114. Args:
  115. text: The string to convert.
  116. Returns:
  117. The title case string.
  118. """
  119. return "".join(word.capitalize() for word in text.split("_"))
  120. def format_string(string: str) -> str:
  121. """Format the given string as a JS string literal..
  122. Args:
  123. string: The string to format.
  124. Returns:
  125. The formatted string.
  126. """
  127. # Escape backticks.
  128. string = string.replace(r"\`", "`")
  129. string = string.replace("`", r"\`")
  130. # Wrap the string so it looks like {`string`}.
  131. string = wrap(string, "`")
  132. string = wrap(string, "{")
  133. return string
  134. def format_route(route: str) -> str:
  135. """Format the given route.
  136. Args:
  137. route: The route to format.
  138. Returns:
  139. The formatted route.
  140. """
  141. # Strip the route.
  142. route = route.strip("/")
  143. route = to_snake_case(route).replace("_", "-")
  144. # If the route is empty, return the index route.
  145. if route == "":
  146. return constants.INDEX_ROUTE
  147. return route
  148. def format_cond(
  149. cond: str,
  150. true_value: str,
  151. false_value: str = '""',
  152. is_prop=False,
  153. ) -> str:
  154. """Format a conditional expression.
  155. Args:
  156. cond: The cond.
  157. true_value: The value to return if the cond is true.
  158. false_value: The value to return if the cond is false.
  159. is_prop: Whether the cond is a prop
  160. Returns:
  161. The formatted conditional expression.
  162. """
  163. # Import here to avoid circular imports.
  164. from pynecone.var import Var
  165. # Use Python truthiness.
  166. cond = f"isTrue({cond})"
  167. # Format prop conds.
  168. if is_prop:
  169. prop1 = Var.create(true_value, is_string=type(true_value) == str)
  170. prop2 = Var.create(false_value, is_string=type(false_value) == str)
  171. assert prop1 is not None and prop2 is not None, "Invalid prop values"
  172. return f"{cond} ? {prop1} : {prop2}".replace("{", "").replace("}", "")
  173. # Format component conds.
  174. return wrap(f"{cond} ? {true_value} : {false_value}", "{")
  175. def get_event_handler_parts(handler: EventHandler) -> Tuple[str, str]:
  176. """Get the state and function name of an event handler.
  177. Args:
  178. handler: The event handler to get the parts of.
  179. Returns:
  180. The state and function name.
  181. """
  182. # Get the class that defines the event handler.
  183. parts = handler.fn.__qualname__.split(".")
  184. # If there's no enclosing class, just return the function name.
  185. if len(parts) == 1:
  186. return ("", parts[-1])
  187. # Get the state and the function name.
  188. state_name, name = parts[-2:]
  189. # Construct the full event handler name.
  190. try:
  191. # Try to get the state from the module.
  192. state = vars(sys.modules[handler.fn.__module__])[state_name]
  193. except Exception:
  194. # If the state isn't in the module, just return the function name.
  195. return ("", handler.fn.__qualname__)
  196. return (state.get_full_name(), name)
  197. def format_event_handler(handler: EventHandler) -> str:
  198. """Format an event handler.
  199. Args:
  200. handler: The event handler to format.
  201. Returns:
  202. The formatted function.
  203. """
  204. state, name = get_event_handler_parts(handler)
  205. if state == "":
  206. return name
  207. return f"{state}.{name}"
  208. def format_event(event_spec: EventSpec) -> str:
  209. """Format an event.
  210. Args:
  211. event_spec: The event to format.
  212. Returns:
  213. The compiled event.
  214. """
  215. args = ",".join(
  216. [
  217. ":".join((name.name, json.dumps(val.name) if val.is_string else val.name))
  218. for name, val in event_spec.args
  219. ]
  220. )
  221. return f"E(\"{format_event_handler(event_spec.handler)}\", {wrap(args, '{')})"
  222. def format_upload_event(event_spec: EventSpec) -> str:
  223. """Format an upload event.
  224. Args:
  225. event_spec: The event to format.
  226. Returns:
  227. The compiled event.
  228. """
  229. from pynecone.compiler import templates
  230. state, name = get_event_handler_parts(event_spec.handler)
  231. parent_state = state.split(".")[0]
  232. return f'uploadFiles({parent_state}, {templates.RESULT}, {templates.SET_RESULT}, {parent_state}.files, "{state}.{name}",UPLOAD)'
  233. def format_full_control_event(event_chain: EventChain) -> str:
  234. """Format a fully controlled input prop.
  235. Args:
  236. event_chain: The event chain for full controlled input.
  237. Returns:
  238. The compiled event.
  239. """
  240. from pynecone.compiler import templates
  241. event_spec = event_chain.events[0]
  242. arg = event_spec.args[0][1]
  243. state_name = event_chain.state_name
  244. chain = ",".join([format_event(event) for event in event_chain.events])
  245. event = templates.FULL_CONTROL(state_name=state_name, arg=arg, chain=chain)
  246. return event
  247. def format_query_params(router_data: Dict[str, Any]) -> Dict[str, str]:
  248. """Convert back query params name to python-friendly case.
  249. Args:
  250. router_data: the router_data dict containing the query params
  251. Returns:
  252. The reformatted query params
  253. """
  254. params = router_data[constants.RouteVar.QUERY]
  255. return {k.replace("-", "_"): v for k, v in params.items()}
  256. def format_dataframe_values(value: Type) -> List[Any]:
  257. """Format dataframe values.
  258. Args:
  259. value: The value to format.
  260. Returns:
  261. Format data
  262. """
  263. if not types.is_dataframe(type(value)):
  264. return value
  265. format_data = []
  266. for data in list(value.values.tolist()):
  267. element = []
  268. for d in data:
  269. element.append(str(d) if isinstance(d, (list, tuple)) else d)
  270. format_data.append(element)
  271. return format_data
  272. def format_state(value: Any) -> Dict:
  273. """Recursively format values in the given state.
  274. Args:
  275. value: The state to format.
  276. Returns:
  277. The formatted state.
  278. Raises:
  279. TypeError: If the given value is not a valid state.
  280. """
  281. # Handle dicts.
  282. if isinstance(value, dict):
  283. return {k: format_state(v) for k, v in value.items()}
  284. # Return state vars as is.
  285. if isinstance(value, types.StateBases):
  286. return value
  287. # Convert plotly figures to JSON.
  288. if isinstance(value, go.Figure):
  289. return json.loads(to_json(value))["data"] # type: ignore
  290. # Convert pandas dataframes to JSON.
  291. if types.is_dataframe(type(value)):
  292. return {
  293. "columns": value.columns.tolist(),
  294. "data": format_dataframe_values(value),
  295. }
  296. raise TypeError(
  297. "State vars must be primitive Python types, "
  298. "or subclasses of pc.Base. "
  299. f"Got var of type {type(value)}."
  300. )
  301. def format_ref(ref: str) -> str:
  302. """Format a ref.
  303. Args:
  304. ref: The ref to format.
  305. Returns:
  306. The formatted ref.
  307. """
  308. # Replace all non-word characters with underscores.
  309. clean_ref = re.sub(r"[^\w]+", "_", ref)
  310. return f"ref_{clean_ref}"
  311. def json_dumps(obj: Any) -> str:
  312. """Takes an object and returns a jsonified string.
  313. Args:
  314. obj: The object to be serialized.
  315. Returns:
  316. A string
  317. """
  318. return json.dumps(obj, ensure_ascii=False)