format.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810
  1. """Formatting operations."""
  2. from __future__ import annotations
  3. import inspect
  4. import json
  5. import os
  6. import re
  7. import sys
  8. from typing import TYPE_CHECKING, Any, List, Union
  9. from reflex import constants
  10. from reflex.utils import exceptions, serializers, types
  11. from reflex.utils.serializers import serialize
  12. from reflex.vars import BaseVar, Var
  13. if TYPE_CHECKING:
  14. from reflex.components.component import ComponentStyle
  15. from reflex.event import EventChain, EventHandler, EventSpec
  16. WRAP_MAP = {
  17. "{": "}",
  18. "(": ")",
  19. "[": "]",
  20. "<": ">",
  21. '"': '"',
  22. "'": "'",
  23. "`": "`",
  24. }
  25. def get_close_char(open: str, close: str | None = None) -> str:
  26. """Check if the given character is a valid brace.
  27. Args:
  28. open: The open character.
  29. close: The close character if provided.
  30. Returns:
  31. The close character.
  32. Raises:
  33. ValueError: If the open character is not a valid brace.
  34. """
  35. if close is not None:
  36. return close
  37. if open not in WRAP_MAP:
  38. raise ValueError(f"Invalid wrap open: {open}, must be one of {WRAP_MAP.keys()}")
  39. return WRAP_MAP[open]
  40. def is_wrapped(text: str, open: str, close: str | None = None) -> bool:
  41. """Check if the given text is wrapped in the given open and close characters.
  42. "(a) + (b)" --> False
  43. "((abc))" --> True
  44. "(abc)" --> True
  45. Args:
  46. text: The text to check.
  47. open: The open character.
  48. close: The close character.
  49. Returns:
  50. Whether the text is wrapped.
  51. """
  52. close = get_close_char(open, close)
  53. if not (text.startswith(open) and text.endswith(close)):
  54. return False
  55. depth = 0
  56. for ch in text[:-1]:
  57. if ch == open:
  58. depth += 1
  59. if ch == close:
  60. depth -= 1
  61. if depth == 0: # it shouldn't close before the end
  62. return False
  63. return True
  64. def wrap(
  65. text: str,
  66. open: str,
  67. close: str | None = None,
  68. check_first: bool = True,
  69. num: int = 1,
  70. ) -> str:
  71. """Wrap the given text in the given open and close characters.
  72. Args:
  73. text: The text to wrap.
  74. open: The open character.
  75. close: The close character.
  76. check_first: Whether to check if the text is already wrapped.
  77. num: The number of times to wrap the text.
  78. Returns:
  79. The wrapped text.
  80. """
  81. close = get_close_char(open, close)
  82. # If desired, check if the text is already wrapped in braces.
  83. if check_first and is_wrapped(text=text, open=open, close=close):
  84. return text
  85. # Wrap the text in braces.
  86. return f"{open * num}{text}{close * num}"
  87. def indent(text: str, indent_level: int = 2) -> str:
  88. """Indent the given text by the given indent level.
  89. Args:
  90. text: The text to indent.
  91. indent_level: The indent level.
  92. Returns:
  93. The indented text.
  94. """
  95. lines = text.splitlines()
  96. if len(lines) < 2:
  97. return text
  98. return os.linesep.join(f"{' ' * indent_level}{line}" for line in lines) + os.linesep
  99. def to_snake_case(text: str) -> str:
  100. """Convert a string to snake case.
  101. The words in the text are converted to lowercase and
  102. separated by underscores.
  103. Args:
  104. text: The string to convert.
  105. Returns:
  106. The snake case string.
  107. """
  108. s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", text)
  109. return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower().replace("-", "_")
  110. def to_camel_case(text: str, allow_hyphens: bool = False) -> str:
  111. """Convert a string to camel case.
  112. The first word in the text is converted to lowercase and
  113. the rest of the words are converted to title case, removing underscores.
  114. Args:
  115. text: The string to convert.
  116. allow_hyphens: Whether to allow hyphens in the string.
  117. Returns:
  118. The camel case string.
  119. """
  120. char = "_" if allow_hyphens else "-_"
  121. words = re.split(f"[{char}]", text.lstrip(char))
  122. leading_underscores_or_hyphens = "".join(re.findall(rf"^[{char}]+", text))
  123. # Capitalize the first letter of each word except the first one
  124. converted_word = words[0] + "".join(x.capitalize() for x in words[1:])
  125. return leading_underscores_or_hyphens + converted_word
  126. def to_title_case(text: str) -> str:
  127. """Convert a string from snake case to title case.
  128. Args:
  129. text: The string to convert.
  130. Returns:
  131. The title case string.
  132. """
  133. return "".join(word.capitalize() for word in text.split("_"))
  134. def to_kebab_case(text: str) -> str:
  135. """Convert a string to kebab case.
  136. The words in the text are converted to lowercase and
  137. separated by hyphens.
  138. Args:
  139. text: The string to convert.
  140. Returns:
  141. The title case string.
  142. """
  143. return to_snake_case(text).replace("_", "-")
  144. def _escape_js_string(string: str) -> str:
  145. """Escape the string for use as a JS string literal.
  146. Args:
  147. string: The string to escape.
  148. Returns:
  149. The escaped string.
  150. """
  151. # Escape backticks.
  152. string = string.replace(r"\`", "`")
  153. string = string.replace("`", r"\`")
  154. return string
  155. def _wrap_js_string(string: str) -> str:
  156. """Wrap string so it looks like {`string`}.
  157. Args:
  158. string: The string to wrap.
  159. Returns:
  160. The wrapped string.
  161. """
  162. string = wrap(string, "`")
  163. string = wrap(string, "{")
  164. return string
  165. def format_string(string: str) -> str:
  166. """Format the given string as a JS string literal..
  167. Args:
  168. string: The string to format.
  169. Returns:
  170. The formatted string.
  171. """
  172. return _wrap_js_string(_escape_js_string(string))
  173. def format_f_string_prop(prop: BaseVar) -> str:
  174. """Format the string in a given prop as an f-string.
  175. Args:
  176. prop: The prop to format.
  177. Returns:
  178. The formatted string.
  179. """
  180. s = prop._var_full_name
  181. var_data = prop._var_data
  182. interps = var_data.interpolations if var_data else []
  183. parts: List[str] = []
  184. if interps:
  185. for i, (start, end) in enumerate(interps):
  186. prev_end = interps[i - 1][1] if i > 0 else 0
  187. parts.append(_escape_js_string(s[prev_end:start]))
  188. parts.append(s[start:end])
  189. parts.append(_escape_js_string(s[interps[-1][1] :]))
  190. else:
  191. parts.append(_escape_js_string(s))
  192. return _wrap_js_string("".join(parts))
  193. def format_var(var: Var) -> str:
  194. """Format the given Var as a javascript value.
  195. Args:
  196. var: The Var to format.
  197. Returns:
  198. The formatted Var.
  199. """
  200. if not var._var_is_local or var._var_is_string:
  201. return str(var)
  202. if types._issubclass(var._var_type, str):
  203. return format_string(var._var_full_name)
  204. if is_wrapped(var._var_full_name, "{"):
  205. return var._var_full_name
  206. return json_dumps(var._var_full_name)
  207. def format_route(route: str, format_case=True) -> str:
  208. """Format the given route.
  209. Args:
  210. route: The route to format.
  211. format_case: whether to format case to kebab case.
  212. Returns:
  213. The formatted route.
  214. """
  215. route = route.strip("/")
  216. # Strip the route and format casing.
  217. if format_case:
  218. route = to_kebab_case(route)
  219. # If the route is empty, return the index route.
  220. if route == "":
  221. return constants.PageNames.INDEX_ROUTE
  222. return route
  223. def format_cond(
  224. cond: str | Var,
  225. true_value: str | Var,
  226. false_value: str | Var = '""',
  227. is_prop=False,
  228. ) -> str:
  229. """Format a conditional expression.
  230. Args:
  231. cond: The cond.
  232. true_value: The value to return if the cond is true.
  233. false_value: The value to return if the cond is false.
  234. is_prop: Whether the cond is a prop
  235. Returns:
  236. The formatted conditional expression.
  237. """
  238. # Use Python truthiness.
  239. cond = f"isTrue({cond})"
  240. def create_var(cond_part):
  241. return Var.create_safe(cond_part, _var_is_string=type(cond_part) is str)
  242. # Format prop conds.
  243. if is_prop:
  244. true_value = create_var(true_value)
  245. prop1 = true_value._replace(
  246. _var_is_local=True,
  247. )
  248. false_value = create_var(false_value)
  249. prop2 = false_value._replace(_var_is_local=True)
  250. # unwrap '{}' to avoid f-string semantics for Var
  251. return f"{cond} ? {prop1._var_name_unwrapped} : {prop2._var_name_unwrapped}"
  252. # Format component conds.
  253. return wrap(f"{cond} ? {true_value} : {false_value}", "{")
  254. def format_match(cond: str | Var, match_cases: List[BaseVar], default: Var) -> str:
  255. """Format a match expression whose return type is a Var.
  256. Args:
  257. cond: The condition.
  258. match_cases: The list of cases to match.
  259. default: The default case.
  260. Returns:
  261. The formatted match expression
  262. """
  263. switch_code = f"(() => {{ switch (JSON.stringify({cond})) {{"
  264. for case in match_cases:
  265. conditions = case[:-1]
  266. return_value = case[-1]
  267. case_conditions = " ".join(
  268. [
  269. f"case JSON.stringify({condition._var_name_unwrapped}):"
  270. for condition in conditions
  271. ]
  272. )
  273. case_code = (
  274. f"{case_conditions} return ({return_value._var_name_unwrapped}); break;"
  275. )
  276. switch_code += case_code
  277. switch_code += f"default: return ({default._var_name_unwrapped}); break;"
  278. switch_code += "};})()"
  279. return switch_code
  280. def format_prop(
  281. prop: Union[Var, EventChain, ComponentStyle, str],
  282. ) -> Union[int, float, str]:
  283. """Format a prop.
  284. Args:
  285. prop: The prop to format.
  286. Returns:
  287. The formatted prop to display within a tag.
  288. Raises:
  289. exceptions.InvalidStylePropError: If the style prop value is not a valid type.
  290. TypeError: If the prop is not valid.
  291. """
  292. # import here to avoid circular import.
  293. from reflex.event import EventChain
  294. try:
  295. # Handle var props.
  296. if isinstance(prop, Var):
  297. if not prop._var_is_local or prop._var_is_string:
  298. return str(prop)
  299. if isinstance(prop, BaseVar) and types._issubclass(prop._var_type, str):
  300. if prop._var_data and prop._var_data.interpolations:
  301. return format_f_string_prop(prop)
  302. return format_string(prop._var_full_name)
  303. prop = prop._var_full_name
  304. # Handle event props.
  305. elif isinstance(prop, EventChain):
  306. sig = inspect.signature(prop.args_spec) # type: ignore
  307. if sig.parameters:
  308. arg_def = ",".join(f"_{p}" for p in sig.parameters)
  309. arg_def = f"({arg_def})"
  310. else:
  311. # add a default argument for addEvents if none were specified in prop.args_spec
  312. # used to trigger the preventDefault() on the event.
  313. arg_def = "(_e)"
  314. chain = ",".join([format_event(event) for event in prop.events])
  315. event = f"addEvents([{chain}], {arg_def}, {json_dumps(prop.event_actions)})"
  316. prop = f"{arg_def} => {event}"
  317. # Handle other types.
  318. elif isinstance(prop, str):
  319. if is_wrapped(prop, "{"):
  320. return prop
  321. return json_dumps(prop)
  322. # For dictionaries, convert any properties to strings.
  323. elif isinstance(prop, dict):
  324. prop = serializers.serialize_dict(prop) # type: ignore
  325. else:
  326. # Dump the prop as JSON.
  327. prop = json_dumps(prop)
  328. except exceptions.InvalidStylePropError:
  329. raise
  330. except TypeError as e:
  331. raise TypeError(f"Could not format prop: {prop} of type {type(prop)}") from e
  332. # Wrap the variable in braces.
  333. assert isinstance(prop, str), "The prop must be a string."
  334. return wrap(prop, "{", check_first=False)
  335. def format_props(*single_props, **key_value_props) -> list[str]:
  336. """Format the tag's props.
  337. Args:
  338. single_props: Props that are not key-value pairs.
  339. key_value_props: Props that are key-value pairs.
  340. Returns:
  341. The formatted props list.
  342. """
  343. # Format all the props.
  344. return [
  345. f"{name}={format_prop(prop)}"
  346. for name, prop in sorted(key_value_props.items())
  347. if prop is not None
  348. ] + [str(prop) for prop in single_props]
  349. def get_event_handler_parts(handler: EventHandler) -> tuple[str, str]:
  350. """Get the state and function name of an event handler.
  351. Args:
  352. handler: The event handler to get the parts of.
  353. Returns:
  354. The state and function name.
  355. """
  356. # Get the class that defines the event handler.
  357. parts = handler.fn.__qualname__.split(".")
  358. # If there's no enclosing class, just return the function name.
  359. if len(parts) == 1:
  360. return ("", parts[-1])
  361. # Get the state and the function name.
  362. state_name, name = parts[-2:]
  363. # Construct the full event handler name.
  364. try:
  365. # Try to get the state from the module.
  366. state = vars(sys.modules[handler.fn.__module__])[state_name]
  367. except Exception:
  368. # If the state isn't in the module, just return the function name.
  369. return ("", to_snake_case(handler.fn.__qualname__))
  370. return (state.get_full_name(), name)
  371. def format_event_handler(handler: EventHandler) -> str:
  372. """Format an event handler.
  373. Args:
  374. handler: The event handler to format.
  375. Returns:
  376. The formatted function.
  377. """
  378. state, name = get_event_handler_parts(handler)
  379. if state == "":
  380. return name
  381. return f"{state}.{name}"
  382. def format_event(event_spec: EventSpec) -> str:
  383. """Format an event.
  384. Args:
  385. event_spec: The event to format.
  386. Returns:
  387. The compiled event.
  388. """
  389. args = ",".join(
  390. [
  391. ":".join(
  392. (
  393. name._var_name,
  394. wrap(json.dumps(val._var_name).strip('"').replace("`", "\\`"), "`")
  395. if val._var_is_string
  396. else val._var_full_name,
  397. )
  398. )
  399. for name, val in event_spec.args
  400. ]
  401. )
  402. event_args = [
  403. wrap(format_event_handler(event_spec.handler), '"'),
  404. ]
  405. event_args.append(wrap(args, "{"))
  406. if event_spec.client_handler_name:
  407. event_args.append(wrap(event_spec.client_handler_name, '"'))
  408. return f"Event({', '.join(event_args)})"
  409. def format_event_chain(
  410. event_chain: EventChain | Var[EventChain],
  411. event_arg: Var | None = None,
  412. ) -> str:
  413. """Format an event chain as a javascript invocation.
  414. Args:
  415. event_chain: The event chain to queue on the frontend.
  416. event_arg: The browser-native event (only used to preventDefault).
  417. Returns:
  418. Compiled javascript code to queue the given event chain on the frontend.
  419. Raises:
  420. ValueError: When the given event chain is not a valid event chain.
  421. """
  422. if isinstance(event_chain, Var):
  423. from reflex.event import EventChain
  424. if event_chain._var_type is not EventChain:
  425. raise ValueError(f"Invalid event chain: {event_chain}")
  426. return "".join(
  427. [
  428. "(() => {",
  429. format_var(event_chain),
  430. f"; preventDefault({format_var(event_arg)})" if event_arg else "",
  431. "})()",
  432. ]
  433. )
  434. chain = ",".join([format_event(event) for event in event_chain.events])
  435. return "".join(
  436. [
  437. f"addEvents([{chain}]",
  438. f", {format_var(event_arg)}" if event_arg else "",
  439. ")",
  440. ]
  441. )
  442. def format_query_params(router_data: dict[str, Any]) -> dict[str, str]:
  443. """Convert back query params name to python-friendly case.
  444. Args:
  445. router_data: the router_data dict containing the query params
  446. Returns:
  447. The reformatted query params
  448. """
  449. params = router_data[constants.RouteVar.QUERY]
  450. return {k.replace("-", "_"): v for k, v in params.items()}
  451. def format_state(value: Any) -> Any:
  452. """Recursively format values in the given state.
  453. Args:
  454. value: The state to format.
  455. Returns:
  456. The formatted state.
  457. Raises:
  458. TypeError: If the given value is not a valid state.
  459. """
  460. # Handle dicts.
  461. if isinstance(value, dict):
  462. return {k: format_state(v) for k, v in value.items()}
  463. # Handle lists, sets, typles.
  464. if isinstance(value, types.StateIterBases):
  465. return [format_state(v) for v in value]
  466. # Return state vars as is.
  467. if isinstance(value, types.StateBases):
  468. return value
  469. # Serialize the value.
  470. serialized = serialize(value)
  471. if serialized is not None:
  472. return serialized
  473. raise TypeError(f"No JSON serializer found for var {value} of type {type(value)}.")
  474. def format_state_name(state_name: str) -> str:
  475. """Format a state name, replacing dots with double underscore.
  476. This allows individual substates to be accessed independently as javascript vars
  477. without using dot notation.
  478. Args:
  479. state_name: The state name to format.
  480. Returns:
  481. The formatted state name.
  482. """
  483. return state_name.replace(".", "__")
  484. def format_ref(ref: str) -> str:
  485. """Format a ref.
  486. Args:
  487. ref: The ref to format.
  488. Returns:
  489. The formatted ref.
  490. """
  491. # Replace all non-word characters with underscores.
  492. clean_ref = re.sub(r"[^\w]+", "_", ref)
  493. return f"ref_{clean_ref}"
  494. def format_array_ref(refs: str, idx: Var | None) -> str:
  495. """Format a ref accessed by array.
  496. Args:
  497. refs : The ref array to access.
  498. idx : The index of the ref in the array.
  499. Returns:
  500. The formatted ref.
  501. """
  502. clean_ref = re.sub(r"[^\w]+", "_", refs)
  503. if idx is not None:
  504. idx._var_is_local = True
  505. return f"refs_{clean_ref}[{idx}]"
  506. return f"refs_{clean_ref}"
  507. def format_breadcrumbs(route: str) -> list[tuple[str, str]]:
  508. """Take a route and return a list of tuple for use in breadcrumb.
  509. Args:
  510. route: The route to transform.
  511. Returns:
  512. list[tuple[str, str]]: the list of tuples for the breadcrumb.
  513. """
  514. route_parts = route.lstrip("/").split("/")
  515. # create and return breadcrumbs
  516. return [
  517. (part, "/".join(["", *route_parts[: i + 1]]))
  518. for i, part in enumerate(route_parts)
  519. ]
  520. def format_library_name(library_fullname: str):
  521. """Format the name of a library.
  522. Args:
  523. library_fullname: The fullname of the library.
  524. Returns:
  525. The name without the @version if it was part of the name
  526. """
  527. lib, at, version = library_fullname.rpartition("@")
  528. if not lib:
  529. lib = at + version
  530. return lib
  531. def json_dumps(obj: Any) -> str:
  532. """Takes an object and returns a jsonified string.
  533. Args:
  534. obj: The object to be serialized.
  535. Returns:
  536. A string
  537. """
  538. return json.dumps(obj, ensure_ascii=False, default=serialize)
  539. def unwrap_vars(value: str) -> str:
  540. """Unwrap var values from a JSON string.
  541. For example, "{var}" will be unwrapped to "var".
  542. Args:
  543. value: The JSON string to unwrap.
  544. Returns:
  545. The unwrapped JSON string.
  546. """
  547. def unescape_double_quotes_in_var(m: re.Match) -> str:
  548. prefix = m.group(1) or ""
  549. # Since the outer quotes are removed, the inner escaped quotes must be unescaped.
  550. return prefix + re.sub('\\\\"', '"', m.group(2))
  551. # This substitution is necessary to unwrap var values.
  552. return (
  553. re.sub(
  554. pattern=r"""
  555. (?<!\\) # must NOT start with a backslash
  556. " # match opening double quote of JSON value
  557. (<reflex.Var>.*?</reflex.Var>)? # Optional encoded VarData (non-greedy)
  558. {(.*?)} # extract the value between curly braces (non-greedy)
  559. " # match must end with an unescaped double quote
  560. """,
  561. repl=unescape_double_quotes_in_var,
  562. string=value,
  563. flags=re.VERBOSE,
  564. )
  565. .replace('"`', "`")
  566. .replace('`"', "`")
  567. )
  568. def collect_form_dict_names(form_dict: dict[str, Any]) -> dict[str, Any]:
  569. """Collapse keys with consecutive suffixes into a single list value.
  570. Separators dash and underscore are removed, unless this would overwrite an existing key.
  571. Args:
  572. form_dict: The dict to collapse.
  573. Returns:
  574. The collapsed dict.
  575. """
  576. ending_digit_regex = re.compile(r"^(.*?)[_-]?(\d+)$")
  577. collapsed = {}
  578. for k in sorted(form_dict):
  579. m = ending_digit_regex.match(k)
  580. if m:
  581. collapsed.setdefault(m.group(1), []).append(form_dict[k])
  582. # collapsing never overwrites valid data from the form_dict
  583. collapsed.update(form_dict)
  584. return collapsed
  585. def format_data_editor_column(col: str | dict):
  586. """Format a given column into the proper format.
  587. Args:
  588. col: The column.
  589. Raises:
  590. ValueError: invalid type provided for column.
  591. Returns:
  592. The formatted column.
  593. """
  594. if isinstance(col, str):
  595. return {"title": col, "id": col.lower(), "type": "str"}
  596. if isinstance(col, (dict,)):
  597. if "id" not in col:
  598. col["id"] = col["title"].lower()
  599. if "type" not in col:
  600. col["type"] = "str"
  601. if "overlayIcon" not in col:
  602. col["overlayIcon"] = None
  603. return col
  604. if isinstance(col, BaseVar):
  605. return col
  606. raise ValueError(
  607. f"unexpected type ({(type(col).__name__)}: {col}) for column header in data_editor"
  608. )
  609. def format_data_editor_cell(cell: Any):
  610. """Format a given data into a renderable cell for data_editor.
  611. Args:
  612. cell: The data to format.
  613. Returns:
  614. The formatted cell.
  615. """
  616. return {"kind": Var.create(value="GridCellKind.Text"), "data": cell}