reflex 0.3.2a1__py3-none-any.whl → 0.3.3a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of reflex might be problematic. Click here for more details.

Files changed (56) hide show
  1. reflex/.templates/jinja/web/pages/custom_component.js.jinja2 +20 -3
  2. reflex/.templates/web/next.config.js +1 -0
  3. reflex/.templates/web/utils/helpers/range.js +43 -0
  4. reflex/.templates/web/utils/state.js +10 -6
  5. reflex/__init__.py +312 -40
  6. reflex/__init__.pyi +477 -0
  7. reflex/compiler/compiler.py +3 -0
  8. reflex/components/__init__.py +138 -138
  9. reflex/components/component.py +29 -22
  10. reflex/components/datadisplay/__init__.py +3 -1
  11. reflex/components/datadisplay/code.py +388 -14
  12. reflex/components/datadisplay/code.pyi +1146 -10
  13. reflex/components/forms/button.py +3 -0
  14. reflex/components/forms/checkbox.py +3 -0
  15. reflex/components/forms/form.py +90 -27
  16. reflex/components/forms/input.py +3 -0
  17. reflex/components/forms/numberinput.py +3 -0
  18. reflex/components/forms/pininput.py +77 -21
  19. reflex/components/forms/radio.py +3 -0
  20. reflex/components/forms/rangeslider.py +3 -0
  21. reflex/components/forms/select.py +3 -0
  22. reflex/components/forms/slider.py +3 -0
  23. reflex/components/forms/switch.py +3 -0
  24. reflex/components/forms/textarea.py +3 -0
  25. reflex/components/layout/foreach.py +12 -6
  26. reflex/components/libs/chakra.py +2 -0
  27. reflex/components/libs/chakra.pyi +323 -24
  28. reflex/components/tags/iter_tag.py +18 -18
  29. reflex/components/tags/tag.py +3 -2
  30. reflex/components/typography/markdown.py +10 -0
  31. reflex/config.py +12 -0
  32. reflex/constants/installer.py +4 -4
  33. reflex/event.py +4 -0
  34. reflex/page.py +3 -4
  35. reflex/page.pyi +17 -0
  36. reflex/reflex.py +3 -0
  37. reflex/state.py +31 -12
  38. reflex/testing.py +1 -1
  39. reflex/utils/build.py +24 -19
  40. reflex/utils/console.py +5 -1
  41. reflex/utils/format.py +26 -9
  42. reflex/utils/prerequisites.py +27 -28
  43. reflex/utils/processes.py +5 -4
  44. reflex/vars.py +80 -12
  45. reflex/vars.pyi +7 -0
  46. {reflex-0.3.2a1.dist-info → reflex-0.3.3a1.dist-info}/METADATA +3 -2
  47. {reflex-0.3.2a1.dist-info → reflex-0.3.3a1.dist-info}/RECORD +50 -53
  48. reflex/.templates/web/.pytest_cache/.gitignore +0 -2
  49. reflex/.templates/web/.pytest_cache/CACHEDIR.TAG +0 -4
  50. reflex/.templates/web/.pytest_cache/README.md +0 -8
  51. reflex/.templates/web/.pytest_cache/v/cache/nodeids +0 -1
  52. reflex/.templates/web/.pytest_cache/v/cache/stepwise +0 -1
  53. reflex/.templates/web/styles/code/prism.js +0 -1015
  54. {reflex-0.3.2a1.dist-info → reflex-0.3.3a1.dist-info}/LICENSE +0 -0
  55. {reflex-0.3.2a1.dist-info → reflex-0.3.3a1.dist-info}/WHEEL +0 -0
  56. {reflex-0.3.2a1.dist-info → reflex-0.3.3a1.dist-info}/entry_points.txt +0 -0
reflex/testing.py CHANGED
@@ -677,7 +677,7 @@ class AppHarnessProd(AppHarness):
677
677
  frontend_server: Optional[Subdir404TCPServer] = None
678
678
 
679
679
  def _run_frontend(self):
680
- web_root = self.app_path / reflex.constants.Dirs.WEB / "_static"
680
+ web_root = self.app_path / reflex.constants.Dirs.WEB_STATIC
681
681
  error_page_map = {
682
682
  404: web_root / "404.html",
683
683
  }
reflex/utils/build.py CHANGED
@@ -35,21 +35,25 @@ def set_os_env(**kwargs):
35
35
  os.environ[key.upper()] = value
36
36
 
37
37
 
38
- def generate_sitemap_config(deploy_url: str):
38
+ def generate_sitemap_config(deploy_url: str, export=False):
39
39
  """Generate the sitemap config file.
40
40
 
41
41
  Args:
42
42
  deploy_url: The URL of the deployed app.
43
+ export: If the sitemap are generated for an export.
43
44
  """
44
45
  # Import here to avoid circular imports.
45
46
  from reflex.compiler import templates
46
47
 
47
- config = json.dumps(
48
- {
49
- "siteUrl": deploy_url,
50
- "generateRobotsTxt": True,
51
- }
52
- )
48
+ config = {
49
+ "siteUrl": deploy_url,
50
+ "generateRobotsTxt": True,
51
+ }
52
+
53
+ if export:
54
+ config["outDir"] = constants.Dirs.STATIC
55
+
56
+ config = json.dumps(config)
53
57
 
54
58
  with open(constants.Next.SITEMAP_CONFIG_FILE, "w") as f:
55
59
  f.write(templates.SITEMAP_CONFIG(config=config))
@@ -115,7 +119,7 @@ def _zip(
115
119
 
116
120
  with progress, zipfile.ZipFile(target, "w", zipfile.ZIP_DEFLATED) as zipf:
117
121
  for file in files_to_zip:
118
- console.debug(f"{target}: {file}")
122
+ console.debug(f"{target}: {file}", progress=progress)
119
123
  progress.advance(task)
120
124
  zipf.write(file, os.path.relpath(file, root_dir))
121
125
 
@@ -145,22 +149,23 @@ def export(
145
149
  command = "export"
146
150
 
147
151
  if frontend:
148
- # Generate a sitemap if a deploy URL is provided.
149
- if deploy_url is not None:
150
- generate_sitemap_config(deploy_url)
151
- command = "export-sitemap"
152
-
153
152
  checkpoints = [
154
153
  "Linting and checking ",
155
- "Compiled successfully",
154
+ "Creating an optimized production build",
156
155
  "Route (pages)",
156
+ "prerendered as static HTML",
157
157
  "Collecting page data",
158
- "automatically rendered as static HTML",
159
- 'Copying "static build" directory',
160
- 'Copying "public" directory',
161
158
  "Finalizing page optimization",
162
- "Export successful",
159
+ "Collecting build traces",
163
160
  ]
161
+
162
+ # Generate a sitemap if a deploy URL is provided.
163
+ if deploy_url is not None:
164
+ generate_sitemap_config(deploy_url, export=zip)
165
+ command = "export-sitemap"
166
+
167
+ checkpoints.extend(["Loading next-sitemap", "Generation completed"])
168
+
164
169
  # Start the subprocess with the progress bar.
165
170
  process = processes.new_process(
166
171
  [prerequisites.get_package_manager(), "run", command],
@@ -181,7 +186,7 @@ def export(
181
186
  target=os.path.join(
182
187
  zip_dest_dir, constants.ComponentName.FRONTEND.zip()
183
188
  ),
184
- root_dir=".web/_static",
189
+ root_dir=constants.Dirs.WEB_STATIC,
185
190
  files_to_exclude=files_to_exclude,
186
191
  exclude_venv_dirs=False,
187
192
  )
reflex/utils/console.py CHANGED
@@ -45,7 +45,11 @@ def debug(msg: str, **kwargs):
45
45
  kwargs: Keyword arguments to pass to the print function.
46
46
  """
47
47
  if _LOG_LEVEL <= LogLevel.DEBUG:
48
- print(f"[blue]Debug: {msg}[/blue]", **kwargs)
48
+ msg_ = f"[blue]Debug: {msg}[/blue]"
49
+ if progress := kwargs.pop("progress", None):
50
+ progress.console.print(msg_, **kwargs)
51
+ else:
52
+ print(msg_, **kwargs)
49
53
 
50
54
 
51
55
  def info(msg: str, **kwargs):
reflex/utils/format.py CHANGED
@@ -122,7 +122,7 @@ def to_snake_case(text: str) -> str:
122
122
  The snake case string.
123
123
  """
124
124
  s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", text)
125
- return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
125
+ return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower().replace("-", "_")
126
126
 
127
127
 
128
128
  def to_camel_case(text: str) -> str:
@@ -137,14 +137,9 @@ def to_camel_case(text: str) -> str:
137
137
  Returns:
138
138
  The camel case string.
139
139
  """
140
- if "_" not in text:
141
- return text
142
- camel = "".join(
143
- word.capitalize() if i > 0 else word.lower()
144
- for i, word in enumerate(text.lstrip("_").split("_"))
145
- )
146
- prefix = "_" if text.startswith("_") else ""
147
- return prefix + camel
140
+ words = re.split("[_-]", text)
141
+ # Capitalize the first letter of each word except the first one
142
+ return words[0] + "".join(x.capitalize() for x in words[1:])
148
143
 
149
144
 
150
145
  def to_title_case(text: str) -> str:
@@ -627,6 +622,28 @@ def unwrap_vars(value: str) -> str:
627
622
  )
628
623
 
629
624
 
625
+ def collect_form_dict_names(form_dict: dict[str, Any]) -> dict[str, Any]:
626
+ """Collapse keys with consecutive suffixes into a single list value.
627
+
628
+ Separators dash and underscore are removed, unless this would overwrite an existing key.
629
+
630
+ Args:
631
+ form_dict: The dict to collapse.
632
+
633
+ Returns:
634
+ The collapsed dict.
635
+ """
636
+ ending_digit_regex = re.compile(r"^(.*?)[_-]?(\d+)$")
637
+ collapsed = {}
638
+ for k in sorted(form_dict):
639
+ m = ending_digit_regex.match(k)
640
+ if m:
641
+ collapsed.setdefault(m.group(1), []).append(form_dict[k])
642
+ # collapsing never overwrites valid data from the form_dict
643
+ collapsed.update(form_dict)
644
+ return collapsed
645
+
646
+
630
647
  def format_data_editor_column(col: str | dict):
631
648
  """Format a given column into the proper format.
632
649
 
@@ -25,7 +25,7 @@ from redis.asyncio import Redis
25
25
 
26
26
  from reflex import constants, model
27
27
  from reflex.compiler import templates
28
- from reflex.config import Config, get_config
28
+ from reflex.config import get_config
29
29
  from reflex.utils import console, path_ops, processes
30
30
 
31
31
 
@@ -288,15 +288,7 @@ def initialize_web_directory():
288
288
 
289
289
  path_ops.mkdir(constants.Dirs.WEB_ASSETS)
290
290
 
291
- # update nextJS config based on rxConfig
292
- next_config_file = os.path.join(constants.Dirs.WEB, constants.Next.CONFIG_FILE)
293
-
294
- with open(next_config_file, "r") as file:
295
- next_config = file.read()
296
- next_config = update_next_config(next_config, get_config())
297
-
298
- with open(next_config_file, "w") as file:
299
- file.write(next_config)
291
+ update_next_config()
300
292
 
301
293
  # Initialize the reflex json file.
302
294
  init_reflex_json()
@@ -337,27 +329,34 @@ def init_reflex_json():
337
329
  path_ops.update_json_file(constants.Reflex.JSON, reflex_json)
338
330
 
339
331
 
340
- def update_next_config(next_config: str, config: Config) -> str:
341
- """Update Next.js config from Reflex config. Is its own function for testing.
332
+ def update_next_config(export=False):
333
+ """Update Next.js config from Reflex config.
342
334
 
343
335
  Args:
344
- next_config: Content of next.config.js.
345
- config: A reflex Config object.
346
-
347
- Returns:
348
- The next_config updated from config.
336
+ export: if the method run during reflex export.
349
337
  """
350
- next_config = re.sub(
351
- "compress: (true|false)",
352
- f'compress: {"true" if config.next_compression else "false"}',
353
- next_config,
354
- )
355
- next_config = re.sub(
356
- 'basePath: ".*?"',
357
- f'basePath: "{config.frontend_path or ""}"',
358
- next_config,
359
- )
360
- return next_config
338
+ next_config_file = os.path.join(constants.Dirs.WEB, constants.Next.CONFIG_FILE)
339
+
340
+ next_config = _update_next_config(get_config(), export=export)
341
+
342
+ with open(next_config_file, "w") as file:
343
+ file.write(next_config)
344
+ file.write("\n")
345
+
346
+
347
+ def _update_next_config(config, export=False):
348
+ next_config = {
349
+ "basePath": config.frontend_path or "",
350
+ "compress": config.next_compression,
351
+ "reactStrictMode": True,
352
+ "trailingSlash": True,
353
+ }
354
+ if export:
355
+ next_config["output"] = "export"
356
+ next_config["distDir"] = constants.Dirs.STATIC
357
+
358
+ next_config_json = re.sub(r'"([^"]+)"(?=:)', r"\1", json.dumps(next_config))
359
+ return f"module.exports = {next_config_json};"
361
360
 
362
361
 
363
362
  def remove_existing_bun_installation():
reflex/utils/processes.py CHANGED
@@ -193,12 +193,13 @@ def run_concurrently(*fns: Union[Callable, Tuple]) -> None:
193
193
  pass
194
194
 
195
195
 
196
- def stream_logs(message: str, process: subprocess.Popen):
196
+ def stream_logs(message: str, process: subprocess.Popen, progress=None):
197
197
  """Stream the logs for a process.
198
198
 
199
199
  Args:
200
200
  message: The message to display.
201
201
  process: The process.
202
+ progress: The ongoing progress bar if one is being used.
202
203
 
203
204
  Yields:
204
205
  The lines of the process output.
@@ -209,11 +210,11 @@ def stream_logs(message: str, process: subprocess.Popen):
209
210
  # Store the tail of the logs.
210
211
  logs = collections.deque(maxlen=512)
211
212
  with process:
212
- console.debug(message)
213
+ console.debug(message, progress=progress)
213
214
  if process.stdout is None:
214
215
  return
215
216
  for line in process.stdout:
216
- console.debug(line, end="")
217
+ console.debug(line, end="", progress=progress)
217
218
  logs.append(line)
218
219
  yield line
219
220
 
@@ -260,7 +261,7 @@ def show_progress(message: str, process: subprocess.Popen, checkpoints: List[str
260
261
  # Iterate over the process output.
261
262
  with console.progress() as progress:
262
263
  task = progress.add_task(f"{message}: ", total=len(checkpoints))
263
- for line in stream_logs(message, process):
264
+ for line in stream_logs(message, process, progress=progress):
264
265
  # Check for special strings and update the progress bar.
265
266
  for special_string in checkpoints:
266
267
  if special_string in line:
reflex/vars.py CHANGED
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
  import contextlib
5
5
  import dataclasses
6
6
  import dis
7
+ import inspect
7
8
  import json
8
9
  import random
9
10
  import string
@@ -1138,17 +1139,76 @@ class Var:
1138
1139
 
1139
1140
  Returns:
1140
1141
  A var representing foreach operation.
1142
+
1143
+ Raises:
1144
+ TypeError: If the var is not a list.
1141
1145
  """
1146
+ inner_types = get_args(self._var_type)
1147
+ if not inner_types:
1148
+ raise TypeError(
1149
+ f"Cannot foreach over non-sequence var {self._var_full_name} of type {self._var_type}."
1150
+ )
1142
1151
  arg = BaseVar(
1143
1152
  _var_name=get_unique_variable_name(),
1144
- _var_type=self._var_type,
1153
+ _var_type=inner_types[0],
1154
+ )
1155
+ index = BaseVar(
1156
+ _var_name=get_unique_variable_name(),
1157
+ _var_type=int,
1145
1158
  )
1159
+ fn_signature = inspect.signature(fn)
1160
+ fn_args = (arg, index)
1161
+ fn_ret = fn(*fn_args[: len(fn_signature.parameters)])
1146
1162
  return BaseVar(
1147
- _var_name=f"{self._var_full_name}.map(({arg._var_name}, i) => {fn(arg, key='i')})",
1163
+ _var_name=f"{self._var_full_name}.map(({arg._var_name}, {index._var_name}) => {fn_ret})",
1148
1164
  _var_type=self._var_type,
1149
1165
  _var_is_local=self._var_is_local,
1150
1166
  )
1151
1167
 
1168
+ @classmethod
1169
+ def range(
1170
+ cls,
1171
+ v1: Var | int = 0,
1172
+ v2: Var | int | None = None,
1173
+ step: Var | int | None = None,
1174
+ ) -> Var:
1175
+ """Return an iterator over indices from v1 to v2 (or 0 to v1).
1176
+
1177
+ Args:
1178
+ v1: The start of the range or end of range if v2 is not given.
1179
+ v2: The end of the range.
1180
+ step: The number of numbers between each item.
1181
+
1182
+ Returns:
1183
+ A var representing range operation.
1184
+
1185
+ Raises:
1186
+ TypeError: If the var is not an int.
1187
+ """
1188
+ if not isinstance(v1, Var):
1189
+ v1 = Var.create_safe(v1)
1190
+ if v1._var_type != int:
1191
+ raise TypeError(f"Cannot get range on non-int var {v1._var_full_name}.")
1192
+ if not isinstance(v2, Var):
1193
+ v2 = Var.create(v2)
1194
+ if v2 is None:
1195
+ v2 = Var.create_safe("undefined")
1196
+ elif v2._var_type != int:
1197
+ raise TypeError(f"Cannot get range on non-int var {v2._var_full_name}.")
1198
+
1199
+ if not isinstance(step, Var):
1200
+ step = Var.create(step)
1201
+ if step is None:
1202
+ step = Var.create_safe(1)
1203
+ elif step._var_type != int:
1204
+ raise TypeError(f"Cannot get range on non-int var {step._var_full_name}.")
1205
+
1206
+ return BaseVar(
1207
+ _var_name=f"Array.from(range({v1._var_full_name}, {v2._var_full_name}, {step._var_name}))",
1208
+ _var_type=list[int],
1209
+ _var_is_local=False,
1210
+ )
1211
+
1152
1212
  def to(self, type_: Type) -> Var:
1153
1213
  """Convert the type of the var.
1154
1214
 
@@ -1418,17 +1478,25 @@ class ComputedVar(Var, property):
1418
1478
  # is referencing an attribute on self
1419
1479
  self_is_top_of_stack = True
1420
1480
  continue
1421
- if self_is_top_of_stack and instruction.opname == "LOAD_ATTR":
1422
- # direct attribute access
1423
- d.add(instruction.argval)
1424
- elif self_is_top_of_stack and instruction.opname == "LOAD_METHOD":
1425
- # method call on self
1426
- d.update(
1427
- self._deps(
1428
- objclass=objclass,
1429
- obj=getattr(objclass, instruction.argval),
1481
+ if self_is_top_of_stack and instruction.opname in (
1482
+ "LOAD_ATTR",
1483
+ "LOAD_METHOD",
1484
+ ):
1485
+ try:
1486
+ ref_obj = getattr(objclass, instruction.argval)
1487
+ except Exception:
1488
+ ref_obj = None
1489
+ if callable(ref_obj):
1490
+ # recurse into callable attributes
1491
+ d.update(
1492
+ self._deps(
1493
+ objclass=objclass,
1494
+ obj=ref_obj,
1495
+ )
1430
1496
  )
1431
- )
1497
+ else:
1498
+ # normal attribute access
1499
+ d.add(instruction.argval)
1432
1500
  elif instruction.opname == "LOAD_CONST" and isinstance(
1433
1501
  instruction.argval, CodeType
1434
1502
  ):
reflex/vars.pyi CHANGED
@@ -85,6 +85,13 @@ class Var:
85
85
  def contains(self, other: Any) -> Var: ...
86
86
  def reverse(self) -> Var: ...
87
87
  def foreach(self, fn: Callable) -> Var: ...
88
+ @classmethod
89
+ def range(
90
+ cls,
91
+ v1: Var | int = 0,
92
+ v2: Var | int | None = None,
93
+ step: Var | int | None = None,
94
+ ) -> Var: ...
88
95
  def to(self, type_: Type) -> Var: ...
89
96
  @property
90
97
  def _var_full_name(self) -> str: ...
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: reflex
3
- Version: 0.3.2a1
3
+ Version: 0.3.3a1
4
4
  Summary: Web apps in pure Python.
5
5
  Home-page: https://reflex.dev
6
6
  License: Apache-2.0
@@ -36,7 +36,8 @@ Requires-Dist: sqlmodel (>=0.0.8,<0.0.9)
36
36
  Requires-Dist: starlette-admin (>=0.9.0,<0.10.0)
37
37
  Requires-Dist: tabulate (>=0.9.0,<0.10.0)
38
38
  Requires-Dist: typer (>=0.4.2,<1)
39
- Requires-Dist: uvicorn (>=0.20.0,<0.21.0)
39
+ Requires-Dist: uvicorn (>=0.20.0,<0.21.0) ; python_version < "3.12"
40
+ Requires-Dist: uvicorn (>=0.24.0,<0.25.0) ; python_version >= "3.12"
40
41
  Requires-Dist: watchdog (>=2.3.1,<3.0.0)
41
42
  Requires-Dist: watchfiles (>=0.19.0,<0.20.0)
42
43
  Requires-Dist: websockets (>=10.4,<11.0)