format.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768
  1. """Formatting operations."""
  2. from __future__ import annotations
  3. import inspect
  4. import json
  5. import os
  6. import re
  7. from typing import TYPE_CHECKING, Any, Union
  8. from reflex import constants
  9. from reflex.constants.state import FRONTEND_EVENT_STATE
  10. from reflex.utils import exceptions
  11. if TYPE_CHECKING:
  12. from reflex.components.component import ComponentStyle
  13. from reflex.event import ArgsSpec, EventChain, EventHandler, EventSpec, EventType
  14. WRAP_MAP = {
  15. "{": "}",
  16. "(": ")",
  17. "[": "]",
  18. "<": ">",
  19. '"': '"',
  20. "'": "'",
  21. "`": "`",
  22. }
  23. def length_of_largest_common_substring(str1: str, str2: str) -> int:
  24. """Find the length of the largest common substring between two strings.
  25. Args:
  26. str1: The first string.
  27. str2: The second string.
  28. Returns:
  29. The length of the largest common substring.
  30. """
  31. if not str1 or not str2:
  32. return 0
  33. # Create a matrix of size (len(str1) + 1) x (len(str2) + 1)
  34. dp = [[0] * (len(str2) + 1) for _ in range(len(str1) + 1)]
  35. # Variables to keep track of maximum length and ending position
  36. max_length = 0
  37. # Fill the dp matrix
  38. for i in range(1, len(str1) + 1):
  39. for j in range(1, len(str2) + 1):
  40. if str1[i - 1] == str2[j - 1]:
  41. dp[i][j] = dp[i - 1][j - 1] + 1
  42. if dp[i][j] > max_length:
  43. max_length = dp[i][j]
  44. return max_length
  45. def get_close_char(open: str, close: str | None = None) -> str:
  46. """Check if the given character is a valid brace.
  47. Args:
  48. open: The open character.
  49. close: The close character if provided.
  50. Returns:
  51. The close character.
  52. Raises:
  53. ValueError: If the open character is not a valid brace.
  54. """
  55. if close is not None:
  56. return close
  57. if open not in WRAP_MAP:
  58. raise ValueError(f"Invalid wrap open: {open}, must be one of {WRAP_MAP.keys()}")
  59. return WRAP_MAP[open]
  60. def is_wrapped(text: str, open: str, close: str | None = None) -> bool:
  61. """Check if the given text is wrapped in the given open and close characters.
  62. "(a) + (b)" --> False
  63. "((abc))" --> True
  64. "(abc)" --> True
  65. Args:
  66. text: The text to check.
  67. open: The open character.
  68. close: The close character.
  69. Returns:
  70. Whether the text is wrapped.
  71. """
  72. close = get_close_char(open, close)
  73. if not (text.startswith(open) and text.endswith(close)):
  74. return False
  75. depth = 0
  76. for ch in text[:-1]:
  77. if ch == open:
  78. depth += 1
  79. if ch == close:
  80. depth -= 1
  81. if depth == 0: # it shouldn't close before the end
  82. return False
  83. return True
  84. def wrap(
  85. text: str,
  86. open: str,
  87. close: str | None = None,
  88. check_first: bool = True,
  89. num: int = 1,
  90. ) -> str:
  91. """Wrap the given text in the given open and close characters.
  92. Args:
  93. text: The text to wrap.
  94. open: The open character.
  95. close: The close character.
  96. check_first: Whether to check if the text is already wrapped.
  97. num: The number of times to wrap the text.
  98. Returns:
  99. The wrapped text.
  100. """
  101. close = get_close_char(open, close)
  102. # If desired, check if the text is already wrapped in braces.
  103. if check_first and is_wrapped(text=text, open=open, close=close):
  104. return text
  105. # Wrap the text in braces.
  106. return f"{open * num}{text}{close * num}"
  107. def indent(text: str, indent_level: int = 2) -> str:
  108. """Indent the given text by the given indent level.
  109. Args:
  110. text: The text to indent.
  111. indent_level: The indent level.
  112. Returns:
  113. The indented text.
  114. """
  115. lines = text.splitlines()
  116. if len(lines) < 2:
  117. return text
  118. return os.linesep.join(f"{' ' * indent_level}{line}" for line in lines) + os.linesep
  119. def to_snake_case(text: str) -> str:
  120. """Convert a string to snake case.
  121. The words in the text are converted to lowercase and
  122. separated by underscores.
  123. Args:
  124. text: The string to convert.
  125. Returns:
  126. The snake case string.
  127. """
  128. s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", text)
  129. return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower().replace("-", "_")
  130. def to_camel_case(text: str, treat_hyphens_as_underscores: bool = True) -> str:
  131. """Convert a string to camel case.
  132. The first word in the text is converted to lowercase and
  133. the rest of the words are converted to title case, removing underscores.
  134. Args:
  135. text: The string to convert.
  136. treat_hyphens_as_underscores: Whether to allow hyphens in the string.
  137. Returns:
  138. The camel case string.
  139. """
  140. char = "_" if not treat_hyphens_as_underscores else "-_"
  141. words = re.split(f"[{char}]", text)
  142. # Capitalize the first letter of each word except the first one
  143. converted_word = words[0] + "".join(x.capitalize() for x in words[1:])
  144. return converted_word
  145. def to_title_case(text: str, sep: str = "") -> str:
  146. """Convert a string from snake case to title case.
  147. Args:
  148. text: The string to convert.
  149. sep: The separator to use to join the words.
  150. Returns:
  151. The title case string.
  152. """
  153. return sep.join(word.title() for word in text.split("_"))
  154. def to_kebab_case(text: str) -> str:
  155. """Convert a string to kebab case.
  156. The words in the text are converted to lowercase and
  157. separated by hyphens.
  158. Args:
  159. text: The string to convert.
  160. Returns:
  161. The title case string.
  162. """
  163. return to_snake_case(text).replace("_", "-")
  164. def make_default_page_title(app_name: str, route: str) -> str:
  165. """Make a default page title from a route.
  166. Args:
  167. app_name: The name of the app owning the page.
  168. route: The route to make the title from.
  169. Returns:
  170. The default page title.
  171. """
  172. route_parts = [
  173. part
  174. for part in route.split("/")
  175. if part and not (part.startswith("[") and part.endswith("]"))
  176. ]
  177. title = constants.DefaultPage.TITLE.format(
  178. app_name, route_parts[-1] if route_parts else constants.PageNames.INDEX_ROUTE
  179. )
  180. return to_title_case(title)
  181. def _escape_js_string(string: str) -> str:
  182. """Escape the string for use as a JS string literal.
  183. Args:
  184. string: The string to escape.
  185. Returns:
  186. The escaped string.
  187. """
  188. # TODO: we may need to re-vist this logic after new Var API is implemented.
  189. def escape_outside_segments(segment: str):
  190. """Escape backticks in segments outside of `${}`.
  191. Args:
  192. segment: The part of the string to escape.
  193. Returns:
  194. The escaped or unescaped segment.
  195. """
  196. if segment.startswith("${") and segment.endswith("}"):
  197. # Return the `${}` segment unchanged
  198. return segment
  199. else:
  200. # Escape backticks in the segment
  201. segment = segment.replace(r"\`", "`")
  202. segment = segment.replace("`", r"\`")
  203. return segment
  204. # Split the string into parts, keeping the `${}` segments
  205. parts = re.split(r"(\$\{.*?\})", string)
  206. escaped_parts = [escape_outside_segments(part) for part in parts]
  207. escaped_string = "".join(escaped_parts)
  208. return escaped_string
  209. def _wrap_js_string(string: str) -> str:
  210. """Wrap string so it looks like {`string`}.
  211. Args:
  212. string: The string to wrap.
  213. Returns:
  214. The wrapped string.
  215. """
  216. string = wrap(string, "`")
  217. string = wrap(string, "{")
  218. return string
  219. def format_string(string: str) -> str:
  220. """Format the given string as a JS string literal..
  221. Args:
  222. string: The string to format.
  223. Returns:
  224. The formatted string.
  225. """
  226. return _wrap_js_string(_escape_js_string(string))
  227. def format_var(var: Var) -> str:
  228. """Format the given Var as a javascript value.
  229. Args:
  230. var: The Var to format.
  231. Returns:
  232. The formatted Var.
  233. """
  234. return str(var)
  235. def format_route(route: str, format_case: bool = True) -> str:
  236. """Format the given route.
  237. Args:
  238. route: The route to format.
  239. format_case: whether to format case to kebab case.
  240. Returns:
  241. The formatted route.
  242. """
  243. route = route.strip("/")
  244. # Strip the route and format casing.
  245. if format_case:
  246. route = to_kebab_case(route)
  247. # If the route is empty, return the index route.
  248. if route == "":
  249. return constants.PageNames.INDEX_ROUTE
  250. return route
  251. def format_match(
  252. cond: str | Var,
  253. match_cases: list[list[Var]],
  254. default: Var,
  255. ) -> str:
  256. """Format a match expression whose return type is a Var.
  257. Args:
  258. cond: The condition.
  259. match_cases: The list of cases to match.
  260. default: The default case.
  261. Returns:
  262. The formatted match expression
  263. """
  264. switch_code = f"(() => {{ switch (JSON.stringify({cond})) {{"
  265. for case in match_cases:
  266. conditions = case[:-1]
  267. return_value = case[-1]
  268. case_conditions = " ".join(
  269. [f"case JSON.stringify({condition!s}):" for condition in conditions]
  270. )
  271. case_code = f"{case_conditions} return ({return_value!s}); break;"
  272. switch_code += case_code
  273. switch_code += f"default: return ({default!s}); break;"
  274. switch_code += "};})()"
  275. return switch_code
  276. def format_prop(
  277. prop: Union[Var, EventChain, ComponentStyle, str],
  278. ) -> int | float | str:
  279. """Format a prop.
  280. Args:
  281. prop: The prop to format.
  282. Returns:
  283. The formatted prop to display within a tag.
  284. Raises:
  285. exceptions.InvalidStylePropError: If the style prop value is not a valid type.
  286. TypeError: If the prop is not valid.
  287. ValueError: If the prop is not a string.
  288. """
  289. # import here to avoid circular import.
  290. from reflex.event import EventChain
  291. from reflex.utils import serializers
  292. from reflex.vars import Var
  293. try:
  294. # Handle var props.
  295. if isinstance(prop, Var):
  296. return str(prop)
  297. # Handle event props.
  298. if isinstance(prop, EventChain):
  299. return str(Var.create(prop))
  300. # Handle other types.
  301. elif isinstance(prop, str):
  302. if is_wrapped(prop, "{"):
  303. return prop
  304. return json_dumps(prop)
  305. # For dictionaries, convert any properties to strings.
  306. elif isinstance(prop, dict):
  307. prop = serializers.serialize_dict(prop) # pyright: ignore [reportAttributeAccessIssue]
  308. else:
  309. # Dump the prop as JSON.
  310. prop = json_dumps(prop)
  311. except exceptions.InvalidStylePropError:
  312. raise
  313. except TypeError as e:
  314. raise TypeError(f"Could not format prop: {prop} of type {type(prop)}") from e
  315. # Wrap the variable in braces.
  316. if not isinstance(prop, str):
  317. raise ValueError(f"Invalid prop: {prop}. Expected a string.")
  318. return wrap(prop, "{", check_first=False)
  319. def format_props(*single_props, **key_value_props) -> list[str]:
  320. """Format the tag's props.
  321. Args:
  322. single_props: Props that are not key-value pairs.
  323. key_value_props: Props that are key-value pairs.
  324. Returns:
  325. The formatted props list.
  326. """
  327. # Format all the props.
  328. from reflex.vars.base import LiteralVar, Var
  329. return [
  330. (
  331. f"{name}={{{format_prop(prop if isinstance(prop, Var) else LiteralVar.create(prop))}}}"
  332. )
  333. for name, prop in sorted(key_value_props.items())
  334. if prop is not None
  335. ] + [(f"{LiteralVar.create(prop)!s}") for prop in single_props]
  336. def get_event_handler_parts(handler: EventHandler) -> tuple[str, str]:
  337. """Get the state and function name of an event handler.
  338. Args:
  339. handler: The event handler to get the parts of.
  340. Returns:
  341. The state and function name.
  342. """
  343. # Get the class that defines the event handler.
  344. parts = handler.fn.__qualname__.split(".")
  345. # Get the state full name
  346. state_full_name = handler.state_full_name
  347. # If there's no enclosing class, just return the function name.
  348. if not state_full_name:
  349. return ("", parts[-1])
  350. # Get the function name
  351. name = parts[-1]
  352. from reflex.state import State
  353. if state_full_name == FRONTEND_EVENT_STATE and name not in State.__dict__:
  354. return ("", to_snake_case(handler.fn.__qualname__))
  355. return (state_full_name, name)
  356. def format_event_handler(handler: EventHandler) -> str:
  357. """Format an event handler.
  358. Args:
  359. handler: The event handler to format.
  360. Returns:
  361. The formatted function.
  362. """
  363. state, name = get_event_handler_parts(handler)
  364. if state == "":
  365. return name
  366. return f"{state}.{name}"
  367. def format_event(event_spec: EventSpec) -> str:
  368. """Format an event.
  369. Args:
  370. event_spec: The event to format.
  371. Returns:
  372. The compiled event.
  373. """
  374. args = ",".join(
  375. [
  376. ":".join(
  377. (
  378. name._js_expr,
  379. (
  380. wrap(
  381. json.dumps(val._js_expr).strip('"').replace("`", "\\`"),
  382. "`",
  383. )
  384. if val._var_is_string
  385. else str(val)
  386. ),
  387. )
  388. )
  389. for name, val in event_spec.args
  390. ]
  391. )
  392. event_args = [
  393. wrap(format_event_handler(event_spec.handler), '"'),
  394. ]
  395. event_args.append(wrap(args, "{"))
  396. if event_spec.client_handler_name:
  397. event_args.append(wrap(event_spec.client_handler_name, '"'))
  398. return f"Event({', '.join(event_args)})"
  399. if TYPE_CHECKING:
  400. from reflex.vars import Var
  401. def format_queue_events(
  402. events: EventType[Any] | None = None,
  403. args_spec: ArgsSpec | None = None,
  404. ) -> Var[EventChain]:
  405. """Format a list of event handler / event spec as a javascript callback.
  406. The resulting code can be passed to interfaces that expect a callback
  407. function and when triggered it will directly call queueEvents.
  408. It is intended to be executed in the rx.call_script context, where some
  409. existing API needs a callback to trigger a backend event handler.
  410. Args:
  411. events: The events to queue.
  412. args_spec: The argument spec for the callback.
  413. Returns:
  414. The compiled javascript callback to queue the given events on the frontend.
  415. Raises:
  416. ValueError: If a lambda function is given which returns a Var.
  417. """
  418. from reflex.event import (
  419. EventChain,
  420. EventHandler,
  421. EventSpec,
  422. call_event_fn,
  423. call_event_handler,
  424. )
  425. from reflex.vars import FunctionVar, Var
  426. if not events:
  427. return Var("(() => null)").to(FunctionVar, EventChain)
  428. # If no spec is provided, the function will take no arguments.
  429. def _default_args_spec():
  430. return []
  431. # Construct the arguments that the function accepts.
  432. sig = inspect.signature(args_spec or _default_args_spec)
  433. if sig.parameters:
  434. arg_def = ",".join(f"_{p}" for p in sig.parameters)
  435. arg_def = f"({arg_def})"
  436. else:
  437. arg_def = "()"
  438. payloads = []
  439. if not isinstance(events, list):
  440. events = [events]
  441. # Process each event/spec/lambda (similar to Component._create_event_chain).
  442. for spec in events:
  443. specs: list[EventSpec] = []
  444. if isinstance(spec, (EventHandler, EventSpec)):
  445. specs = [call_event_handler(spec, args_spec or _default_args_spec)]
  446. elif isinstance(spec, type(lambda: None)):
  447. specs = call_event_fn(spec, args_spec or _default_args_spec) # pyright: ignore [reportAssignmentType, reportArgumentType]
  448. if isinstance(specs, Var):
  449. raise ValueError(
  450. f"Invalid event spec: {specs}. Expected a list of EventSpecs."
  451. )
  452. payloads.extend(format_event(s) for s in specs)
  453. # Return the final code snippet, expecting queueEvents, processEvent, and socket to be in scope.
  454. # Typically this snippet will _only_ run from within an rx.call_script eval context.
  455. return Var(
  456. f"{arg_def} => {{queueEvents([{','.join(payloads)}], {constants.CompileVars.SOCKET}); "
  457. f"processEvent({constants.CompileVars.SOCKET})}}",
  458. ).to(FunctionVar, EventChain)
  459. def format_query_params(router_data: dict[str, Any]) -> dict[str, str]:
  460. """Convert back query params name to python-friendly case.
  461. Args:
  462. router_data: the router_data dict containing the query params
  463. Returns:
  464. The reformatted query params
  465. """
  466. params = router_data[constants.RouteVar.QUERY]
  467. return {k.replace("-", "_"): v for k, v in params.items()}
  468. def format_state_name(state_name: str) -> str:
  469. """Format a state name, replacing dots with double underscore.
  470. This allows individual substates to be accessed independently as javascript vars
  471. without using dot notation.
  472. Args:
  473. state_name: The state name to format.
  474. Returns:
  475. The formatted state name.
  476. """
  477. return state_name.replace(".", "__")
  478. def format_ref(ref: str) -> str:
  479. """Format a ref.
  480. Args:
  481. ref: The ref to format.
  482. Returns:
  483. The formatted ref.
  484. """
  485. # Replace all non-word characters with underscores.
  486. clean_ref = re.sub(r"[^\w]+", "_", ref)
  487. return f"ref_{clean_ref}"
  488. def format_library_name(library_fullname: str):
  489. """Format the name of a library.
  490. Args:
  491. library_fullname: The fullname of the library.
  492. Returns:
  493. The name without the @version if it was part of the name
  494. """
  495. if library_fullname.startswith("https://"):
  496. return library_fullname
  497. lib, at, version = library_fullname.rpartition("@")
  498. if not lib:
  499. lib = at + version
  500. return lib
  501. def json_dumps(obj: Any, **kwargs) -> str:
  502. """Takes an object and returns a jsonified string.
  503. Args:
  504. obj: The object to be serialized.
  505. kwargs: Additional keyword arguments to pass to json.dumps.
  506. Returns:
  507. A string
  508. """
  509. from reflex.utils import serializers
  510. kwargs.setdefault("ensure_ascii", False)
  511. kwargs.setdefault("default", serializers.serialize)
  512. return json.dumps(obj, **kwargs)
  513. def collect_form_dict_names(form_dict: dict[str, Any]) -> dict[str, Any]:
  514. """Collapse keys with consecutive suffixes into a single list value.
  515. Separators dash and underscore are removed, unless this would overwrite an existing key.
  516. Args:
  517. form_dict: The dict to collapse.
  518. Returns:
  519. The collapsed dict.
  520. """
  521. ending_digit_regex = re.compile(r"^(.*?)[_-]?(\d+)$")
  522. collapsed = {}
  523. for k in sorted(form_dict):
  524. m = ending_digit_regex.match(k)
  525. if m:
  526. collapsed.setdefault(m.group(1), []).append(form_dict[k])
  527. # collapsing never overwrites valid data from the form_dict
  528. collapsed.update(form_dict)
  529. return collapsed
  530. def format_array_ref(refs: str, idx: Var | None) -> str:
  531. """Format a ref accessed by array.
  532. Args:
  533. refs : The ref array to access.
  534. idx : The index of the ref in the array.
  535. Returns:
  536. The formatted ref.
  537. """
  538. clean_ref = re.sub(r"[^\w]+", "_", refs)
  539. if idx is not None:
  540. return f"refs_{clean_ref}[{idx!s}]"
  541. return f"refs_{clean_ref}"
  542. def format_data_editor_column(col: str | dict):
  543. """Format a given column into the proper format.
  544. Args:
  545. col: The column.
  546. Raises:
  547. ValueError: invalid type provided for column.
  548. Returns:
  549. The formatted column.
  550. """
  551. from reflex.vars import Var
  552. if isinstance(col, str):
  553. return {"title": col, "id": col.lower(), "type": "str"}
  554. if isinstance(col, (dict,)):
  555. if "id" not in col:
  556. col["id"] = col["title"].lower()
  557. if "type" not in col:
  558. col["type"] = "str"
  559. if "overlayIcon" not in col:
  560. col["overlayIcon"] = None
  561. return col
  562. if isinstance(col, Var):
  563. return col
  564. raise ValueError(
  565. f"unexpected type ({(type(col).__name__)}: {col}) for column header in data_editor"
  566. )
  567. def format_data_editor_cell(cell: Any):
  568. """Format a given data into a renderable cell for data_editor.
  569. Args:
  570. cell: The data to format.
  571. Returns:
  572. The formatted cell.
  573. """
  574. from reflex.vars.base import Var
  575. return {
  576. "kind": Var(_js_expr="GridCellKind.Text"),
  577. "data": cell,
  578. }