Skip to content

Commit f830f37

Browse files
committed
pyi_generator: Recognize post-prop docstring
1 parent fcd7d96 commit f830f37

File tree

3 files changed

+74
-1
lines changed

3 files changed

+74
-1
lines changed

packages/reflex-base/src/reflex_base/utils/pyi_generator.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,28 +314,71 @@ def _get_class_prop_comments(clz: type[Component]) -> Mapping[str, tuple[str, ..
314314
"""
315315
props_comments: dict[str, tuple[str, ...]] = {}
316316
comments = []
317+
last_prop = ""
318+
in_docstring = False
319+
docstring_lines: list[str] = []
317320
for line in _get_source(clz).splitlines():
318321
reached_functions = re.search(r"def ", line)
319322
if reached_functions:
320323
# We've reached the functions, so stop.
321324
break
322325

326+
stripped = line.strip()
327+
328+
# Handle triple-quoted docstrings after prop definitions.
329+
if in_docstring:
330+
if '"""' in stripped or "'''" in stripped:
331+
# End of multi-line docstring.
332+
end_text = stripped.partition('"""')[0] or stripped.partition("'''")[0]
333+
if end_text:
334+
docstring_lines.append(end_text.strip())
335+
if last_prop and docstring_lines:
336+
props_comments[last_prop] = tuple(docstring_lines)
337+
in_docstring = False
338+
docstring_lines = []
339+
last_prop = ""
340+
else:
341+
docstring_lines.append(stripped)
342+
continue
343+
344+
# Check for start of a docstring right after a prop.
345+
if last_prop and (stripped.startswith(('"""', "'''"))):
346+
quote = '"""' if stripped.startswith('"""') else "'''"
347+
content_after_open = stripped[3:]
348+
if quote in content_after_open:
349+
# Single-line docstring: """text"""
350+
doc_text = content_after_open.partition(quote)[0].strip()
351+
if doc_text:
352+
props_comments[last_prop] = (doc_text,)
353+
last_prop = ""
354+
else:
355+
# Multi-line docstring starts here.
356+
in_docstring = True
357+
docstring_lines = []
358+
first_line = content_after_open.strip()
359+
if first_line:
360+
docstring_lines.append(first_line)
361+
continue
362+
323363
if line == "":
324364
# We hit a blank line, so clear comments to avoid commented out prop appearing in next prop docs.
325365
comments.clear()
366+
last_prop = ""
326367
continue
327368

328369
# Get comments for prop
329-
if line.strip().startswith("#"):
370+
if stripped.startswith("#"):
330371
# Remove noqa from the comments.
331372
line = line.partition(" # noqa")[0]
332373
comments.append(line)
374+
last_prop = ""
333375
continue
334376

335377
# Check if this line has a prop.
336378
match = re.search(r"\w+:", line)
337379
if match is None:
338380
# This line doesn't have a var, so continue.
381+
last_prop = ""
339382
continue
340383

341384
# Get the prop.
@@ -345,6 +388,7 @@ def _get_class_prop_comments(clz: type[Component]) -> Mapping[str, tuple[str, ..
345388
comment.strip().lstrip("#").strip() for comment in comments
346389
)
347390
comments.clear()
391+
last_prop = prop
348392

349393
return MappingProxyType(props_comments)
350394

@@ -1286,6 +1330,23 @@ def visit_Assign(self, node: ast.Assign) -> ast.Assign | None:
12861330

12871331
return node
12881332

1333+
def visit_Expr(self, node: ast.Expr) -> ast.Expr | None:
1334+
"""Remove bare string expressions (attribute docstrings) in component classes.
1335+
1336+
Args:
1337+
node: The Expr node to visit.
1338+
1339+
Returns:
1340+
The modified Expr node (or None).
1341+
"""
1342+
if (
1343+
self._current_class_is_component()
1344+
and isinstance(node.value, ast.Constant)
1345+
and isinstance(node.value.value, str)
1346+
):
1347+
return None
1348+
return node
1349+
12891350
def visit_AnnAssign(self, node: ast.AnnAssign) -> ast.AnnAssign | None:
12901351
"""Visit an AnnAssign node (Annotated assignment).
12911352

tests/units/reflex_base/utils/pyi_generator/dataset/simple_component.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- Default event handlers inherited from Component
77
- Props with doc strings (via field(doc=...))
88
- Props with comment-based docs (# comment above prop)
9+
- Props with inline docstrings (triple-quoted string after prop)
910
- Module docstring removal in stubs
1011
- visit_Assign: assignment to `Any` is preserved
1112
- visit_Assign: non-annotated assignments are removed
@@ -41,6 +42,13 @@ class SimpleComponent(Component):
4142
doc="An optional label with a default value.",
4243
)
4344

45+
description: Var[str]
46+
"""A detailed description of the component."""
47+
48+
tooltip: Var[str]
49+
"""A tooltip that appears on hover
50+
with additional details."""
51+
4452
def _private_method(self):
4553
"""This should not appear in the stub.
4654

tests/units/reflex_base/utils/pyi_generator/golden/simple_component.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ class SimpleComponent(Component):
2525
is_active: Var[bool] | bool | None = None,
2626
opacity: Var[float] | float | None = None,
2727
label: Var[str] | str | None = None,
28+
description: Var[str] | str | None = None,
29+
tooltip: Var[str] | str | None = None,
2830
style: Sequence[Mapping[str, Any]]
2931
| Mapping[str, Any]
3032
| Var[Mapping[str, Any]]
@@ -62,6 +64,8 @@ class SimpleComponent(Component):
6264
is_active: Whether the component is active.
6365
opacity: The opacity of the component.
6466
label: An optional label with a default value.
67+
description: A detailed description of the component.
68+
tooltip: A tooltip that appears on hover with additional details.
6569
style: The style of the component.
6670
key: A unique key for the component.
6771
id: The id for the component.

0 commit comments

Comments
 (0)