reflex 0.4.5a1__py3-none-any.whl → 0.4.6__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 (199) hide show
  1. reflex/.templates/jinja/custom_components/pyproject.toml.jinja2 +5 -15
  2. reflex/.templates/jinja/web/pages/index.js.jinja2 +4 -0
  3. reflex/.templates/jinja/web/pages/stateful_component.js.jinja2 +4 -0
  4. reflex/.templates/web/utils/state.js +3 -0
  5. reflex/__init__.py +12 -2
  6. reflex/__init__.pyi +4 -0
  7. reflex/app.py +23 -3
  8. reflex/base.py +16 -4
  9. reflex/compiler/compiler.py +1 -0
  10. reflex/compiler/utils.py +11 -2
  11. reflex/components/base/app_wrap.pyi +1 -1
  12. reflex/components/base/bare.py +3 -4
  13. reflex/components/base/body.pyi +1 -1
  14. reflex/components/base/document.pyi +1 -1
  15. reflex/components/base/fragment.pyi +1 -1
  16. reflex/components/base/head.pyi +1 -1
  17. reflex/components/base/link.pyi +1 -1
  18. reflex/components/base/meta.pyi +1 -1
  19. reflex/components/base/script.pyi +1 -1
  20. reflex/components/chakra/base.pyi +1 -1
  21. reflex/components/chakra/datadisplay/badge.pyi +1 -1
  22. reflex/components/chakra/datadisplay/code.pyi +1 -1
  23. reflex/components/chakra/datadisplay/divider.pyi +1 -1
  24. reflex/components/chakra/datadisplay/keyboard_key.pyi +1 -1
  25. reflex/components/chakra/datadisplay/list.pyi +1 -1
  26. reflex/components/chakra/datadisplay/stat.pyi +1 -1
  27. reflex/components/chakra/datadisplay/table.pyi +1 -1
  28. reflex/components/chakra/datadisplay/tag.pyi +1 -1
  29. reflex/components/chakra/disclosure/accordion.pyi +1 -1
  30. reflex/components/chakra/disclosure/tabs.pyi +1 -1
  31. reflex/components/chakra/disclosure/transition.pyi +1 -1
  32. reflex/components/chakra/disclosure/visuallyhidden.pyi +1 -1
  33. reflex/components/chakra/feedback/alert.pyi +1 -1
  34. reflex/components/chakra/feedback/circularprogress.pyi +1 -1
  35. reflex/components/chakra/feedback/progress.pyi +1 -1
  36. reflex/components/chakra/feedback/skeleton.pyi +1 -1
  37. reflex/components/chakra/feedback/spinner.pyi +1 -1
  38. reflex/components/chakra/forms/button.pyi +1 -1
  39. reflex/components/chakra/forms/checkbox.pyi +1 -1
  40. reflex/components/chakra/forms/colormodeswitch.pyi +1 -1
  41. reflex/components/chakra/forms/date_picker.pyi +1 -1
  42. reflex/components/chakra/forms/date_time_picker.pyi +1 -1
  43. reflex/components/chakra/forms/editable.pyi +1 -1
  44. reflex/components/chakra/forms/email.pyi +1 -1
  45. reflex/components/chakra/forms/form.pyi +1 -1
  46. reflex/components/chakra/forms/iconbutton.pyi +1 -1
  47. reflex/components/chakra/forms/input.pyi +1 -1
  48. reflex/components/chakra/forms/numberinput.pyi +1 -1
  49. reflex/components/chakra/forms/password.pyi +1 -1
  50. reflex/components/chakra/forms/pininput.pyi +1 -1
  51. reflex/components/chakra/forms/radio.pyi +1 -1
  52. reflex/components/chakra/forms/rangeslider.pyi +1 -1
  53. reflex/components/chakra/forms/select.pyi +1 -1
  54. reflex/components/chakra/forms/slider.pyi +1 -1
  55. reflex/components/chakra/forms/switch.pyi +1 -1
  56. reflex/components/chakra/forms/textarea.pyi +1 -1
  57. reflex/components/chakra/forms/time_picker.pyi +1 -1
  58. reflex/components/chakra/layout/aspect_ratio.pyi +1 -1
  59. reflex/components/chakra/layout/box.pyi +1 -1
  60. reflex/components/chakra/layout/card.pyi +1 -1
  61. reflex/components/chakra/layout/center.pyi +1 -1
  62. reflex/components/chakra/layout/container.pyi +1 -1
  63. reflex/components/chakra/layout/flex.pyi +1 -1
  64. reflex/components/chakra/layout/grid.pyi +1 -1
  65. reflex/components/chakra/layout/spacer.pyi +1 -1
  66. reflex/components/chakra/layout/stack.pyi +1 -1
  67. reflex/components/chakra/layout/wrap.pyi +1 -1
  68. reflex/components/chakra/media/avatar.pyi +1 -1
  69. reflex/components/chakra/media/icon.pyi +1 -1
  70. reflex/components/chakra/media/image.pyi +1 -1
  71. reflex/components/chakra/navigation/breadcrumb.pyi +1 -1
  72. reflex/components/chakra/navigation/link.pyi +1 -1
  73. reflex/components/chakra/navigation/linkoverlay.pyi +1 -1
  74. reflex/components/chakra/navigation/stepper.pyi +1 -1
  75. reflex/components/chakra/overlay/alertdialog.pyi +1 -1
  76. reflex/components/chakra/overlay/drawer.pyi +1 -1
  77. reflex/components/chakra/overlay/menu.pyi +1 -1
  78. reflex/components/chakra/overlay/modal.pyi +1 -1
  79. reflex/components/chakra/overlay/popover.pyi +1 -1
  80. reflex/components/chakra/overlay/tooltip.pyi +1 -1
  81. reflex/components/chakra/typography/heading.pyi +1 -1
  82. reflex/components/chakra/typography/highlight.pyi +1 -1
  83. reflex/components/chakra/typography/span.pyi +1 -1
  84. reflex/components/chakra/typography/text.pyi +1 -1
  85. reflex/components/component.py +82 -30
  86. reflex/components/core/banner.py +1 -2
  87. reflex/components/core/banner.pyi +1 -2
  88. reflex/components/core/client_side_routing.pyi +1 -1
  89. reflex/components/core/cond.py +1 -1
  90. reflex/components/core/debounce.pyi +1 -1
  91. reflex/components/core/html.pyi +1 -1
  92. reflex/components/core/responsive.py +1 -1
  93. reflex/components/core/upload.pyi +1 -1
  94. reflex/components/datadisplay/code.py +17 -9
  95. reflex/components/datadisplay/code.pyi +3 -1
  96. reflex/components/datadisplay/dataeditor.pyi +1 -1
  97. reflex/components/el/element.pyi +1 -1
  98. reflex/components/el/elements/base.pyi +1 -1
  99. reflex/components/el/elements/forms.py +78 -1
  100. reflex/components/el/elements/forms.pyi +10 -2
  101. reflex/components/el/elements/inline.pyi +1 -1
  102. reflex/components/el/elements/media.pyi +1 -1
  103. reflex/components/el/elements/metadata.pyi +1 -1
  104. reflex/components/el/elements/other.pyi +1 -1
  105. reflex/components/el/elements/scripts.pyi +1 -1
  106. reflex/components/el/elements/sectioning.pyi +1 -1
  107. reflex/components/el/elements/tables.pyi +1 -1
  108. reflex/components/el/elements/typography.pyi +1 -1
  109. reflex/components/gridjs/datatable.pyi +1 -1
  110. reflex/components/lucide/icon.py +275 -115
  111. reflex/components/lucide/icon.pyi +259 -114
  112. reflex/components/markdown/markdown.pyi +1 -1
  113. reflex/components/moment/moment.pyi +1 -1
  114. reflex/components/next/base.pyi +1 -1
  115. reflex/components/next/image.pyi +1 -1
  116. reflex/components/next/link.pyi +1 -1
  117. reflex/components/next/video.pyi +1 -1
  118. reflex/components/plotly/plotly.pyi +1 -1
  119. reflex/components/radix/primitives/accordion.pyi +1 -1
  120. reflex/components/radix/primitives/base.pyi +1 -1
  121. reflex/components/radix/primitives/drawer.pyi +1 -1
  122. reflex/components/radix/primitives/form.pyi +1 -1
  123. reflex/components/radix/primitives/progress.pyi +1 -1
  124. reflex/components/radix/primitives/slider.pyi +1 -1
  125. reflex/components/radix/themes/base.pyi +1 -1
  126. reflex/components/radix/themes/color_mode.pyi +1 -1
  127. reflex/components/radix/themes/components/alert_dialog.pyi +1 -1
  128. reflex/components/radix/themes/components/aspect_ratio.pyi +1 -1
  129. reflex/components/radix/themes/components/avatar.pyi +1 -1
  130. reflex/components/radix/themes/components/badge.pyi +1 -1
  131. reflex/components/radix/themes/components/button.pyi +1 -1
  132. reflex/components/radix/themes/components/callout.pyi +1 -1
  133. reflex/components/radix/themes/components/card.pyi +1 -1
  134. reflex/components/radix/themes/components/checkbox.pyi +1 -1
  135. reflex/components/radix/themes/components/context_menu.pyi +1 -1
  136. reflex/components/radix/themes/components/dialog.pyi +1 -1
  137. reflex/components/radix/themes/components/dropdown_menu.pyi +1 -1
  138. reflex/components/radix/themes/components/hover_card.pyi +1 -1
  139. reflex/components/radix/themes/components/icon_button.pyi +1 -1
  140. reflex/components/radix/themes/components/inset.pyi +1 -1
  141. reflex/components/radix/themes/components/popover.pyi +1 -1
  142. reflex/components/radix/themes/components/radio_group.pyi +1 -1
  143. reflex/components/radix/themes/components/scroll_area.pyi +1 -1
  144. reflex/components/radix/themes/components/select.py +4 -1
  145. reflex/components/radix/themes/components/select.pyi +5 -1
  146. reflex/components/radix/themes/components/separator.pyi +1 -1
  147. reflex/components/radix/themes/components/slider.pyi +1 -1
  148. reflex/components/radix/themes/components/switch.pyi +1 -1
  149. reflex/components/radix/themes/components/table.pyi +1 -1
  150. reflex/components/radix/themes/components/tabs.pyi +1 -1
  151. reflex/components/radix/themes/components/text_area.pyi +5 -1
  152. reflex/components/radix/themes/components/text_field.pyi +1 -1
  153. reflex/components/radix/themes/components/tooltip.pyi +1 -1
  154. reflex/components/radix/themes/layout/__init__.py +5 -4
  155. reflex/components/radix/themes/layout/base.pyi +1 -1
  156. reflex/components/radix/themes/layout/box.pyi +1 -1
  157. reflex/components/radix/themes/layout/center.pyi +1 -1
  158. reflex/components/radix/themes/layout/container.pyi +1 -1
  159. reflex/components/radix/themes/layout/flex.pyi +1 -1
  160. reflex/components/radix/themes/layout/grid.pyi +1 -1
  161. reflex/components/radix/themes/layout/list.py +21 -13
  162. reflex/components/radix/themes/layout/list.pyi +139 -481
  163. reflex/components/radix/themes/layout/section.pyi +1 -1
  164. reflex/components/radix/themes/layout/spacer.pyi +1 -1
  165. reflex/components/radix/themes/layout/stack.pyi +1 -1
  166. reflex/components/radix/themes/typography/blockquote.pyi +1 -1
  167. reflex/components/radix/themes/typography/code.pyi +1 -1
  168. reflex/components/radix/themes/typography/heading.pyi +1 -1
  169. reflex/components/radix/themes/typography/link.pyi +1 -1
  170. reflex/components/radix/themes/typography/text.pyi +1 -1
  171. reflex/components/react_player/audio.pyi +1 -1
  172. reflex/components/react_player/react_player.pyi +1 -1
  173. reflex/components/react_player/video.pyi +1 -1
  174. reflex/components/recharts/cartesian.pyi +1 -1
  175. reflex/components/recharts/charts.pyi +1 -1
  176. reflex/components/recharts/general.pyi +1 -1
  177. reflex/components/recharts/polar.pyi +1 -1
  178. reflex/components/recharts/recharts.pyi +1 -1
  179. reflex/components/suneditor/editor.pyi +1 -1
  180. reflex/config.py +12 -2
  181. reflex/custom_components/custom_components.py +305 -21
  182. reflex/event.py +36 -46
  183. reflex/reflex.py +19 -13
  184. reflex/state.py +184 -39
  185. reflex/testing.py +15 -11
  186. reflex/utils/console.py +15 -7
  187. reflex/utils/exec.py +9 -0
  188. reflex/utils/prerequisites.py +12 -1
  189. reflex/utils/processes.py +8 -25
  190. reflex/utils/pyi_generator.py +842 -0
  191. reflex/utils/telemetry.py +18 -1
  192. reflex/utils/types.py +14 -2
  193. reflex/vars.py +33 -3
  194. {reflex-0.4.5a1.dist-info → reflex-0.4.6.dist-info}/METADATA +31 -29
  195. {reflex-0.4.5a1.dist-info → reflex-0.4.6.dist-info}/RECORD +198 -198
  196. reflex/page.pyi +0 -17
  197. {reflex-0.4.5a1.dist-info → reflex-0.4.6.dist-info}/LICENSE +0 -0
  198. {reflex-0.4.5a1.dist-info → reflex-0.4.6.dist-info}/WHEEL +0 -0
  199. {reflex-0.4.5a1.dist-info → reflex-0.4.6.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,842 @@
1
+ """The pyi generator module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ import contextlib
7
+ import importlib
8
+ import inspect
9
+ import logging
10
+ import re
11
+ import subprocess
12
+ import textwrap
13
+ import typing
14
+ from inspect import getfullargspec
15
+ from multiprocessing import Pool, cpu_count
16
+ from pathlib import Path
17
+ from types import ModuleType, SimpleNamespace
18
+ from typing import Any, Callable, Iterable, Type, get_args
19
+
20
+ try:
21
+ import black
22
+ import black.mode
23
+ except ImportError:
24
+ black = None
25
+
26
+ from reflex.components.component import Component
27
+ from reflex.utils import types as rx_types
28
+ from reflex.vars import Var
29
+
30
+ logger = logging.getLogger("pyi_generator")
31
+
32
+ INIT_FILE = Path("reflex/__init__.pyi").resolve()
33
+ PWD = Path(".").resolve()
34
+
35
+ EXCLUDED_FILES = [
36
+ "__init__.py",
37
+ "component.py",
38
+ "bare.py",
39
+ "foreach.py",
40
+ "cond.py",
41
+ "match.py",
42
+ "multiselect.py",
43
+ "literals.py",
44
+ ]
45
+
46
+ # These props exist on the base component, but should not be exposed in create methods.
47
+ EXCLUDED_PROPS = [
48
+ "alias",
49
+ "children",
50
+ "event_triggers",
51
+ "library",
52
+ "lib_dependencies",
53
+ "tag",
54
+ "is_default",
55
+ "special_props",
56
+ "_invalid_children",
57
+ "_memoization_mode",
58
+ "_rename_props",
59
+ "_valid_children",
60
+ "_valid_parents",
61
+ "State",
62
+ ]
63
+
64
+ DEFAULT_TYPING_IMPORTS = {
65
+ "overload",
66
+ "Any",
67
+ "Dict",
68
+ # "List",
69
+ "Literal",
70
+ "Optional",
71
+ "Union",
72
+ }
73
+
74
+
75
+ def _walk_files(path):
76
+ """Walk all files in a path.
77
+ This can be replaced with Path.walk() in python3.12.
78
+
79
+ Args:
80
+ path: The path to walk.
81
+
82
+ Yields:
83
+ The next file in the path.
84
+ """
85
+ for p in Path(path).iterdir():
86
+ if p.is_dir():
87
+ yield from _walk_files(p)
88
+ continue
89
+ yield p.resolve()
90
+
91
+
92
+ def _relative_to_pwd(path: Path) -> Path:
93
+ """Get the relative path of a path to the current working directory.
94
+
95
+ Args:
96
+ path: The path to get the relative path for.
97
+
98
+ Returns:
99
+ The relative path.
100
+ """
101
+ if path.is_absolute():
102
+ return path.relative_to(PWD)
103
+ return path
104
+
105
+
106
+ def _get_type_hint(value, type_hint_globals, is_optional=True) -> str:
107
+ """Resolve the type hint for value.
108
+
109
+ Args:
110
+ value: The type annotation as a str or actual types/aliases.
111
+ type_hint_globals: The globals to use to resolving a type hint str.
112
+ is_optional: Whether the type hint should be wrapped in Optional.
113
+
114
+ Returns:
115
+ The resolved type hint as a str.
116
+ """
117
+ res = ""
118
+ args = get_args(value)
119
+ if args:
120
+ inner_container_type_args = (
121
+ [repr(arg) for arg in args]
122
+ if rx_types.is_literal(value)
123
+ else [
124
+ _get_type_hint(arg, type_hint_globals, is_optional=False)
125
+ for arg in args
126
+ if arg is not type(None)
127
+ ]
128
+ )
129
+ res = f"{value.__name__}[{', '.join(inner_container_type_args)}]"
130
+
131
+ if value.__name__ == "Var":
132
+ # For Var types, Union with the inner args so they can be passed directly.
133
+ types = [res] + [
134
+ _get_type_hint(arg, type_hint_globals, is_optional=False)
135
+ for arg in args
136
+ if arg is not type(None)
137
+ ]
138
+ if len(types) > 1:
139
+ res = ", ".join(types)
140
+ res = f"Union[{res}]"
141
+ elif isinstance(value, str):
142
+ ev = eval(value, type_hint_globals)
143
+ res = (
144
+ _get_type_hint(ev, type_hint_globals, is_optional=False)
145
+ if ev.__name__ == "Var"
146
+ else value
147
+ )
148
+ else:
149
+ res = value.__name__
150
+ if is_optional and not res.startswith("Optional"):
151
+ res = f"Optional[{res}]"
152
+ return res
153
+
154
+
155
+ def _generate_imports(typing_imports: Iterable[str]) -> list[ast.ImportFrom]:
156
+ """Generate the import statements for the stub file.
157
+
158
+ Args:
159
+ typing_imports: The typing imports to include.
160
+
161
+ Returns:
162
+ The list of import statements.
163
+ """
164
+ return [
165
+ ast.ImportFrom(
166
+ module="typing",
167
+ names=[ast.alias(name=imp) for imp in sorted(typing_imports)],
168
+ ),
169
+ *ast.parse( # type: ignore
170
+ textwrap.dedent(
171
+ """
172
+ from reflex.vars import Var, BaseVar, ComputedVar
173
+ from reflex.event import EventChain, EventHandler, EventSpec
174
+ from reflex.style import Style"""
175
+ )
176
+ ).body,
177
+ # *[
178
+ # ast.ImportFrom(module=name, names=[ast.alias(name=val) for val in values])
179
+ # for name, values in EXTRA_IMPORTS.items()
180
+ # ],
181
+ ]
182
+
183
+
184
+ def _generate_docstrings(clzs: list[Type[Component]], props: list[str]) -> str:
185
+ """Generate the docstrings for the create method.
186
+
187
+ Args:
188
+ clzs: The classes to generate docstrings for.
189
+ props: The props to generate docstrings for.
190
+
191
+ Returns:
192
+ The docstring for the create method.
193
+ """
194
+ props_comments = {}
195
+ comments = []
196
+ for clz in clzs:
197
+ for line in inspect.getsource(clz).splitlines():
198
+ reached_functions = re.search("def ", line)
199
+ if reached_functions:
200
+ # We've reached the functions, so stop.
201
+ break
202
+
203
+ # Get comments for prop
204
+ if line.strip().startswith("#"):
205
+ comments.append(line)
206
+ continue
207
+
208
+ # Check if this line has a prop.
209
+ match = re.search("\\w+:", line)
210
+ if match is None:
211
+ # This line doesn't have a var, so continue.
212
+ continue
213
+
214
+ # Get the prop.
215
+ prop = match.group(0).strip(":")
216
+ if prop in props:
217
+ if not comments: # do not include undocumented props
218
+ continue
219
+ props_comments[prop] = [
220
+ comment.strip().strip("#") for comment in comments
221
+ ]
222
+ comments.clear()
223
+ clz = clzs[0]
224
+ new_docstring = []
225
+ for line in (clz.create.__doc__ or "").splitlines():
226
+ if "**" in line:
227
+ indent = line.split("**")[0]
228
+ for nline in [
229
+ f"{indent}{n}:{' '.join(c)}" for n, c in props_comments.items()
230
+ ]:
231
+ new_docstring.append(nline)
232
+ new_docstring.append(line)
233
+ return "\n".join(new_docstring)
234
+
235
+
236
+ def _extract_func_kwargs_as_ast_nodes(
237
+ func: Callable,
238
+ type_hint_globals: dict[str, Any],
239
+ ) -> list[tuple[ast.arg, ast.Constant | None]]:
240
+ """Get the kwargs already defined on the function.
241
+
242
+ Args:
243
+ func: The function to extract kwargs from.
244
+ type_hint_globals: The globals to use to resolving a type hint str.
245
+
246
+ Returns:
247
+ The list of kwargs as ast arg nodes.
248
+ """
249
+ spec = getfullargspec(func)
250
+ kwargs = []
251
+
252
+ for kwarg in spec.kwonlyargs:
253
+ arg = ast.arg(arg=kwarg)
254
+ if kwarg in spec.annotations:
255
+ arg.annotation = ast.Name(
256
+ id=_get_type_hint(spec.annotations[kwarg], type_hint_globals)
257
+ )
258
+ default = None
259
+ if spec.kwonlydefaults is not None and kwarg in spec.kwonlydefaults:
260
+ default = ast.Constant(value=spec.kwonlydefaults[kwarg])
261
+ kwargs.append((arg, default))
262
+ return kwargs
263
+
264
+
265
+ def _extract_class_props_as_ast_nodes(
266
+ func: Callable,
267
+ clzs: list[Type],
268
+ type_hint_globals: dict[str, Any],
269
+ extract_real_default: bool = False,
270
+ ) -> list[tuple[ast.arg, ast.Constant | None]]:
271
+ """Get the props defined on the class and all parents.
272
+
273
+ Args:
274
+ func: The function that kwargs will be added to.
275
+ clzs: The classes to extract props from.
276
+ type_hint_globals: The globals to use to resolving a type hint str.
277
+ extract_real_default: Whether to extract the real default value from the
278
+ pydantic field definition.
279
+
280
+ Returns:
281
+ The list of props as ast arg nodes
282
+ """
283
+ spec = getfullargspec(func)
284
+ all_props = []
285
+ kwargs = []
286
+ for target_class in clzs:
287
+ # Import from the target class to ensure type hints are resolvable.
288
+ exec(f"from {target_class.__module__} import *", type_hint_globals)
289
+ for name, value in target_class.__annotations__.items():
290
+ if (
291
+ name in spec.kwonlyargs
292
+ or name in EXCLUDED_PROPS
293
+ or name in all_props
294
+ or (isinstance(value, str) and "ClassVar" in value)
295
+ ):
296
+ continue
297
+ all_props.append(name)
298
+
299
+ default = None
300
+ if extract_real_default:
301
+ # TODO: This is not currently working since the default is not type compatible
302
+ # with the annotation in some cases.
303
+ with contextlib.suppress(AttributeError, KeyError):
304
+ # Try to get default from pydantic field definition.
305
+ default = target_class.__fields__[name].default
306
+ if isinstance(default, Var):
307
+ default = default._decode() # type: ignore
308
+
309
+ kwargs.append(
310
+ (
311
+ ast.arg(
312
+ arg=name,
313
+ annotation=ast.Name(
314
+ id=_get_type_hint(value, type_hint_globals)
315
+ ),
316
+ ),
317
+ ast.Constant(value=default),
318
+ )
319
+ )
320
+ return kwargs
321
+
322
+
323
+ def _get_parent_imports(func):
324
+ _imports = {"reflex.vars": ["Var"]}
325
+ for type_hint in inspect.get_annotations(func).values():
326
+ try:
327
+ match = re.match(r"\w+\[([\w\d]+)\]", type_hint)
328
+ except TypeError:
329
+ continue
330
+ if match:
331
+ type_hint = match.group(1)
332
+ if type_hint in importlib.import_module(func.__module__).__dir__():
333
+ _imports.setdefault(func.__module__, []).append(type_hint)
334
+ return _imports
335
+
336
+
337
+ def _generate_component_create_functiondef(
338
+ node: ast.FunctionDef | None,
339
+ clz: type[Component] | type[SimpleNamespace],
340
+ type_hint_globals: dict[str, Any],
341
+ ) -> ast.FunctionDef:
342
+ """Generate the create function definition for a Component.
343
+
344
+ Args:
345
+ node: The existing create functiondef node from the ast
346
+ clz: The Component class to generate the create functiondef for.
347
+ type_hint_globals: The globals to use to resolving a type hint str.
348
+
349
+ Returns:
350
+ The create functiondef node for the ast.
351
+
352
+ Raises:
353
+ TypeError: If clz is not a subclass of Component.
354
+ """
355
+ if not issubclass(clz, Component):
356
+ raise TypeError(f"clz must be a subclass of Component, not {clz!r}")
357
+
358
+ # add the imports needed by get_type_hint later
359
+ type_hint_globals.update(
360
+ {name: getattr(typing, name) for name in DEFAULT_TYPING_IMPORTS}
361
+ )
362
+
363
+ if clz.__module__ != clz.create.__module__:
364
+ _imports = _get_parent_imports(clz.create)
365
+ for name, values in _imports.items():
366
+ exec(f"from {name} import {','.join(values)}", type_hint_globals)
367
+
368
+ kwargs = _extract_func_kwargs_as_ast_nodes(clz.create, type_hint_globals)
369
+
370
+ # kwargs associated with props defined in the class and its parents
371
+ all_classes = [c for c in clz.__mro__ if issubclass(c, Component)]
372
+ prop_kwargs = _extract_class_props_as_ast_nodes(
373
+ clz.create, all_classes, type_hint_globals
374
+ )
375
+ all_props = [arg[0].arg for arg in prop_kwargs]
376
+ kwargs.extend(prop_kwargs)
377
+
378
+ # event handler kwargs
379
+ kwargs.extend(
380
+ (
381
+ ast.arg(
382
+ arg=trigger,
383
+ annotation=ast.Name(
384
+ id="Optional[Union[EventHandler, EventSpec, list, function, BaseVar]]"
385
+ ),
386
+ ),
387
+ ast.Constant(value=None),
388
+ )
389
+ for trigger in sorted(clz().get_event_triggers().keys())
390
+ )
391
+ logger.debug(f"Generated {clz.__name__}.create method with {len(kwargs)} kwargs")
392
+ create_args = ast.arguments(
393
+ args=[ast.arg(arg="cls")],
394
+ posonlyargs=[],
395
+ vararg=ast.arg(arg="children"),
396
+ kwonlyargs=[arg[0] for arg in kwargs],
397
+ kw_defaults=[arg[1] for arg in kwargs],
398
+ kwarg=ast.arg(arg="props"),
399
+ defaults=[],
400
+ )
401
+ definition = ast.FunctionDef(
402
+ name="create",
403
+ args=create_args,
404
+ body=[
405
+ ast.Expr(
406
+ value=ast.Constant(value=_generate_docstrings(all_classes, all_props))
407
+ ),
408
+ ast.Expr(
409
+ value=ast.Ellipsis(),
410
+ ),
411
+ ],
412
+ decorator_list=[
413
+ ast.Name(id="overload"),
414
+ *(
415
+ node.decorator_list
416
+ if node is not None
417
+ else [ast.Name(id="classmethod")]
418
+ ),
419
+ ],
420
+ lineno=node.lineno if node is not None else None,
421
+ returns=ast.Constant(value=clz.__name__),
422
+ )
423
+ return definition
424
+
425
+
426
+ def _generate_namespace_call_functiondef(
427
+ clz_name: str,
428
+ classes: dict[str, type[Component] | type[SimpleNamespace]],
429
+ type_hint_globals: dict[str, Any],
430
+ ) -> ast.FunctionDef | None:
431
+ """Generate the __call__ function definition for a SimpleNamespace.
432
+
433
+ Args:
434
+ clz_name: The name of the SimpleNamespace class to generate the __call__ functiondef for.
435
+ classes: Map name to actual class definition.
436
+ type_hint_globals: The globals to use to resolving a type hint str.
437
+
438
+ Returns:
439
+ The create functiondef node for the ast.
440
+ """
441
+ # add the imports needed by get_type_hint later
442
+ type_hint_globals.update(
443
+ {name: getattr(typing, name) for name in DEFAULT_TYPING_IMPORTS}
444
+ )
445
+
446
+ clz = classes[clz_name]
447
+
448
+ # Determine which class is wrapped by the namespace __call__ method
449
+ component_clz = clz.__call__.__self__
450
+
451
+ # Only generate for create functions
452
+ if clz.__call__.__func__.__name__ != "create":
453
+ return None
454
+
455
+ definition = _generate_component_create_functiondef(
456
+ node=None,
457
+ clz=component_clz, # type: ignore
458
+ type_hint_globals=type_hint_globals,
459
+ )
460
+ definition.name = "__call__"
461
+
462
+ # Turn the definition into a staticmethod
463
+ del definition.args.args[0] # remove `cls` arg
464
+ definition.decorator_list = [ast.Name(id="staticmethod")]
465
+
466
+ return definition
467
+
468
+
469
+ class StubGenerator(ast.NodeTransformer):
470
+ """A node transformer that will generate the stubs for a given module."""
471
+
472
+ def __init__(
473
+ self, module: ModuleType, classes: dict[str, Type[Component | SimpleNamespace]]
474
+ ):
475
+ """Initialize the stub generator.
476
+
477
+ Args:
478
+ module: The actual module object module to generate stubs for.
479
+ classes: The actual Component class objects to generate stubs for.
480
+ """
481
+ super().__init__()
482
+ # Dict mapping class name to actual class object.
483
+ self.classes = classes
484
+ # Track the last class node that was visited.
485
+ self.current_class = None
486
+ # These imports will be included in the AST of stub files.
487
+ self.typing_imports = DEFAULT_TYPING_IMPORTS
488
+ # Whether those typing imports have been inserted yet.
489
+ self.inserted_imports = False
490
+ # Collected import statements from the module.
491
+ self.import_statements: list[str] = []
492
+ # This dict is used when evaluating type hints.
493
+ self.type_hint_globals = module.__dict__.copy()
494
+
495
+ @staticmethod
496
+ def _remove_docstring(
497
+ node: ast.Module | ast.ClassDef | ast.FunctionDef,
498
+ ) -> ast.Module | ast.ClassDef | ast.FunctionDef:
499
+ """Removes any docstring in place.
500
+
501
+ Args:
502
+ node: The node to remove the docstring from.
503
+
504
+ Returns:
505
+ The modified node.
506
+ """
507
+ if (
508
+ node.body
509
+ and isinstance(node.body[0], ast.Expr)
510
+ and isinstance(node.body[0].value, ast.Constant)
511
+ ):
512
+ node.body.pop(0)
513
+ return node
514
+
515
+ def _current_class_is_component(self) -> bool:
516
+ """Check if the current class is a Component.
517
+
518
+ Returns:
519
+ Whether the current class is a Component.
520
+ """
521
+ return (
522
+ self.current_class is not None
523
+ and self.current_class in self.classes
524
+ and issubclass(self.classes[self.current_class], Component)
525
+ )
526
+
527
+ def visit_Module(self, node: ast.Module) -> ast.Module:
528
+ """Visit a Module node and remove docstring from body.
529
+
530
+ Args:
531
+ node: The Module node to visit.
532
+
533
+ Returns:
534
+ The modified Module node.
535
+ """
536
+ self.generic_visit(node)
537
+ return self._remove_docstring(node) # type: ignore
538
+
539
+ def visit_Import(
540
+ self, node: ast.Import | ast.ImportFrom
541
+ ) -> ast.Import | ast.ImportFrom | list[ast.Import | ast.ImportFrom]:
542
+ """Collect import statements from the module.
543
+
544
+ If this is the first import statement, insert the typing imports before it.
545
+
546
+ Args:
547
+ node: The import node to visit.
548
+
549
+ Returns:
550
+ The modified import node(s).
551
+ """
552
+ self.import_statements.append(ast.unparse(node))
553
+ if not self.inserted_imports:
554
+ self.inserted_imports = True
555
+ return _generate_imports(self.typing_imports) + [node]
556
+ return node
557
+
558
+ def visit_ImportFrom(
559
+ self, node: ast.ImportFrom
560
+ ) -> ast.Import | ast.ImportFrom | list[ast.Import | ast.ImportFrom] | None:
561
+ """Visit an ImportFrom node.
562
+
563
+ Remove any `from __future__ import *` statements, and hand off to visit_Import.
564
+
565
+ Args:
566
+ node: The ImportFrom node to visit.
567
+
568
+ Returns:
569
+ The modified ImportFrom node.
570
+ """
571
+ if node.module == "__future__":
572
+ return None # ignore __future__ imports
573
+ return self.visit_Import(node)
574
+
575
+ def visit_ClassDef(self, node: ast.ClassDef) -> ast.ClassDef:
576
+ """Visit a ClassDef node.
577
+
578
+ Remove all assignments in the class body, and add a create functiondef
579
+ if one does not exist.
580
+
581
+ Args:
582
+ node: The ClassDef node to visit.
583
+
584
+ Returns:
585
+ The modified ClassDef node.
586
+ """
587
+ exec("\n".join(self.import_statements), self.type_hint_globals)
588
+ self.current_class = node.name
589
+ self._remove_docstring(node)
590
+
591
+ # Define `__call__` as a real function so the docstring appears in the stub.
592
+ call_definition = None
593
+ for child in node.body[:]:
594
+ found_call = False
595
+ if isinstance(child, ast.Assign):
596
+ for target in child.targets[:]:
597
+ if isinstance(target, ast.Name) and target.id == "__call__":
598
+ child.targets.remove(target)
599
+ found_call = True
600
+ if not found_call:
601
+ continue
602
+ if not child.targets[:]:
603
+ node.body.remove(child)
604
+ call_definition = _generate_namespace_call_functiondef(
605
+ self.current_class,
606
+ self.classes,
607
+ type_hint_globals=self.type_hint_globals,
608
+ )
609
+ break
610
+
611
+ self.generic_visit(node) # Visit child nodes.
612
+
613
+ if (
614
+ not any(
615
+ isinstance(child, ast.FunctionDef) and child.name == "create"
616
+ for child in node.body
617
+ )
618
+ and self._current_class_is_component()
619
+ ):
620
+ # Add a new .create FunctionDef since one does not exist.
621
+ node.body.append(
622
+ _generate_component_create_functiondef(
623
+ node=None,
624
+ clz=self.classes[self.current_class],
625
+ type_hint_globals=self.type_hint_globals,
626
+ )
627
+ )
628
+ if call_definition is not None:
629
+ node.body.append(call_definition)
630
+ if not node.body:
631
+ # We should never return an empty body.
632
+ node.body.append(ast.Expr(value=ast.Ellipsis()))
633
+ self.current_class = None
634
+ return node
635
+
636
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> Any:
637
+ """Visit a FunctionDef node.
638
+
639
+ Special handling for `.create` functions to add type hints for all props
640
+ defined on the component class.
641
+
642
+ Remove all private functions and blank out the function body of the
643
+ remaining public functions.
644
+
645
+ Args:
646
+ node: The FunctionDef node to visit.
647
+
648
+ Returns:
649
+ The modified FunctionDef node (or None).
650
+ """
651
+ if node.name == "create" and self.current_class in self.classes:
652
+ node = _generate_component_create_functiondef(
653
+ node, self.classes[self.current_class], self.type_hint_globals
654
+ )
655
+ else:
656
+ if node.name.startswith("_") and node.name != "__call__":
657
+ return None # remove private methods
658
+
659
+ if node.body[-1] != ast.Expr(value=ast.Ellipsis()):
660
+ # Blank out the function body for public functions.
661
+ node.body = [ast.Expr(value=ast.Ellipsis())]
662
+ return node
663
+
664
+ def visit_Assign(self, node: ast.Assign) -> ast.Assign | None:
665
+ """Remove non-annotated assignment statements.
666
+
667
+ Args:
668
+ node: The Assign node to visit.
669
+
670
+ Returns:
671
+ The modified Assign node (or None).
672
+ """
673
+ # Special case for assignments to `typing.Any` as fallback.
674
+ if (
675
+ node.value is not None
676
+ and isinstance(node.value, ast.Name)
677
+ and node.value.id == "Any"
678
+ ):
679
+ return node
680
+
681
+ if self._current_class_is_component():
682
+ # Remove annotated assignments in Component classes (props)
683
+ return None
684
+
685
+ return node
686
+
687
+ def visit_AnnAssign(self, node: ast.AnnAssign) -> ast.AnnAssign | None:
688
+ """Visit an AnnAssign node (Annotated assignment).
689
+
690
+ Remove private target and remove the assignment value in the stub.
691
+
692
+ Args:
693
+ node: The AnnAssign node to visit.
694
+
695
+ Returns:
696
+ The modified AnnAssign node (or None).
697
+ """
698
+ # skip ClassVars
699
+ if (
700
+ isinstance(node.annotation, ast.Subscript)
701
+ and isinstance(node.annotation.value, ast.Name)
702
+ and node.annotation.value.id == "ClassVar"
703
+ ):
704
+ return node
705
+ if isinstance(node.target, ast.Name) and node.target.id.startswith("_"):
706
+ return None
707
+ if self.current_class in self.classes:
708
+ # Remove annotated assignments in Component classes (props)
709
+ return None
710
+ # Blank out assignments in type stubs.
711
+ node.value = None
712
+ return node
713
+
714
+
715
+ class PyiGenerator:
716
+ """A .pyi file generator that will scan all defined Component in Reflex and
717
+ generate the approriate stub.
718
+ """
719
+
720
+ modules: list = []
721
+ root: str = ""
722
+ current_module: Any = {}
723
+
724
+ def _write_pyi_file(self, module_path: Path, source: str):
725
+ relpath = str(_relative_to_pwd(module_path)).replace("\\", "/")
726
+ pyi_content = [
727
+ f'"""Stub file for {relpath}"""',
728
+ "# ------------------- DO NOT EDIT ----------------------",
729
+ "# This file was generated by `reflex/utils/pyi_generator.py`!",
730
+ "# ------------------------------------------------------",
731
+ "",
732
+ ]
733
+ if black is not None:
734
+ for formatted_line in black.format_file_contents(
735
+ src_contents=source,
736
+ fast=True,
737
+ mode=black.mode.Mode(is_pyi=True),
738
+ ).splitlines():
739
+ # Bit of a hack here, since the AST cannot represent comments.
740
+ if "def create(" in formatted_line:
741
+ pyi_content.append(formatted_line + " # type: ignore")
742
+ elif "Figure" in formatted_line:
743
+ pyi_content.append(formatted_line + " # type: ignore")
744
+ else:
745
+ pyi_content.append(formatted_line)
746
+ pyi_content.append("") # add empty line at the end for formatting
747
+ else:
748
+ pyi_content = source.splitlines()
749
+
750
+ pyi_path = module_path.with_suffix(".pyi")
751
+ pyi_path.write_text("\n".join(pyi_content))
752
+ logger.info(f"Wrote {relpath}")
753
+
754
+ def _scan_file(self, module_path: Path):
755
+ module_import = (
756
+ _relative_to_pwd(module_path)
757
+ .with_suffix("")
758
+ .as_posix()
759
+ .replace("/", ".")
760
+ .replace("\\", ".")
761
+ )
762
+ module = importlib.import_module(module_import)
763
+ logger.debug(f"Read {module_path}")
764
+ class_names = {
765
+ name: obj
766
+ for name, obj in vars(module).items()
767
+ if inspect.isclass(obj)
768
+ and (issubclass(obj, Component) or issubclass(obj, SimpleNamespace))
769
+ and obj != Component
770
+ and inspect.getmodule(obj) == module
771
+ }
772
+ if not class_names:
773
+ return
774
+
775
+ new_tree = StubGenerator(module, class_names).visit(
776
+ ast.parse(inspect.getsource(module))
777
+ )
778
+ self._write_pyi_file(module_path, ast.unparse(new_tree))
779
+
780
+ def _scan_files_multiprocess(self, files: list[Path]):
781
+ with Pool(processes=cpu_count()) as pool:
782
+ pool.map(self._scan_file, files)
783
+
784
+ def _scan_files(self, files: list[Path]):
785
+ for file in files:
786
+ self._scan_file(file)
787
+
788
+ def scan_all(self, targets, changed_files: list[Path] | None = None):
789
+ """Scan all targets for class inheriting Component and generate the .pyi files.
790
+
791
+ Args:
792
+ targets: the list of file/folders to scan.
793
+ changed_files (optional): the list of changed files since the last run.
794
+ """
795
+ file_targets = []
796
+ for target in targets:
797
+ target_path = Path(target)
798
+ if target_path.is_file() and target_path.suffix == ".py":
799
+ file_targets.append(target_path)
800
+ continue
801
+ if not target_path.is_dir():
802
+ continue
803
+ for file_path in _walk_files(target_path):
804
+ relative = _relative_to_pwd(file_path)
805
+ if relative.name in EXCLUDED_FILES or file_path.suffix != ".py":
806
+ continue
807
+ if (
808
+ changed_files is not None
809
+ and _relative_to_pwd(file_path) not in changed_files
810
+ ):
811
+ continue
812
+ file_targets.append(file_path)
813
+
814
+ # check if pyi changed but not the source
815
+ if changed_files is not None:
816
+ for changed_file in changed_files:
817
+ if changed_file.suffix != ".pyi":
818
+ continue
819
+ py_file_path = changed_file.with_suffix(".py")
820
+ if not py_file_path.exists() and changed_file.exists():
821
+ changed_file.unlink()
822
+ if py_file_path in file_targets:
823
+ continue
824
+ subprocess.run(["git", "checkout", changed_file])
825
+
826
+ if cpu_count() == 1 or len(file_targets) < 5:
827
+ self._scan_files(file_targets)
828
+ else:
829
+ self._scan_files_multiprocess(file_targets)
830
+
831
+
832
+ def generate_init():
833
+ """Generate a pyi file for the main __init__.py."""
834
+ from reflex import _MAPPING # type: ignore
835
+
836
+ imports = [
837
+ f"from {path if mod != path.rsplit('.')[-1] or mod == 'page' else '.'.join(path.rsplit('.')[:-1])} import {mod} as {mod}"
838
+ for mod, path in _MAPPING.items()
839
+ ]
840
+ imports.append("")
841
+ with contextlib.suppress(Exception):
842
+ INIT_FILE.write_text("\n".join(imports))