From a178f953136de4470b7e2662e7ca721c4bf65d03 Mon Sep 17 00:00:00 2001 From: Ahmad Hakim Date: Mon, 3 Aug 2026 18:26:23 +0200 Subject: [PATCH] chore: scripts + tests --- .github/workflows/validate-assets.yml | 3 + assets/docs/components/accordion.md | 2 +- assets/docs/components/attachment.md | 2 +- assets/docs/components/avatar.md | 2 +- assets/docs/components/bubble.md | 2 +- assets/docs/components/button-group.md | 32 +++----- assets/docs/components/card.md | 2 +- assets/docs/components/checkbox.md | 2 +- assets/docs/components/collapsible.md | 2 +- assets/docs/components/dialog.md | 2 +- assets/docs/components/field.md | 2 +- assets/docs/components/frame.md | 6 +- assets/docs/components/input-group.md | 6 +- assets/docs/components/input.md | 6 +- assets/docs/components/marker.md | 2 +- assets/docs/components/menu.md | 2 +- assets/docs/components/message.md | 2 +- assets/docs/components/select.md | 6 +- assets/docs/components/switch.md | 10 +-- assets/docs/components/table.md | 6 +- assets/docs/components/tabs.md | 2 +- components/ui/switch.py | 4 +- docs/components/button_group.md | 24 ++---- native/templates/_intro.py | 7 +- native/templates/_usage.py | 5 +- scripts/generate_markdown.py | 105 +++++++++++++++++++++++-- tests/test_docs_error.py | 78 ++++++++++++++++++ 27 files changed, 240 insertions(+), 84 deletions(-) create mode 100644 tests/test_docs_error.py diff --git a/.github/workflows/validate-assets.yml b/.github/workflows/validate-assets.yml index 4d58717..31e31f0 100644 --- a/.github/workflows/validate-assets.yml +++ b/.github/workflows/validate-assets.yml @@ -35,3 +35,6 @@ jobs: - name: Run asset verification tests run: uv run pytest tests/test_generated_assets.py + + - name: Run docs verification tests + run: uv run pytest tests/test_docs_error.py diff --git a/assets/docs/components/accordion.md b/assets/docs/components/accordion.md index 9246b76..113033b 100644 --- a/assets/docs/components/accordion.md +++ b/assets/docs/components/accordion.md @@ -9,7 +9,7 @@ order: 0 ```python -from components.ui.accordion import Accordion +from components.ui.accordion import accordion ``` ```python diff --git a/assets/docs/components/attachment.md b/assets/docs/components/attachment.md index 88a246f..fb1911e 100644 --- a/assets/docs/components/attachment.md +++ b/assets/docs/components/attachment.md @@ -9,7 +9,7 @@ order: 0 ```python -from components.ui.attachment import Attachment +from components.ui.attachment import attachment ``` ```python diff --git a/assets/docs/components/avatar.md b/assets/docs/components/avatar.md index 7410c0f..eac989d 100644 --- a/assets/docs/components/avatar.md +++ b/assets/docs/components/avatar.md @@ -9,7 +9,7 @@ order: 1 ```python -from components.ui.avatar import Avatar +from components.ui.avatar import avatar ``` ```python diff --git a/assets/docs/components/bubble.md b/assets/docs/components/bubble.md index 71a9aaa..99464d7 100644 --- a/assets/docs/components/bubble.md +++ b/assets/docs/components/bubble.md @@ -9,7 +9,7 @@ order: 0 ```python -from components.ui.bubble import Bubble +from components.ui.bubble import bubble ``` ```python diff --git a/assets/docs/components/button-group.md b/assets/docs/components/button-group.md index 149faf1..947b6d5 100644 --- a/assets/docs/components/button-group.md +++ b/assets/docs/components/button-group.md @@ -8,9 +8,9 @@ order: 3 ## Button Group, A Container That Groups Related Buttons Together With Consistent Styling. - -> Component `button_group` not found - +```python +from components.ui.button_group import button_group +``` ```python from typing import Any @@ -323,16 +323,6 @@ def button_group_dropdown() -> rx.Component: ) ``` -## Select - -Pair with a `select` component. - -**Props used:** see the [Select](/docs/components/select) docs for select-specific props. - - -> Component `button_group_select` not found - - # API Reference ## button_group.root @@ -344,10 +334,10 @@ button_group.root( ) ``` -| Prop | Type | Default | -| ------------- | -------------------------------------- | -------------- | -| `orientation` | `Literal["horizontal", "vertical"]` | `"horizontal"` | -| `class_name` | `str` | `""` | +| Prop | Type | Default | +| ------------- | ----------------------------------- | -------------- | +| `orientation` | `Literal["horizontal", "vertical"]` | `"horizontal"` | +| `class_name` | `str` | `""` | ## button_group.separator @@ -359,10 +349,10 @@ button_group.root( ) ``` -| Prop | Type | Default | -| ------------- | -------------------------------------- | ------------ | -| `orientation` | `Literal["horizontal", "vertical"]` | `"vertical"` | -| `class_name` | `str` | `""` | +| Prop | Type | Default | +| ------------- | ----------------------------------- | ------------ | +| `orientation` | `Literal["horizontal", "vertical"]` | `"vertical"` | +| `class_name` | `str` | `""` | ## button_group.text diff --git a/assets/docs/components/card.md b/assets/docs/components/card.md index 3c989c6..8847a20 100644 --- a/assets/docs/components/card.md +++ b/assets/docs/components/card.md @@ -9,7 +9,7 @@ order: 0 ```python -from components.ui.card import Card +from components.ui.card import card ``` ```python diff --git a/assets/docs/components/checkbox.md b/assets/docs/components/checkbox.md index fc94e40..875dc57 100644 --- a/assets/docs/components/checkbox.md +++ b/assets/docs/components/checkbox.md @@ -9,7 +9,7 @@ order: 5 ```python -from components.ui.checkbox import Checkbox +from components.ui.checkbox import checkbox ``` ```python diff --git a/assets/docs/components/collapsible.md b/assets/docs/components/collapsible.md index d25427f..9e567c8 100644 --- a/assets/docs/components/collapsible.md +++ b/assets/docs/components/collapsible.md @@ -9,7 +9,7 @@ order: 0 ```python -from components.ui.collapsible import Collapsible +from components.ui.collapsible import collapsible ``` ```python diff --git a/assets/docs/components/dialog.md b/assets/docs/components/dialog.md index d9f4a09..c11199f 100644 --- a/assets/docs/components/dialog.md +++ b/assets/docs/components/dialog.md @@ -9,7 +9,7 @@ order: 8 ```python -from components.ui.dialog import Dialog +from components.ui.dialog import dialog ``` ```python diff --git a/assets/docs/components/field.md b/assets/docs/components/field.md index 5ce0116..ce01dde 100644 --- a/assets/docs/components/field.md +++ b/assets/docs/components/field.md @@ -9,7 +9,7 @@ order: 0 ```python -from components.ui.field import Field +from components.ui.field import field ``` ```python diff --git a/assets/docs/components/frame.md b/assets/docs/components/frame.md index aaf564c..d69f870 100644 --- a/assets/docs/components/frame.md +++ b/assets/docs/components/frame.md @@ -8,9 +8,9 @@ order: 0 ## Frame, Displays Related Content In A Structured Frame. - -> Error processing `usage`: module, class, method, function, traceback, frame, or code object was expected, got Frame - +```python +from components.ui.frame import frame +``` ```python from typing import Any diff --git a/assets/docs/components/input-group.md b/assets/docs/components/input-group.md index 1d5f7fa..c19daf4 100644 --- a/assets/docs/components/input-group.md +++ b/assets/docs/components/input-group.md @@ -8,9 +8,9 @@ order: 0 ## Input Group, Add Addons, Buttons, And Helper Content To Inputs. - -> Component `input_group` not found - +```python +from components.ui.input_group import input_group +``` ```python from typing import Any diff --git a/assets/docs/components/input.md b/assets/docs/components/input.md index 862edf7..048076e 100644 --- a/assets/docs/components/input.md +++ b/assets/docs/components/input.md @@ -8,9 +8,9 @@ order: 11 ## Input, A Text Input Component For Forms And User Data Entry With Built-In Styling And Accessibility Features. - -> Error processing `usage`: module, class, method, function, traceback, frame, or code object was expected, got Input - +```python +from components.ui.input import input +``` ```python from typing import Any diff --git a/assets/docs/components/marker.md b/assets/docs/components/marker.md index a22137b..ec68057 100644 --- a/assets/docs/components/marker.md +++ b/assets/docs/components/marker.md @@ -9,7 +9,7 @@ order: 0 ```python -from components.ui.marker import Marker +from components.ui.marker import marker ``` ```python diff --git a/assets/docs/components/menu.md b/assets/docs/components/menu.md index e2faf22..5b2e647 100644 --- a/assets/docs/components/menu.md +++ b/assets/docs/components/menu.md @@ -9,7 +9,7 @@ order: 0 ```python -from components.ui.menu import Menu +from components.ui.menu import menu ``` ```python diff --git a/assets/docs/components/message.md b/assets/docs/components/message.md index 4a7a755..450e90d 100644 --- a/assets/docs/components/message.md +++ b/assets/docs/components/message.md @@ -9,7 +9,7 @@ order: 13 ```python -from components.ui.message import Message +from components.ui.message import message ``` ```python diff --git a/assets/docs/components/select.md b/assets/docs/components/select.md index 5861952..63d6959 100644 --- a/assets/docs/components/select.md +++ b/assets/docs/components/select.md @@ -8,9 +8,9 @@ order: 13 ## Select, A Styled Native Html Select Element With Consistent Design System Integration. - -> Error processing `usage`: module, class, method, function, traceback, frame, or code object was expected, got NativeSelect - +```python +from components.ui.select import select +``` ```python from typing import Any diff --git a/assets/docs/components/switch.md b/assets/docs/components/switch.md index 44246dd..cf6188f 100644 --- a/assets/docs/components/switch.md +++ b/assets/docs/components/switch.md @@ -8,9 +8,9 @@ order: 0 ## Switch, A Control That Allows The User To Toggle Between Checked And Not Checked. - -> Component `switch` not found - +```python +from components.ui.switch import switch +``` ```python from typing import Any @@ -146,12 +146,12 @@ class NativeSwitch(CoreComponent): ) -class SwitchNamespace(ComponentNamespace): +class Switch(ComponentNamespace): root = staticmethod(NativeSwitch.create) class_names = ClassNames -switch = SwitchNamespace() +switch = Switch() ``` # Examples diff --git a/assets/docs/components/table.md b/assets/docs/components/table.md index 62a82c7..c0e2788 100644 --- a/assets/docs/components/table.md +++ b/assets/docs/components/table.md @@ -8,9 +8,9 @@ order: 0 ## Table, Powerful Table And Datagrids With Built-In Features. - -> Component `table` not found - +```python +from components.ui.table import table +``` ```python from typing import Any diff --git a/assets/docs/components/tabs.md b/assets/docs/components/tabs.md index 866dde9..e60f982 100644 --- a/assets/docs/components/tabs.md +++ b/assets/docs/components/tabs.md @@ -9,7 +9,7 @@ order: 0 ```python -from components.ui.tabs import Tabs +from components.ui.tabs import tabs ``` ```python diff --git a/components/ui/switch.py b/components/ui/switch.py index 9c4112b..68dca94 100644 --- a/components/ui/switch.py +++ b/components/ui/switch.py @@ -82,9 +82,9 @@ def create(cls, label_text: str = "", **props) -> rx.Component: ) -class SwitchNamespace(ComponentNamespace): +class Switch(ComponentNamespace): root = staticmethod(NativeSwitch.create) class_names = ClassNames -switch = SwitchNamespace() +switch = Switch() diff --git a/docs/components/button_group.md b/docs/components/button_group.md index 0de72f9..7fd0c7e 100644 --- a/docs/components/button_group.md +++ b/docs/components/button_group.md @@ -66,14 +66,6 @@ A split button group with a `menu` as the second segment. --DEMO(button_group_dropdown)-- -## Select - -Pair with a `select` component. - -**Props used:** see the [Select](/docs/components/select) docs for select-specific props. - ---DEMO(button_group_select)-- - # API Reference ## button_group.root @@ -85,10 +77,10 @@ button_group.root( ) ``` -| Prop | Type | Default | -| ------------- | -------------------------------------- | -------------- | -| `orientation` | `Literal["horizontal", "vertical"]` | `"horizontal"` | -| `class_name` | `str` | `""` | +| Prop | Type | Default | +| ------------- | ----------------------------------- | -------------- | +| `orientation` | `Literal["horizontal", "vertical"]` | `"horizontal"` | +| `class_name` | `str` | `""` | ## button_group.separator @@ -100,10 +92,10 @@ button_group.root( ) ``` -| Prop | Type | Default | -| ------------- | -------------------------------------- | ------------ | -| `orientation` | `Literal["horizontal", "vertical"]` | `"vertical"` | -| `class_name` | `str` | `""` | +| Prop | Type | Default | +| ------------- | ----------------------------------- | ------------ | +| `orientation` | `Literal["horizontal", "vertical"]` | `"vertical"` | +| `class_name` | `str` | `""` | ## button_group.text diff --git a/native/templates/_intro.py b/native/templates/_intro.py index 447bf10..bf1fc21 100644 --- a/native/templates/_intro.py +++ b/native/templates/_intro.py @@ -112,11 +112,13 @@ def command_selector(): def intro(raw_arg): intro = raw_arg.strip("[]").strip() + print(intro) + if "," not in intro: raise ValueError(f"Invalid INTRO format: {raw_arg!r}") title, description = intro.split(",", 1) - slug = title.strip().lower().replace(" ", "-") + slug = title.strip().lower().replace(" ", "_") copy_id = f"copy-command-{slug}" command_id = f"command-{slug}" @@ -142,7 +144,7 @@ def intro(raw_arg): class_name="text-muted-foreground command-prefix", ), rx.el.span( - f" {title.strip().lower()}", + f" {slug}", class_name="font-medium text-foreground", ), class_name=( @@ -188,6 +190,5 @@ def intro(raw_arg): ), class_name="flex flex-col items-start gap-4 max-w-xl", ), - # rx.el.button("Open Full Preview"), class_name="flex flex-col gap-6 sm:flex-row sm:items-start sm:justify-between mb-10", ) diff --git a/native/templates/_usage.py b/native/templates/_usage.py index aa3927d..f772896 100644 --- a/native/templates/_usage.py +++ b/native/templates/_usage.py @@ -87,6 +87,9 @@ def _usage_demo(component_name: str): def usage(raw_arg): + + name = raw_arg.replace("_", " ").title() + return div( div( h1( @@ -95,7 +98,7 @@ def usage(raw_arg): ), p( span("Use the following to build the "), - strong(raw_arg.capitalize()), + strong(name), span(" component."), class_name="text-sm text-muted-foreground", ), diff --git a/scripts/generate_markdown.py b/scripts/generate_markdown.py index c0bc0dd..2e2b7ca 100644 --- a/scripts/generate_markdown.py +++ b/scripts/generate_markdown.py @@ -10,6 +10,8 @@ ROOT_DIR = Path(__file__).parent.parent sys.path.append(str(ROOT_DIR)) +from reflex.components.component import ComponentNamespace + from native.registry.components import COMPONENT_REGISTRY # --------------------------------------------------------- @@ -55,13 +57,60 @@ def build_live_registry(dirs: list[str]) -> dict[str, tuple[object, str]]: if name.startswith("_"): continue - if inspect.isfunction(obj) or inspect.isclass(obj) or callable(obj): - if getattr(obj, "__module__", None) == module_name: - registry[name.lower()] = (obj, name) + is_valid_obj = ( + inspect.isfunction(obj) + or inspect.isclass(obj) + or callable(obj) + or isinstance(obj, ComponentNamespace) + ) + + if is_valid_obj: + target_cls = ( + type(obj) if isinstance(obj, ComponentNamespace) else obj + ) + obj_module = getattr(target_cls, "__module__", None) + + if obj_module == module_name: + key = name.lower() + # Save both underscore AND hyphenated versions in LIVE_REGISTRY! + registry[key] = (obj, name) + registry[key.replace("_", "-")] = (obj, name) return registry +# def build_live_registry(dirs: list[str]) -> dict[str, tuple[object, str]]: +# registry = {} + +# for folder in dirs: +# base = ROOT_DIR / folder + +# if not base.exists(): +# continue + +# for py_file in base.rglob("*.py"): +# if py_file.name.startswith("__"): +# continue + +# module_name = ".".join(py_file.relative_to(ROOT_DIR).with_suffix("").parts) + +# try: +# module = importlib.import_module(module_name) +# except Exception as e: +# print(f"Could not import {module_name}: {e}") +# continue + +# for name, obj in vars(module).items(): +# if name.startswith("_"): +# continue + +# if inspect.isfunction(obj) or inspect.isclass(obj) or callable(obj): +# if getattr(obj, "__module__", None) == module_name: +# registry[name.lower()] = (obj, name) + +# return registry + + LIVE_REGISTRY = build_live_registry(DYNAMIC_LOAD_DIRS) @@ -78,7 +127,9 @@ def get_dependencies(name: str): ordered = [] def resolve(component): - data = COMPONENT_REGISTRY.get(component.lower()) + # Normalize key so button-group matches button_group in COMPONENT_REGISTRY + comp_key = component.lower().replace("-", "_") + data = COMPONENT_REGISTRY.get(comp_key) if not data: return @@ -87,14 +138,35 @@ def resolve(component): if dep not in ordered: resolve(dep) - if component not in ordered: - ordered.append(component) + if comp_key not in ordered: + ordered.append(comp_key) resolve(name) return ordered +# def get_dependencies(name: str): +# ordered = [] + +# def resolve(component): +# data = COMPONENT_REGISTRY.get(component.lower()) + +# if not data: +# return + +# for dep in data.get("dependencies", []): +# if dep not in ordered: +# resolve(dep) + +# if component not in ordered: +# ordered.append(component) + +# resolve(name) + +# return ordered + + def read_component_files(name: str): output = [] @@ -144,19 +216,36 @@ def replace(match): # ----------------------------- if cmd == "usage": - entry = LIVE_REGISTRY.get(name) + entry = LIVE_REGISTRY.get(name) or LIVE_REGISTRY.get( + name.replace("-", "_") + ) if not entry: return f"\n> Component `{name}` not found\n" obj, preferred_name = entry - file = Path(inspect.getfile(obj)).stem + target = type(obj) if isinstance(obj, ComponentNamespace) else obj + file = Path(inspect.getfile(target)).stem return ( f"```python\nfrom components.ui.{file} import {preferred_name}\n```" ) + # if cmd == "usage": + # entry = LIVE_REGISTRY.get(name) + + # if not entry: + # return f"\n> Component `{name}` not found\n" + + # obj, preferred_name = entry + + # file = Path(inspect.getfile(obj)).stem + + # return ( + # f"```python\nfrom components.ui.{file} import {preferred_name}\n```" + # ) + # ----------------------------- # DEMO # ----------------------------- diff --git a/tests/test_docs_error.py b/tests/test_docs_error.py new file mode 100644 index 0000000..f1b7ade --- /dev/null +++ b/tests/test_docs_error.py @@ -0,0 +1,78 @@ +"""Scans every markdown file under assets/docs/ for component-resolution +errors that indicate a doc's embedded demo/code reference (e.g. a +--DEMO(...)-- block) failed to resolve to a real component, such as: + + Component `button_group_select` not found + Component `input_group` not found + +These strings mean something in the doc didn't get parsed/rendered +correctly and needs fixing in either the markdown or the component +registry — this test exists to catch that automatically instead of +someone spotting it by eye while browsing the docs site. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +import pytest + +# Matches: Component `` not found +# Extend this if other renderers/registries use a differently-worded error. +COMPONENT_NOT_FOUND_RE = re.compile(r"Component `([^`]+)` not found") + + +def _find_docs_dir() -> Path: + """Locate assets/docs/, searching upward from this file if the current + working directory isn't the repo root (e.g. running pytest from a + subdirectory, or from an IDE's own cwd).""" + candidate = Path.cwd() / "assets" / "docs" + if candidate.is_dir(): + return candidate + + for parent in Path(__file__).resolve().parents: + candidate = parent / "assets" / "docs" + if candidate.is_dir(): + return candidate + + raise FileNotFoundError( + "Could not locate an 'assets/docs' directory from the current " + "working directory or any parent of this test file. If your docs " + "live somewhere else, update _find_docs_dir()." + ) + + +def _iter_markdown_files(docs_dir: Path) -> list[Path]: + return sorted(p for ext in ("*.md", "*.mdx") for p in docs_dir.rglob(ext)) + + +DOCS_DIR = _find_docs_dir() +MARKDOWN_FILES = _iter_markdown_files(DOCS_DIR) + + +def test_docs_dir_is_not_empty(): + """Sanity check for the parametrized test below. + + If MARKDOWN_FILES is empty, pytest.mark.parametrize silently generates + zero test cases rather than failing — so test_no_unresolved_components + would report "0 passed" and look completely fine while actually + checking nothing. This test exists so a broken glob/path shows up as an + explicit failure instead of quiet false confidence. + """ + assert MARKDOWN_FILES, f"No markdown files found under {DOCS_DIR}" + + +@pytest.mark.parametrize( + "md_file", + MARKDOWN_FILES, + ids=[str(p.relative_to(DOCS_DIR)) for p in MARKDOWN_FILES], +) +def test_no_unresolved_components(md_file: Path): + content = md_file.read_text(encoding="utf-8") + matches = COMPONENT_NOT_FOUND_RE.findall(content) + + assert not matches, ( + f"{md_file.relative_to(DOCS_DIR)} references component(s) that " + f"failed to resolve: {', '.join(sorted(set(matches)))}" + )